Repository: un-ts/eslint-plugin-import-x
Pull request #329 (2025-05-18 02:59:52)

feat: add suggestions support for `extensions` `unexpected` case
follow #327
<!-- ELLIPSIS_HIDDEN -->


----

> [!IMPORTANT]
> Adds suggestions for unexpected file extensions and improves import path handling in `extensions.ts`, with expanded test coverage.
> 
>   - **New Features**:
>     - Added suggestions for unexpected file extensions in `extensions.ts`, enhancing feedback and autofix capabilities.
>   - **Bug Fixes**:
>     - Improved import path handling in `extensions.ts` to preserve quote styles and support query/hash components.
>   - **Tests**:
>     - Expanded test coverage in `extensions.spec.ts` with detailed autofix suggestions for invalid imports.
>     - Improved snapshot identification in `parse-path.spec.ts` for better clarity.
> 
> <sup>This description was created by </sup>[<img alt="Ellipsis" src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=un-ts%2Feslint-plugin-import-x&utm_source=github&utm_medium=referral)<sup> for 813a4a087aa614a5577fe01c7f135317992d31ca. You can [customize](https://app.ellipsis.dev/un-ts/settings/summaries) this summary. It will automatically update as commits are pushed.</sup>

----


<!-- ELLIPSIS_HIDDEN -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added suggestions support for handling unexpected file extensions in import statements, enhancing the plugin's feedback and autofix capabilities.

- **Bug Fixes**
  - Improved handling of import path fixes to preserve original quote styles and support query/hash components.

- **Tests**
  - Expanded test coverage with detailed autofix suggestions for invalid import cases, including imports with query strings and hashes.
  - Improved snapshot identification in path parsing tests for better clarity.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: 46585a6c52b72bab67a1b3fb3f2c9325f5606bdc

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/un-ts/eslint-plugin-import-x/new/feat/extensions_suggestions?filename=.changeset/weak-points-pull.md&value=---%0A%22eslint-plugin-import-x%22%3A%20patch%0A---%0A%0Afeat%3A%20add%20suggestions%20support%20for%20%60extensions%60%20%60unexpected%60%20case%0A)


coderabbitai[bot]: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: review in progress by coderabbit.ai -->

> [!NOTE]
> Currently processing new changes in this PR. This may take a few minutes, please wait...
> 
> <details>
> <summary>📥 Commits</summary>
> 
> Reviewing files that changed from the base of the PR and between b80490e5b7d9d8505b1caa2f0a3a14c53bb83609 and 46585a6c52b72bab67a1b3fb3f2c9325f5606bdc.
> 
> </details>
> 
> <details>
> <summary>⛔ Files ignored due to path filters (1)</summary>
> 
> * `test/utils/__snapshots__/parse-path.spec.ts.snap` is excluded by `!**/*.snap`
> 
> </details>
> 
> <details>
> <summary>📒 Files selected for processing (3)</summary>
> 
> * `src/rules/extensions.ts` (9 hunks)
> * `test/rules/extensions.spec.ts` (19 hunks)
> * `test/utils/parse-path.spec.ts` (1 hunks)
> 
> </details>
> 
> ```ascii
>  __________________________________
> < Jazz Jackrabbit is my alter ego. >
>  ----------------------------------
>   \
>    \   (\__/)
>        (•ㅅ•)
>        / 　 づ
> ```

<!-- end of auto-generated comment: review in progress by coderabbit.ai -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=un-ts/eslint-plugin-import-x&utm_content=329):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
petercat-assistant[bot]: ### Walkthrough
This pull request introduces support for suggestions in the `extensions` rule, specifically for handling the `unexpected` case. It adds functionality to provide suggestions for removing unexpected file extensions in import paths, enhancing the rule's capability to guide developers in maintaining consistent import statements.

### Changes
| File | Summary |
|------|---------|
| src/rules/extensions.ts | Added support for suggestions to remove unexpected file extensions in import paths. Introduced `fixSource` function and updated `MessageId` type. |
| test/rules/extensions.spec.ts | Added test cases to verify the new suggestion feature for removing unexpected file extensions. |
| test/utils/__snapshots__/parse-path.spec.ts.snap | Updated snapshots to reflect changes in path parsing behavior. |
| test/utils/parse-path.spec.ts | Modified test to include input in snapshot matching for better clarity. |
codesandbox-ci[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/un-ts/eslint-plugin-import-x/pr/329/builds/617259) or the icon next to each commit SHA.
sonarqubecloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=un-ts_eslint-plugin-import-x&pullRequest=329) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [16.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=un-ts_eslint-plugin-import-x&pullRequest=329&metric=new_duplicated_lines_density&view=list) (required ≤ 3%)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=un-ts_eslint-plugin-import-x&pullRequest=329)


pkg-pr-new[bot]: 
[Open in StackBlitz](https://pkg.pr.new/template/68676e08-8de8-41d3-ae93-a20bed821713)


```
npm i https://pkg.pr.new/eslint-plugin-import-x@329
```


_commit: <a href="https://github.com/un-ts/eslint-plugin-import-x/runs/42421038211"><code>5898310</code></a>_

sonarqubecloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=un-ts_eslint-plugin-import-x&pullRequest=329) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [16.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=un-ts_eslint-plugin-import-x&pullRequest=329&metric=new_duplicated_lines_density&view=list) (required ≤ 3%)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=un-ts_eslint-plugin-import-x&pullRequest=329)


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/un-ts/eslint-plugin-import-x/pull/329?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=un-ts) Report
Attention: Patch coverage is `93.10345%` with `2 lines` in your changes missing coverage. Please review.
> Project coverage is 96.01%. Comparing base [(`b80490e`)](https://app.codecov.io/gh/un-ts/eslint-plugin-import-x/commit/b80490e5b7d9d8505b1caa2f0a3a14c53bb83609?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=un-ts) to head [(`5898310`)](https://app.codecov.io/gh/un-ts/eslint-plugin-import-x/commit/589831081cb3f2a7ec123fc29f63e2d2bf2b9b64?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=un-ts).

| [Files with missing lines](https://app.codecov.io/gh/un-ts/eslint-plugin-import-x/pull/329?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=un-ts) | Patch % | Lines |
|---|---|---|
| [src/rules/extensions.ts](https://app.codecov.io/gh/un-ts/eslint-plugin-import-x/pull/329?src=pr&el=tree&filepath=src%2Frules%2Fextensions.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=un-ts#diff-c3JjL3J1bGVzL2V4dGVuc2lvbnMudHM=) | 93.10% | [1 Missing and 1 partial :warning: ](https://app.codecov.io/gh/un-ts/eslint-plugin-import-x/pull/329?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=un-ts) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #329      +/-   ##
==========================================
- Coverage   96.04%   96.01%   -0.03%     
==========================================
  Files          93       93              
  Lines        4802     4818      +16     
  Branches     1818     1826       +8     
==========================================
+ Hits         4612     4626      +14     
- Misses        189      190       +1     
- Partials        1        2       +1     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/un-ts/eslint-plugin-import-x/pull/329?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=un-ts).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=un-ts).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
codacy-production[bot]: ## Coverage summary from Codacy
### [See diff coverage on Codacy](https://app.codacy.com/gh/un-ts/eslint-plugin-import-x/pull-requests/329/coverage?utm_source=github.com&utm_medium=coverageSummary&utm_campaign=coverageSummaryPullRequest)

 | Coverage variation | Diff coverage | 
 | ------------- | ------------- |
 | :white_check_mark: **+0.01%** (target: -1.00%) | :white_check_mark: **100.00%** |

<details>
<summary>Coverage variation details</summary>

| | Coverable lines | Covered lines | Coverage |
| ------------- | ------------- | ------------- | ------------- |
| Common ancestor commit (b80490e5b7d9d8505b1caa2f0a3a14c53bb83609) | 3596 | 3438 | 95.61% | |
| Head commit (589831081cb3f2a7ec123fc29f63e2d2bf2b9b64) | 3603 (+7) | 3445 (+7) | 95.61% (**+0.01%**) |

**Coverage variation** is the difference between the coverage for the head and common ancestor commits of the pull request branch: `<coverage of head commit> - <coverage of common ancestor commit>`
</details>

<details>
<summary>Diff coverage details</summary>

| | Coverable lines | Covered lines | Diff coverage |
| ------------- | ------------- | ------------- | ------------- |
| Pull request (#329) | 11 | 11 | **100.00%** |

**Diff coverage** is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: `<covered lines added or modified>/<coverable lines added or modified> * 100%`
</details>

#### [See your quality gate settings](https://app.codacy.com/gh/un-ts/eslint-plugin-import-x/settings/quality?utm_source=github.com&utm_medium=coverageSummary&utm_campaign=coverageSummaryGates)&nbsp;&nbsp;&nbsp;&nbsp;[Change summary preferences](https://app.codacy.com/gh/un-ts/eslint-plugin-import-x/settings/integrations?utm_source=github.com&utm_medium=coverageSummary&utm_campaign=coverageSummarySettings)

<!-- 108f945d-e553-4918-8ccf-027c6d4acc2c -->
sonarqubecloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=un-ts_eslint-plugin-import-x&pullRequest=329) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [16.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=un-ts_eslint-plugin-import-x&pullRequest=329&metric=new_duplicated_lines_density&view=list) (required ≤ 3%)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=un-ts_eslint-plugin-import-x&pullRequest=329)


sonarqubecloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=un-ts_eslint-plugin-import-x&pullRequest=329) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [15.7% Duplication on New Code](https://sonarcloud.io/component_measures?id=un-ts_eslint-plugin-import-x&pullRequest=329&metric=new_duplicated_lines_density&view=list) (required ≤ 3%)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=un-ts_eslint-plugin-import-x&pullRequest=329)


sonarqubecloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=un-ts_eslint-plugin-import-x&pullRequest=329) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [15.7% Duplication on New Code](https://sonarcloud.io/component_measures?id=un-ts_eslint-plugin-import-x&pullRequest=329&metric=new_duplicated_lines_density&view=list) (required ≤ 3%)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=un-ts_eslint-plugin-import-x&pullRequest=329)


sonarqubecloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=un-ts_eslint-plugin-import-x&pullRequest=329) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [15.4% Duplication on New Code](https://sonarcloud.io/component_measures?id=un-ts_eslint-plugin-import-x&pullRequest=329&metric=new_duplicated_lines_density&view=list) (required ≤ 3%)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=un-ts_eslint-plugin-import-x&pullRequest=329)


sonarqubecloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=un-ts_eslint-plugin-import-x&pullRequest=329) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [13.8% Duplication on New Code](https://sonarcloud.io/component_measures?id=un-ts_eslint-plugin-import-x&pullRequest=329&metric=new_duplicated_lines_density&view=list) (required ≤ 3%)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=un-ts_eslint-plugin-import-x&pullRequest=329)


sonarqubecloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=un-ts_eslint-plugin-import-x&pullRequest=329) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [23.7% Duplication on New Code](https://sonarcloud.io/component_measures?id=un-ts_eslint-plugin-import-x&pullRequest=329&metric=new_duplicated_lines_density&view=list) (required ≤ 3%)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=un-ts_eslint-plugin-import-x&pullRequest=329)



Patch:
diff --git a/.changeset/weak-points-pull.md b/.changeset/weak-points-pull.md
new file mode 100644
index 00000000..6fb086b2
--- /dev/null
+++ b/.changeset/weak-points-pull.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-import-x": patch
+---
+
+feat: add suggestions support for `extensions` `unexpected` case
diff --git a/src/rules/extensions.ts b/src/rules/extensions.ts
index 27b3dbef..29d177e2 100644
--- a/src/rules/extensions.ts
+++ b/src/rules/extensions.ts
@@ -5,7 +5,7 @@ import type { RuleFixer } from '@typescript-eslint/utils/ts-eslint'
 import { minimatch } from 'minimatch'
 import type { MinimatchOptions } from 'minimatch'
 
-import type { FileExtension, RuleContext } from '../types.js'
+import type { RuleContext } from '../types.js'
 import {
   isBuiltIn,
   isExternalModule,
@@ -105,7 +105,12 @@ export interface NormalizedOptions {
   fix?: boolean
 }
 
-export type MessageId = 'missing' | 'missingKnown' | 'unexpected' | 'addMissing'
+export type MessageId =
+  | 'missing'
+  | 'missingKnown'
+  | 'unexpected'
+  | 'addMissing'
+  | 'removeUnexpected'
 
 function buildProperties(context: RuleContext<MessageId, Options>) {
   const result: Required<NormalizedOptions> = {
@@ -188,6 +193,20 @@ function computeOverrideAction(
   }
 }
 
+/**
+ * Replaces the import path in a source string with a new import path.
+ *
+ * @param source - The original source string containing the import statement.
+ * @param importPath - The new import path to replace the existing one.
+ * @returns The updated source string with the replaced import path.
+ */
+function replaceImportPath(source: string, importPath: string) {
+  return source.replace(
+    /^(['"])(.+)\1$/,
+    (_, quote: string) => `${quote}${importPath}${quote}`,
+  )
+}
+
 export default createRule<Options, MessageId>({
   name: 'extensions',
   meta: {
@@ -236,27 +255,26 @@ export default createRule<Options, MessageId>({
         'Unexpected use of file extension "{{extension}}" for "{{importPath}}"',
       addMissing:
         'Add "{{extension}}" file extension from "{{importPath}}" into "{{fixedImportPath}}"',
+      removeUnexpected:
+        'Remove unexpected "{{extension}}" file extension from "{{importPath}}" into "{{fixedImportPath}}"',
     },
   },
   defaultOptions: [],
   create(context) {
     const props = buildProperties(context)
 
-    function getModifier(extension: FileExtension) {
+    function getModifier(extension: string) {
       return props.pattern[extension] || props.defaultConfig
     }
 
-    function isUseOfExtensionRequired(
-      extension: FileExtension,
-      isPackage: boolean,
-    ) {
+    function isUseOfExtensionRequired(extension: string, isPackage: boolean) {
       return (
         getModifier(extension) === 'always' &&
         (!props.ignorePackages || !isPackage)
       )
     }
 
-    function isUseOfExtensionForbidden(extension: FileExtension) {
+    function isUseOfExtensionForbidden(extension: string) {
       return getModifier(extension) === 'never'
     }
 
@@ -297,7 +315,11 @@ export default createRule<Options, MessageId>({
           return
         }
 
-        const importPath = importPathWithQueryString.replace(/\?(.*)$/, '')
+        const {
+          pathname: importPath,
+          query,
+          hash,
+        } = parsePath(importPathWithQueryString)
 
         // don't enforce in root external packages as they may have names with `.js`.
         // Like `import Decimal from decimal.js`)
@@ -309,9 +331,7 @@ export default createRule<Options, MessageId>({
 
         // get extension from resolved path, if possible.
         // for unresolved, use source value.
-        const extension = path
-          .extname(resolvedPath || importPath)
-          .slice(1) as FileExtension
+        const extension = path.extname(resolvedPath || importPath).slice(1)
 
         // determine if this is a module
         const isPackage =
@@ -336,16 +356,15 @@ export default createRule<Options, MessageId>({
           )
           const extensionForbidden = isUseOfExtensionForbidden(extension)
           if (extensionRequired && !extensionForbidden) {
-            const { pathname, query, hash } = parsePath(
-              importPathWithQueryString,
-            )
             const fixedImportPath = stringifyPath({
               pathname: `${
-                /([\\/]|[\\/]?\.?\.)$/.test(pathname)
+                /([\\/]|[\\/]?\.?\.)$/.test(importPath)
                   ? `${
-                      pathname.endsWith('/') ? pathname.slice(0, -1) : pathname
+                      importPath.endsWith('/')
+                        ? importPath.slice(0, -1)
+                        : importPath
                     }/index.${extension}`
-                  : `${pathname}.${extension}`
+                  : `${importPath}.${extension}`
               }`,
               query,
               hash,
@@ -354,7 +373,7 @@ export default createRule<Options, MessageId>({
               fix(fixer: RuleFixer) {
                 return fixer.replaceText(
                   source,
-                  JSON.stringify(fixedImportPath),
+                  replaceImportPath(source.raw, fixedImportPath),
                 )
               },
             }
@@ -376,7 +395,7 @@ export default createRule<Options, MessageId>({
                           data: {
                             extension,
                             importPath: importPathWithQueryString,
-                            fixedImportPath: fixedImportPath,
+                            fixedImportPath,
                           },
                         },
                       ],
@@ -388,6 +407,30 @@ export default createRule<Options, MessageId>({
           isUseOfExtensionForbidden(extension) &&
           isResolvableWithoutExtension(importPath)
         ) {
+          const fixedPathname = importPath.slice(0, -(extension.length + 1))
+          const isIndex = fixedPathname.endsWith('/index')
+          const fixedImportPath = stringifyPath({
+            pathname: isIndex ? fixedPathname.slice(0, -6) : fixedPathname,
+            query,
+            hash,
+          })
+          const fixOrSuggest = {
+            fix(fixer: RuleFixer) {
+              return fixer.replaceText(
+                source,
+                replaceImportPath(source.raw, fixedImportPath),
+              )
+            },
+          }
+          const commonSuggestion = {
+            ...fixOrSuggest,
+            messageId: 'removeUnexpected' as const,
+            data: {
+              extension,
+              importPath: importPathWithQueryString,
+              fixedImportPath,
+            },
+          }
           context.report({
             node: source,
             messageId: 'unexpected',
@@ -395,14 +438,37 @@ export default createRule<Options, MessageId>({
               extension,
               importPath: importPathWithQueryString,
             },
-            ...(props.fix && {
-              fix(fixer) {
-                return fixer.replaceText(
-                  source,
-                  JSON.stringify(importPath.slice(0, -(extension.length + 1))),
-                )
-              },
-            }),
+            ...(props.fix
+              ? fixOrSuggest
+              : {
+                  suggest: [
+                    commonSuggestion,
+                    isIndex && {
+                      ...commonSuggestion,
+                      fix(fixer: RuleFixer) {
+                        return fixer.replaceText(
+                          source,
+                          replaceImportPath(
+                            source.raw,
+                            stringifyPath({
+                              pathname: fixedPathname,
+                              query,
+                              hash,
+                            }),
+                          ),
+                        )
+                      },
+                      data: {
+                        ...commonSuggestion.data,
+                        fixedImportPath: stringifyPath({
+                          pathname: fixedPathname,
+                          query,
+                          hash,
+                        }),
+                      },
+                    },
+                  ].filter(Boolean),
+                }),
           })
         }
       },


Test patch:
diff --git a/test/rules/extensions.spec.ts b/test/rules/extensions.spec.ts
index 989801f4..01b97b3e 100644
--- a/test/rules/extensions.spec.ts
+++ b/test/rules/extensions.spec.ts
@@ -181,6 +181,26 @@ ruleTester.run('extensions', rule, {
           data: { extension: 'js', importPath: 'a/index.js' },
           line: 1,
           column: 15,
+          suggestions: [
+            {
+              messageId: 'removeUnexpected',
+              data: {
+                extension: 'js',
+                importPath: 'a/index.js',
+                fixedImportPath: 'a',
+              },
+              output: 'import a from "a"',
+            },
+            {
+              messageId: 'removeUnexpected',
+              data: {
+                extension: 'js',
+                importPath: 'a/index.js',
+                fixedImportPath: 'a/index',
+              },
+              output: 'import a from "a/index"',
+            },
+          ],
         },
       ],
     }),
@@ -220,6 +240,32 @@ ruleTester.run('extensions', rule, {
           data: { extension: 'js', importPath: 'a/index.js' },
           line: 1,
           column: 15,
+          suggestions: [
+            {
+              messageId: 'removeUnexpected',
+              data: {
+                extension: 'js',
+                importPath: 'a/index.js',
+                fixedImportPath: 'a',
+              },
+              output: [
+                'import a from "a"',
+                'import packageConfig from "./package"',
+              ].join('\n'),
+            },
+            {
+              messageId: 'removeUnexpected',
+              data: {
+                extension: 'js',
+                importPath: 'a/index.js',
+                fixedImportPath: 'a/index',
+              },
+              output: [
+                'import a from "a/index"',
+                'import packageConfig from "./package"',
+              ].join('\n'),
+            },
+          ],
         },
         {
           messageId: 'missingKnown',
@@ -259,6 +305,21 @@ ruleTester.run('extensions', rule, {
           data: { extension: 'js', importPath: './bar.js' },
           line: 1,
           column: 17,
+          suggestions: [
+            {
+              messageId: 'removeUnexpected',
+              data: {
+                extension: 'js',
+                importPath: './bar.js',
+                fixedImportPath: './bar',
+              },
+              output: [
+                'import lib from "./bar"',
+                'import component from "./bar.jsx"',
+                'import data from "./bar.json"',
+              ].join('\n'),
+            },
+          ],
         },
       ],
     }),
@@ -278,6 +339,21 @@ ruleTester.run('extensions', rule, {
           data: { extension: 'js', importPath: './bar.js' },
           line: 1,
           column: 17,
+          suggestions: [
+            {
+              messageId: 'removeUnexpected',
+              data: {
+                extension: 'js',
+                importPath: './bar.js',
+                fixedImportPath: './bar',
+              },
+              output: [
+                'import lib from "./bar"',
+                'import component from "./bar.jsx"',
+                'import data from "./bar.json"',
+              ].join('\n'),
+            },
+          ],
         },
       ],
     }),
@@ -297,6 +373,20 @@ ruleTester.run('extensions', rule, {
           data: { extension: 'jsx', importPath: './bar.jsx' },
           line: 1,
           column: 23,
+          suggestions: [
+            {
+              messageId: 'removeUnexpected',
+              data: {
+                extension: 'jsx',
+                importPath: './bar.jsx',
+                fixedImportPath: './bar',
+              },
+              output: [
+                'import component from "./bar"',
+                'import data from "./bar.json"',
+              ].join('\n'),
+            },
+          ],
         },
       ],
     }),
@@ -308,6 +398,17 @@ ruleTester.run('extensions', rule, {
           data: { extension: 'coffee', importPath: './bar.coffee' },
           line: 1,
           column: 8,
+          suggestions: [
+            {
+              messageId: 'removeUnexpected',
+              data: {
+                extension: 'coffee',
+                importPath: './bar.coffee',
+                fixedImportPath: './bar',
+              },
+              output: 'import "./bar"',
+            },
+          ],
         },
       ],
       options: ['never', { js: 'always', jsx: 'always' }],
@@ -330,6 +431,21 @@ ruleTester.run('extensions', rule, {
           data: { extension: 'js', importPath: './bar.js' },
           line: 1,
           column: 19,
+          suggestions: [
+            {
+              messageId: 'removeUnexpected',
+              data: {
+                extension: 'js',
+                importPath: './bar.js',
+                fixedImportPath: './bar',
+              },
+              output: [
+                'import barjs from "./bar"',
+                'import barjson from "./bar.json"',
+                'import barnone from "./bar"',
+              ].join('\n'),
+            },
+          ],
         },
       ],
     }),
@@ -397,6 +513,21 @@ ruleTester.run('extensions', rule, {
           data: { extension: 'js', importPath: './bar.js' },
           line: 1,
           column: 19,
+          suggestions: [
+            {
+              messageId: 'removeUnexpected',
+              data: {
+                extension: 'js',
+                importPath: './bar.js',
+                fixedImportPath: './bar',
+              },
+              output: [
+                'import barjs from "./bar"',
+                'import barjson from "./bar.json"',
+                'import barnone from "./bar"',
+              ].join('\n'),
+            },
+          ],
         },
       ],
     }),
@@ -411,6 +542,17 @@ ruleTester.run('extensions', rule, {
           data: { extension: 'js', importPath: './fake-file.js' },
           line: 1,
           column: 19,
+          suggestions: [
+            {
+              messageId: 'removeUnexpected',
+              data: {
+                extension: 'js',
+                importPath: './fake-file.js',
+                fixedImportPath: './fake-file',
+              },
+              output: 'import thing from "./fake-file"',
+            },
+          ],
         },
       ],
     }),
@@ -449,6 +591,17 @@ ruleTester.run('extensions', rule, {
           data: { extension: 'js', importPath: '@name/pkg/test.js' },
           line: 1,
           column: 19,
+          suggestions: [
+            {
+              messageId: 'removeUnexpected',
+              data: {
+                extension: 'js',
+                importPath: '@name/pkg/test.js',
+                fixedImportPath: '@name/pkg/test',
+              },
+              output: 'import thing from "@name/pkg/test"',
+            },
+          ],
         },
       ],
     }),
@@ -518,15 +671,46 @@ ruleTester.run('extensions', rule, {
         {
           messageId: 'unexpected',
           data: { extension: 'js', importPath: './foo.js' },
-
           line: 2,
           column: 25,
+          suggestions: [
+            {
+              messageId: 'removeUnexpected',
+              data: {
+                extension: 'js',
+                importPath: './foo.js',
+                fixedImportPath: './foo',
+              },
+              output: `
+        import foo from './foo'
+        import bar from './bar.json'
+        import Component from './Component.jsx'
+        import express from 'express'
+      `,
+            },
+          ],
         },
         {
           messageId: 'unexpected',
           data: { extension: 'jsx', importPath: './Component.jsx' },
           line: 4,
           column: 31,
+          suggestions: [
+            {
+              messageId: 'removeUnexpected',
+              data: {
+                extension: 'jsx',
+                importPath: './Component.jsx',
+                fixedImportPath: './Component',
+              },
+              output: `
+        import foo from './foo.js'
+        import bar from './bar.json'
+        import Component from './Component'
+        import express from 'express'
+      `,
+            },
+          ],
         },
       ],
       options: ['never', { ignorePackages: true }],
@@ -544,6 +728,21 @@ ruleTester.run('extensions', rule, {
           data: { extension: 'jsx', importPath: './Component.jsx' },
           line: 4,
           column: 31,
+          suggestions: [
+            {
+              messageId: 'removeUnexpected',
+              data: {
+                extension: 'jsx',
+                importPath: './Component.jsx',
+                fixedImportPath: './Component',
+              },
+              output: `
+        import foo from './foo.js'
+        import bar from './bar.json'
+        import Component from './Component'
+      `,
+            },
+          ],
         },
       ],
       options: ['always', { pattern: { jsx: 'never' } }],
@@ -575,6 +774,20 @@ ruleTester.run('extensions', rule, {
           data: { extension: 'js', importPath: './foo.js' },
           line: 1,
           column: 21,
+          suggestions: [
+            {
+              messageId: 'removeUnexpected',
+              data: {
+                extension: 'js',
+                importPath: './foo.js',
+                fixedImportPath: './foo',
+              },
+              output: [
+                'export { foo } from "./foo"',
+                'let bar; export { bar }',
+              ].join('\n'),
+            },
+          ],
         },
       ],
     }),
@@ -589,6 +802,17 @@ ruleTester.run('extensions', rule, {
           data: { extension: 'js', importPath: './foo.js?a=True' },
           line: 1,
           column: 27,
+          suggestions: [
+            {
+              messageId: 'removeUnexpected',
+              data: {
+                extension: 'js',
+                importPath: './foo.js?a=True',
+                fixedImportPath: './foo?a=True',
+              },
+              output: 'import withExtension from "./foo?a=True"',
+            },
+          ],
         },
       ],
     }),
@@ -629,6 +853,20 @@ ruleTester.run('extensions', rule, {
           data: { extension: 'js', importPath: './foo.js' },
           line: 1,
           column: 25,
+          suggestions: [
+            {
+              messageId: 'removeUnexpected',
+              data: {
+                extension: 'js',
+                importPath: './foo.js',
+                fixedImportPath: './foo',
+              },
+              output: [
+                'const { foo } = require("./foo")',
+                'export { foo }',
+              ].join('\n'),
+            },
+          ],
         },
       ],
     }),
@@ -674,6 +912,17 @@ ruleTester.run('extensions', rule, {
           data: { extension: 'js', importPath: './foo.js' },
           line: 1,
           column: 21,
+          suggestions: [
+            {
+              messageId: 'removeUnexpected',
+              data: {
+                extension: 'js',
+                importPath: './foo.js',
+                fixedImportPath: './foo',
+              },
+              output: 'export { foo } from "./foo"',
+            },
+          ],
         },
       ],
     }),
@@ -698,9 +947,19 @@ ruleTester.run('extensions', rule, {
         {
           messageId: 'unexpected',
           data: { extension: 'js', importPath: './foo.js' },
-
           line: 1,
           column: 15,
+          suggestions: [
+            {
+              messageId: 'removeUnexpected',
+              data: {
+                extension: 'js',
+                importPath: './foo.js',
+                fixedImportPath: './foo',
+              },
+              output: 'export * from "./foo"',
+            },
+          ],
         },
       ],
     }),
@@ -711,8 +970,18 @@ ruleTester.run('extensions', rule, {
         {
           messageId: 'unexpected',
           data: { extension: 'js', importPath: '@/ImNotAScopedModule.js' },
-
           line: 1,
+          suggestions: [
+            {
+              messageId: 'removeUnexpected',
+              data: {
+                extension: 'js',
+                importPath: '@/ImNotAScopedModule.js',
+                fixedImportPath: '@/ImNotAScopedModule',
+              },
+              output: 'import foo from "@/ImNotAScopedModule"',
+            },
+          ],
         },
       ],
     }),
@@ -739,6 +1008,36 @@ ruleTester.run('extensions', rule, {
             importPath: '@test-scope/some-module/index.js',
           },
           line: 3,
+          suggestions: [
+            {
+              messageId: 'removeUnexpected',
+              data: {
+                extension: 'js',
+                importPath: '@test-scope/some-module/index.js',
+                fixedImportPath: '@test-scope/some-module',
+              },
+              output: `
+        import _ from 'lodash';
+        import m from '@test-scope/some-module';
+
+        import bar from './bar';
+      `,
+            },
+            {
+              messageId: 'removeUnexpected',
+              data: {
+                extension: 'js',
+                importPath: '@test-scope/some-module/index.js',
+                fixedImportPath: '@test-scope/some-module/index',
+              },
+              output: `
+        import _ from 'lodash';
+        import m from '@test-scope/some-module/index';
+
+        import bar from './bar';
+      `,
+            },
+          ],
         },
       ],
     }),
@@ -1024,6 +1323,83 @@ describe('TypeScript', () => {
         ],
         output: 'import foo from "./foo";',
       }),
+
+      tInvalid({
+        code: 'import foo from "./index.js?query#hash";',
+        options: ['always', { pattern: { js: 'never' } }],
+        errors: [
+          {
+            messageId: 'unexpected',
+            data: { extension: 'js', importPath: './index.js?query#hash' },
+            suggestions: [
+              {
+                messageId: 'removeUnexpected',
+                data: {
+                  extension: 'js',
+                  importPath: './index.js?query#hash',
+                  fixedImportPath: '.?query#hash',
+                },
+                output: 'import foo from ".?query#hash";',
+              },
+              {
+                messageId: 'removeUnexpected',
+                data: {
+                  extension: 'js',
+                  importPath: './index.js?query#hash',
+                  fixedImportPath: './index?query#hash',
+                },
+                output: 'import foo from "./index?query#hash";',
+              },
+            ],
+          },
+        ],
+      }),
+
+      tInvalid({
+        code: 'import foo from "./index.js?query#hash";',
+        options: ['always', { pattern: { js: 'never' }, fix: true }],
+        errors: [
+          {
+            messageId: 'unexpected',
+            data: { extension: 'js', importPath: './index.js?query#hash' },
+          },
+        ],
+        output: 'import foo from ".?query#hash";',
+      }),
+
+      tInvalid({
+        code: 'import foo from "./?query#hash";',
+        options: ['always', { pattern: { js: 'always' } }],
+        errors: [
+          {
+            messageId: 'missingKnown',
+            data: { extension: 'js', importPath: './?query#hash' },
+            suggestions: [
+              {
+                messageId: 'addMissing',
+                data: {
+                  extension: 'js',
+                  importPath: './?query#hash',
+                  fixedImportPath: './index.js?query#hash',
+                },
+                output: 'import foo from "./index.js?query#hash";',
+              },
+            ],
+          },
+        ],
+      }),
+
+      tInvalid({
+        code: 'import foo from "./?query#hash";',
+        options: ['always', { pattern: { js: 'always' }, fix: true }],
+        errors: [
+          {
+            messageId: 'missingKnown',
+            data: { extension: 'js', importPath: './?query#hash' },
+          },
+        ],
+        output: 'import foo from "./index.js?query#hash";',
+      }),
     ],
   })
 })
diff --git a/test/utils/__snapshots__/parse-path.spec.ts.snap b/test/utils/__snapshots__/parse-path.spec.ts.snap
index 2d55a2b1..acdb3070 100644
--- a/test/utils/__snapshots__/parse-path.spec.ts.snap
+++ b/test/utils/__snapshots__/parse-path.spec.ts.snap
@@ -1,6 +1,6 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`parse-path should parse and stringify path expectedly 1`] = `
+exports[`parse-path should parse and stringify path expectedly: foo 1`] = `
 {
   "hash": "",
   "pathname": "foo",
@@ -8,34 +8,34 @@ exports[`parse-path should parse and stringify path expectedly 1`] = `
 }
 `;
 
-exports[`parse-path should parse and stringify path expectedly 2`] = `
+exports[`parse-path should parse and stringify path expectedly: foo#hash 1`] = `
 {
-  "hash": "",
+  "hash": "#hash",
   "pathname": "foo",
-  "query": "?query",
+  "query": "",
 }
 `;
 
-exports[`parse-path should parse and stringify path expectedly 3`] = `
+exports[`parse-path should parse and stringify path expectedly: foo#hash?query 1`] = `
 {
-  "hash": "#hash",
+  "hash": "#hash?query",
   "pathname": "foo",
   "query": "",
 }
 `;
 
-exports[`parse-path should parse and stringify path expectedly 4`] = `
+exports[`parse-path should parse and stringify path expectedly: foo?query 1`] = `
 {
-  "hash": "#hash",
+  "hash": "",
   "pathname": "foo",
   "query": "?query",
 }
 `;
 
-exports[`parse-path should parse and stringify path expectedly 5`] = `
+exports[`parse-path should parse and stringify path expectedly: foo?query#hash 1`] = `
 {
-  "hash": "#hash?query",
+  "hash": "#hash",
   "pathname": "foo",
-  "query": "",
+  "query": "?query",
 }
 `;
diff --git a/test/utils/parse-path.spec.ts b/test/utils/parse-path.spec.ts
index b6b7584c..febfbbdf 100644
--- a/test/utils/parse-path.spec.ts
+++ b/test/utils/parse-path.spec.ts
@@ -12,7 +12,7 @@ describe('parse-path', () => {
 
     for (const input of cases) {
       const output = parsePath(input)
-      expect(output).toMatchSnapshot()
+      expect(output).toMatchSnapshot(input)
       expect(stringifyPath(output)).toBe(input)
     }
   })