Repository: un-ts/eslint-plugin-import-x (ts)

Issue:
extensions rule autofix requires strange configuration options to work
First of all, thank you @stephenjason89 and @JounQin for getting the extensions autofix contribution merged in #327. I’ve been eager to use Stephen’s PR since I first saw the attempt in import-js/eslint-plugin-import#1749.

I've been finding it difficult to configure the extensions rule autofix to work. I'm suspicious that the behavior I'm seeing is unintentional and might be a bug.

What I've found is that the extensions rule autofix only works when **both** of the following are true:
1. a `fix: true` option is set
2. a `pattern: {}` option is set, even if empty (the contents of the pattern object do not seem to matter)

## Steps to Reproduce

In my testing, these **do not enable** autofix:
```typescript
'import-x/extensions': ['warn', 'never'],
'import-x/extensions': ['warn', 'never', { fix: true }],
'import-x/extensions': ['warn', 'never', { pattern: {} }],
```

But this **does enable** autofix:
```typescript
'import-x/extensions': ['warn', 'never', { fix: true, pattern: {} }],
```

1) is auto-fix meant to be opt-in? I saw [this comment that it was being considered](https://github.com/un-ts/eslint-plugin-import-x/issues/318#issuecomment-2848521188) but it's not documented that way, and none of the other rules have a `fix: boolean` option.
2) I can't imagine requiring a pattern object to enable the flag was intentional.

## Questions
1. Is autofix intended to be opt-in via `fix: true`? I saw [this comment](https://github.com/un-ts/eslint-plugin-import-x/issues/318#issuecomment-2848521188) suggesting it might be, but this [isn’t documented](https://github.com/un-ts/eslint-plugin-import-x/blob/v4.15.2/docs/rules/extensions.md), and no other rules in this project use a fix option. I was only able to discover the `fix: boolean` option by looking through the PR history and source code.
2. Is requiring a pattern object to enable autofix intentional? It seems like an oversight, especially since an empty object works.

Thanks again for all the work on this plugin.



Pull request description:
fix(extensions): always calculate `fix` option
close #391

Initially, it only adds failing test cases that expose the broken autofix behavior of the `extensions` rule.

### Root issue

Autofix does not work unless both `fix: true` and a `pattern` object are set — even if the pattern is empty. This behavior is not intuitive, not documented, and not consistent with how autofix works in other ESLint rules.

### Suggested solution

The rule should apply autofixes by default when it is safe to do so. The `fix` option should be removed entirely.

This PR starts by adding tests to document the current (broken) behavior.

### Notes

- There are around 80 tests in `extensions.spec.ts`, but none of the existing tests asserted on the actual `output` field, which is what validates whether autofixes are being applied. That gap likely contributed to this issue being missed.
- The new tests are intentionally simple and direct, and I haven't attempted to deduplicate or reorganize them within the file. The test suite is already quite dense, and my focus was on surfacing the issue as clearly as possible.
- This PR is open to edits by maintainers. If you'd like to continue from here and iterate toward a fix, feel free to push directly.
- For an easier-to-follow standalone reproduction, see:  https://github.com/lnhrdt/un-ts-eslint-plugin-import-x-issue-391
<!-- ELLIPSIS_HIDDEN -->

----

> [!IMPORTANT]
> Add failing tests in `extensions.spec.ts` to expose broken autofix behavior of `extensions` rule requiring both `fix: true` and a `pattern` object.
> 
>   - **Tests**:
>     - Add failing test cases in `extensions.spec.ts` to expose broken autofix behavior of `extensions` rule.
>     - Tests show autofix requires both `fix: true` and a `pattern` object, even if empty, to work.
>     - Tests include cases for default autofix, `fix: true`, `fix: true` with pattern, and `fix: false`.
> 
> <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 d51f400ce566c40fba65d332a80cf0b21b16158b. 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

- **Tests**
  - Added new test cases to verify autofix and suggestion behaviors for import path file extensions under different autofix settings.  
- **Bug Fixes**
  - Ensured consistent handling of the autofix option for import path extensions.  
- **Documentation**
  - Clarified that automatic fixing of import extensions requires explicit enabling of the fix option, with examples and future default behavior noted.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Patch:
diff --git a/.changeset/angry-lions-learn.md b/.changeset/angry-lions-learn.md
new file mode 100644
index 00000000..843dbc15
--- /dev/null
+++ b/.changeset/angry-lions-learn.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-import-x": patch
+---
+
+fix(extensions): always calculate `fix` option
diff --git a/docs/rules/extensions.md b/docs/rules/extensions.md
index e8386799..414d5e5a 100644
--- a/docs/rules/extensions.md
+++ b/docs/rules/extensions.md
@@ -4,6 +4,18 @@
 
 <!-- end auto-generated rule header -->
 
+> [!NOTE]
+>
+> This rule is only fixable when the `fix` option is set to `true` for compatibility, otherwise `suggestions` will be provided instead.
+>
+> Example:
+>
+> ```json
+> "import-x/extensions": ["error", "never", { "fix": true }]
+> ```
+>
+> It will change to be automatically fixable in the next major version.
+
 Some file resolve algorithms allow you to omit the file extension within the import source path. For example the `node` resolver (which does not yet support ESM/`import`) can resolve `./foo/bar` to the absolute path `/User/someone/foo/bar.js` because the `.js` extension is resolved automatically by default in CJS. Depending on the resolver you can configure more extensions to get resolved automatically.
 
 In order to provide a consistent use of file extensions across your code base, this rule can enforce or disallow the use of certain file extensions.
diff --git a/src/rules/extensions.ts b/src/rules/extensions.ts
index 73bb6a75..7f8c5f82 100644
--- a/src/rules/extensions.ts
+++ b/src/rules/extensions.ts
@@ -132,6 +132,10 @@ function buildProperties(context: RuleContext<MessageId, Options>) {
       continue
     }
 
+    if (obj.fix != null) {
+      result.fix = Boolean(obj.fix)
+    }
+
     // If this is not the new structure, transfer all props to result.pattern
     if (
       (!('pattern' in obj) || obj.pattern == null) &&
@@ -156,10 +160,6 @@ function buildProperties(context: RuleContext<MessageId, Options>) {
       result.checkTypeImports = obj.checkTypeImports
     }
 
-    if (obj.fix != null) {
-      result.fix = Boolean(obj.fix)
-    }
-
     if (Array.isArray(obj.pathGroupOverrides)) {
       result.pathGroupOverrides = obj.pathGroupOverrides
     }


Test patch:
diff --git a/test/rules/extensions.spec.ts b/test/rules/extensions.spec.ts
index 01b97b3e..f7824036 100644
--- a/test/rules/extensions.spec.ts
+++ b/test/rules/extensions.spec.ts
@@ -173,6 +173,83 @@ ruleTester.run('extensions', rule, {
   ],
 
   invalid: [
+    tInvalid({
+      name: 'extensions should provide suggestions by default',
+      code: 'import a from "./foo.js"',
+      options: ['never'],
+      errors: [
+        {
+          messageId: 'unexpected',
+          data: { extension: 'js', importPath: './foo.js' },
+          line: 1,
+          column: 15,
+          suggestions: [
+            {
+              messageId: 'removeUnexpected',
+              data: {
+                extension: 'js',
+                importPath: './foo.js',
+                fixedImportPath: './foo',
+              },
+              output: 'import a from "./foo"',
+            },
+          ],
+        },
+      ],
+    }),
+    tInvalid({
+      name: 'extensions should autofix when fix is set to true',
+      code: 'import a from "./foo.js"',
+      options: ['never', { fix: true }],
+      errors: [
+        {
+          messageId: 'unexpected',
+          data: { extension: 'js', importPath: './foo.js' },
+          line: 1,
+          column: 15,
+        },
+      ],
+      output: 'import a from "./foo"',
+    }),
+    tInvalid({
+      name: 'extensions should autofix when fix is set to true and a pattern object is provided',
+      code: 'import a from "./foo.js"',
+      options: ['never', { fix: true, pattern: {} }],
+      errors: [
+        {
+          messageId: 'unexpected',
+          data: { extension: 'js', importPath: './foo.js' },
+          line: 1,
+          column: 15,
+        },
+      ],
+      output: 'import a from "./foo"',
+    }),
+    tInvalid({
+      name: 'extensions should not autofix when fix is set to false',
+      code: 'import a from "./foo.js"',
+      options: ['never', { fix: false }],
+      errors: [
+        {
+          messageId: 'unexpected',
+          data: { extension: 'js', importPath: './foo.js' },
+          line: 1,
+          column: 15,
+          suggestions: [
+            {
+              messageId: 'removeUnexpected',
+              data: {
+                extension: 'js',
+                importPath: './foo.js',
+                fixedImportPath: './foo',
+              },
+              output: 'import a from "./foo"',
+            },
+          ],
+        },
+      ],
+      output: null,
+    }),
     tInvalid({
       code: 'import a from "a/index.js"',
       errors: [