Skip to content

feat(extensions): enhance import extension enforcement with autofix support #3177

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a
| [consistent-type-specifier-style](docs/rules/consistent-type-specifier-style.md) | Enforce or ban the use of inline type-only markers for named imports. | | | | 🔧 | | |
| [dynamic-import-chunkname](docs/rules/dynamic-import-chunkname.md) | Enforce a leading comment with the webpackChunkName for dynamic imports. | | | | | 💡 | |
| [exports-last](docs/rules/exports-last.md) | Ensure all exports appear after other statements. | | | | | | |
| [extensions](docs/rules/extensions.md) | Ensure consistent use of file extension within the import path. | | | | | | |
| [extensions](docs/rules/extensions.md) | Ensure consistent use of file extension within the import path. | | | | 🔧 | | |
| [first](docs/rules/first.md) | Ensure all imports appear before other statements. | | | | 🔧 | | |
| [group-exports](docs/rules/group-exports.md) | Prefer named exports to be grouped together in a single export declaration | | | | | | |
| [imports-first](docs/rules/imports-first.md) | Replaced by `import/first`. | | | | 🔧 | | ❌ |
Expand Down
2 changes: 2 additions & 0 deletions docs/rules/extensions.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# import/extensions

🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).

<!-- end auto-generated rule header -->

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.
Expand Down
27 changes: 26 additions & 1 deletion src/rules/extensions.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ function buildProperties(context) {
defaultConfig: 'never',
pattern: {},
ignorePackages: false,
fix: false,
};

context.options.forEach((obj) => {
Expand All @@ -72,6 +73,11 @@ function buildProperties(context) {
result.ignorePackages = obj.ignorePackages;
}

// If fix is provided, transfer it to result
if (obj.fix !== undefined) {
result.fix = obj.fix;
}

if (obj.checkTypeImports !== undefined) {
result.checkTypeImports = obj.checkTypeImports;
}
Expand All @@ -97,7 +103,7 @@ module.exports = {
description: 'Ensure consistent use of file extension within the import path.',
url: docsUrl('extensions'),
},

fixable: 'code',
schema: {
anyOf: [
{
Expand Down Expand Up @@ -225,13 +231,32 @@ module.exports = {
node: source,
message:
`Missing file extension ${extension ? `"${extension}" ` : ''}for "${importPathWithQueryString}"`,
...props.fix && extension ? {
fix(fixer) {
return fixer.replaceText(
source,
JSON.stringify(`${importPathWithQueryString}.${extension}`),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See https://github.com/un-ts/eslint-plugin-import-x/pull/329/files#r2094349587

JSON.stringify is not correct for single quoted imports.

https://github.com/un-ts/eslint-plugin-import-x/pull/327/files#r2094298890

importPathWithQueryString could contains query and hash, and could also ends with ., .. or /.

);
},
} : {},
});
}
} else if (extension) {
if (isUseOfExtensionForbidden(extension) && isResolvableWithoutExtension(importPath)) {
context.report({
node: source,
message: `Unexpected use of file extension "${extension}" for "${importPathWithQueryString}"`,
...props.fix
? {
fix(fixer) {
return fixer.replaceText(
source,
JSON.stringify(
importPath.slice(0, -(extension.length + 1)),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

),
);
},
} : {},
});
}
}
Expand Down
17 changes: 17 additions & 0 deletions tests/src/rules/extensions.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,16 @@ ruleTester.run('extensions', rule, {
].join('\n'),
options: ['always'],
}),

test({
code: "import foo from './foo';",
options: [{ fix: true }],
}),

test({
code: "import foo from './foo.js';",
options: [{ fix: true, pattern: { js: 'always' } }],
}),
],

invalid: [
Expand Down Expand Up @@ -652,6 +662,13 @@ ruleTester.run('extensions', rule, {
},
],
}),

test({
code: 'import foo from "./foo.js";',
options: ['always', { pattern: { js: 'never' }, fix: true }],
errors: [{ message: 'Unexpected use of file extension "js" for "./foo.js"' }],
output: 'import foo from "./foo";',
}),
],
});

Expand Down