From ab600662307196f9bf9d0d9cb82f07ced558d468 Mon Sep 17 00:00:00 2001 From: Leonhardt Koepsell Date: Mon, 23 Jun 2025 00:31:08 -0700 Subject: [PATCH 1/5] test(extensions): add failing tests to expose broken autofix behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to un-ts/eslint-plugin-import-x#391 These tests demonstrate that the `import-x/extensions` rule only applies autofixes when both `fix: true` and a `pattern` object are set—even if the pattern is empty. This behavior is unintuitive, undocumented, and inconsistent with how autofix typically works in ESLint. The tests assert on the `output` field to document the current (broken) behavior and provide a basis for resolving the issue. In addition to fixing the logic, I recommend removing the `fix` option entirely, as it is undocumented and not used in any other rule in the plugin. --- test/rules/extensions.spec.ts | 67 +++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/test/rules/extensions.spec.ts b/test/rules/extensions.spec.ts index 01b97b3e..6aecdf5a 100644 --- a/test/rules/extensions.spec.ts +++ b/test/rules/extensions.spec.ts @@ -173,6 +173,73 @@ ruleTester.run('extensions', rule, { ], invalid: [ + tInvalid({ + name: 'extensions should autofix by default', + code: 'import a from "a/foo.js"', + options: ['never'], + errors: [ + { + messageId: 'unexpected', + data: { extension: 'js', importPath: 'a/foo.js' }, + line: 1, + column: 15, + }, + ], + output: 'import a from "a/foo"', + }), + tInvalid({ + name: 'extensions should autofix when fix is set to true', + code: 'import a from "a/foo.js"', + options: ['never', {fix: true}], + errors: [ + { + messageId: 'unexpected', + data: { extension: 'js', importPath: 'a/foo.js' }, + line: 1, + column: 15, + }, + ], + output: 'import a from "a/foo"', + }), + tInvalid({ + name: 'extensions should autofix when fix is set to true and a pattern object is provided', + code: 'import a from "a/foo.js"', + options: ['never', {fix: true, pattern: {}}], + errors: [ + { + messageId: 'unexpected', + data: { extension: 'js', importPath: 'a/foo.js' }, + line: 1, + column: 15, + }, + ], + output: 'import a from "a/foo"', + }), + tInvalid({ + name: 'extensions should not autofix when fix is set to false', + code: 'import a from "a/foo.js"', + options: ['never', {fix: false}], + errors: [ + { + messageId: 'unexpected', + data: { extension: 'js', importPath: 'a/foo.js' }, + line: 1, + column: 15, + suggestions: [ + { + messageId: 'removeUnexpected', + data: { + extension: 'js', + importPath: 'a/foo.js', + fixedImportPath: 'a/foo', + }, + output: 'import a from "a/foo"', + }, + ], + }, + ], + output: null, + }), tInvalid({ code: 'import a from "a/index.js"', errors: [ From b9edb07c6c3eaa85297f6fbcfbd28cd229ee2822 Mon Sep 17 00:00:00 2001 From: Leonhardt Koepsell Date: Mon, 23 Jun 2025 00:45:41 -0700 Subject: [PATCH 2/5] test(extensions): simplify example setup for autofix tests --- test/rules/extensions.spec.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/test/rules/extensions.spec.ts b/test/rules/extensions.spec.ts index 6aecdf5a..cfbf28e2 100644 --- a/test/rules/extensions.spec.ts +++ b/test/rules/extensions.spec.ts @@ -175,54 +175,54 @@ ruleTester.run('extensions', rule, { invalid: [ tInvalid({ name: 'extensions should autofix by default', - code: 'import a from "a/foo.js"', + code: 'import a from "./foo.js"', options: ['never'], errors: [ { messageId: 'unexpected', - data: { extension: 'js', importPath: 'a/foo.js' }, + data: { extension: 'js', importPath: './foo.js' }, line: 1, column: 15, }, ], - output: 'import a from "a/foo"', + output: 'import a from "./foo"', }), tInvalid({ name: 'extensions should autofix when fix is set to true', - code: 'import a from "a/foo.js"', + code: 'import a from "./foo.js"', options: ['never', {fix: true}], errors: [ { messageId: 'unexpected', - data: { extension: 'js', importPath: 'a/foo.js' }, + data: { extension: 'js', importPath: './foo.js' }, line: 1, column: 15, }, ], - output: 'import a from "a/foo"', + 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 "a/foo.js"', + code: 'import a from "./foo.js"', options: ['never', {fix: true, pattern: {}}], errors: [ { messageId: 'unexpected', - data: { extension: 'js', importPath: 'a/foo.js' }, + data: { extension: 'js', importPath: './foo.js' }, line: 1, column: 15, }, ], - output: 'import a from "a/foo"', + output: 'import a from "./foo"', }), tInvalid({ name: 'extensions should not autofix when fix is set to false', - code: 'import a from "a/foo.js"', + code: 'import a from "./foo.js"', options: ['never', {fix: false}], errors: [ { messageId: 'unexpected', - data: { extension: 'js', importPath: 'a/foo.js' }, + data: { extension: 'js', importPath: './foo.js' }, line: 1, column: 15, suggestions: [ @@ -230,10 +230,10 @@ ruleTester.run('extensions', rule, { messageId: 'removeUnexpected', data: { extension: 'js', - importPath: 'a/foo.js', - fixedImportPath: 'a/foo', + importPath: './foo.js', + fixedImportPath: './foo', }, - output: 'import a from "a/foo"', + output: 'import a from "./foo"', }, ], }, From 42b062f41e29f1dbf71a95f3ebb72197e3ba4f35 Mon Sep 17 00:00:00 2001 From: JounQin Date: Tue, 24 Jun 2025 22:58:43 +0800 Subject: [PATCH 3/5] fix: always calculate `fix` option --- src/rules/extensions.ts | 8 ++++---- test/rules/extensions.spec.ts | 20 +++++++++++++++----- 2 files changed, 19 insertions(+), 9 deletions(-) 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) { 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) { result.checkTypeImports = obj.checkTypeImports } - if (obj.fix != null) { - result.fix = Boolean(obj.fix) - } - if (Array.isArray(obj.pathGroupOverrides)) { result.pathGroupOverrides = obj.pathGroupOverrides } diff --git a/test/rules/extensions.spec.ts b/test/rules/extensions.spec.ts index cfbf28e2..f7824036 100644 --- a/test/rules/extensions.spec.ts +++ b/test/rules/extensions.spec.ts @@ -174,7 +174,7 @@ ruleTester.run('extensions', rule, { invalid: [ tInvalid({ - name: 'extensions should autofix by default', + name: 'extensions should provide suggestions by default', code: 'import a from "./foo.js"', options: ['never'], errors: [ @@ -183,14 +183,24 @@ ruleTester.run('extensions', rule, { 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: '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}], + options: ['never', { fix: true }], errors: [ { messageId: 'unexpected', @@ -204,7 +214,7 @@ ruleTester.run('extensions', rule, { 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: {}}], + options: ['never', { fix: true, pattern: {} }], errors: [ { messageId: 'unexpected', @@ -218,7 +228,7 @@ ruleTester.run('extensions', rule, { tInvalid({ name: 'extensions should not autofix when fix is set to false', code: 'import a from "./foo.js"', - options: ['never', {fix: false}], + options: ['never', { fix: false }], errors: [ { messageId: 'unexpected', From e0ca3333185f7984c4f2a6505ba864bc9d51c21d Mon Sep 17 00:00:00 2001 From: JounQin Date: Tue, 24 Jun 2025 23:07:29 +0800 Subject: [PATCH 4/5] Create angry-lions-learn.md Signed-off-by: JounQin --- .changeset/angry-lions-learn.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/angry-lions-learn.md 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 From bd5a7b585555c3acc3d1b09d13c82dd9df5a5e2a Mon Sep 17 00:00:00 2001 From: JounQin Date: Tue, 24 Jun 2025 23:18:57 +0800 Subject: [PATCH 5/5] docs: document about `fix` option --- docs/rules/extensions.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 @@ +> [!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.