From 1b66a8117070f3d452bfca8434e8b92efa789ff1 Mon Sep 17 00:00:00 2001 From: kumavis Date: Thu, 10 Nov 2022 22:07:18 -1000 Subject: [PATCH] [New] `extensions`: add auto fixer! --- README.md | 2 +- docs/rules/extensions.md | 2 + src/rules/extensions.js | 17 +++++- tests/src/rules/extensions.js | 106 +++++++++++++++++++++++++++++++++- 4 files changed, 124 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ed1e4f822..1ded200fb 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,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`. | | | | 🔧 | | ❌ | diff --git a/docs/rules/extensions.md b/docs/rules/extensions.md index 9e78b8c70..2a532ceaf 100644 --- a/docs/rules/extensions.md +++ b/docs/rules/extensions.md @@ -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). + Some file resolve algorithms allow you to omit the file extension within the import source path. For example the `node` resolver can resolve `./foo/bar` to the absolute path `/User/someone/foo/bar.js` because the `.js` extension is resolved automatically by default. Depending on the resolver you can configure more extensions to get resolved automatically. diff --git a/src/rules/extensions.js b/src/rules/extensions.js index 7d026c787..72e1e6466 100644 --- a/src/rules/extensions.js +++ b/src/rules/extensions.js @@ -67,7 +67,7 @@ module.exports = { description: 'Ensure consistent use of file extension within the import path.', url: docsUrl('extensions'), }, - + fixable: 'code', schema: { anyOf: [ { @@ -175,6 +175,12 @@ module.exports = { node: source, message: `Missing file extension ${extension ? `"${extension}" ` : ''}for "${importPathWithQueryString}"`, + fix: extension && (fixer => { + const [start, end] = source.range; + return [ + fixer.replaceTextRange([start + 1, end - 1], `${source.value}.${extension}`), + ]; + }), }); } } else if (extension) { @@ -182,6 +188,15 @@ module.exports = { context.report({ node: source, message: `Unexpected use of file extension "${extension}" for "${importPathWithQueryString}"`, + fix: (fixer) => { + const [start, end] = source.range; + const extensionIndex = source.value.lastIndexOf(`.${extension}`); + const specifierBeforeExt = source.value.slice(0, extensionIndex); + const specifierAfterExt = source.value.slice(extensionIndex + extension.length + 1); + return [ + fixer.replaceTextRange([start + 1, end - 1], `${specifierBeforeExt}${specifierAfterExt}`), + ]; + }, }); } } diff --git a/tests/src/rules/extensions.js b/tests/src/rules/extensions.js index 45b4498fe..762a586c6 100644 --- a/tests/src/rules/extensions.js +++ b/tests/src/rules/extensions.js @@ -8,7 +8,10 @@ ruleTester.run('extensions', rule, { valid: [ test({ code: 'import a from "@/a"' }), test({ code: 'import a from "a"' }), - test({ code: 'import dot from "./file.with.dot"' }), + test({ + code: 'import dot from "./file.with.dot"', + // output: 'import dot from "./file.with.dot.js"', + }), test({ code: 'import a from "a/index.js"', options: [ 'always' ], @@ -30,6 +33,11 @@ ruleTester.run('extensions', rule, { 'import component from "./bar.jsx"', 'import data from "./bar.json"', ].join('\n'), + // output: [ + // 'import lib from "./bar.js"', + // 'import component from "./bar.jsx"', + // 'import data from "./bar.json"', + // ].join('\n'), options: [ 'never' ], settings: { 'import/resolve': { 'extensions': [ '.js', '.jsx', '.json' ] } }, }), @@ -151,6 +159,7 @@ ruleTester.run('extensions', rule, { invalid: [ test({ code: 'import a from "a/index.js"', + output: 'import a from "a/index"', errors: [ { message: 'Unexpected use of file extension "js" for "a/index.js"', line: 1, @@ -159,6 +168,7 @@ ruleTester.run('extensions', rule, { }), test({ code: 'import dot from "./file.with.dot"', + output: 'import dot from "./file.with.dot.js"', options: [ 'always' ], errors: [ { @@ -173,6 +183,10 @@ ruleTester.run('extensions', rule, { 'import a from "a/index.js"', 'import packageConfig from "./package"', ].join('\n'), + output: [ + 'import a from "a/index"', + 'import packageConfig from "./package.json"', + ].join('\n'), options: [ { json: 'always', js: 'never' } ], settings: { 'import/resolve': { 'extensions': [ '.js', '.json' ] } }, errors: [ @@ -194,6 +208,11 @@ ruleTester.run('extensions', rule, { 'import component from "./bar.jsx"', 'import data from "./bar.json"', ].join('\n'), + output: [ + 'import lib from "./bar"', + 'import component from "./bar.jsx"', + 'import data from "./bar.json"', + ].join('\n'), options: [ 'never' ], settings: { 'import/resolve': { 'extensions': [ '.js', '.jsx', '.json' ] } }, errors: [ @@ -210,6 +229,11 @@ ruleTester.run('extensions', rule, { 'import component from "./bar.jsx"', 'import data from "./bar.json"', ].join('\n'), + output: [ + 'import lib from "./bar"', + 'import component from "./bar.jsx"', + 'import data from "./bar.json"', + ].join('\n'), options: [ { json: 'always', js: 'never', jsx: 'never' } ], settings: { 'import/resolve': { 'extensions': [ '.js', '.jsx', '.json' ] } }, errors: [ @@ -226,6 +250,10 @@ ruleTester.run('extensions', rule, { 'import component from "./bar.jsx"', 'import data from "./bar.json"', ].join('\n'), + output: [ + 'import component from "./bar"', + 'import data from "./bar.json"', + ].join('\n'), options: [ { json: 'always', js: 'never', jsx: 'never' } ], settings: { 'import/resolve': { 'extensions': [ '.jsx', '.json', '.js' ] } }, errors: [ @@ -238,6 +266,7 @@ ruleTester.run('extensions', rule, { }), test({ code: 'import "./bar.coffee"', + output: 'import "./bar"', errors: [ { message: 'Unexpected use of file extension "coffee" for "./bar.coffee"', @@ -255,6 +284,11 @@ ruleTester.run('extensions', rule, { 'import barjson from "./bar.json"', 'import barnone from "./bar"', ].join('\n'), + output: [ + 'import barjs from "./bar"', + 'import barjson from "./bar.json"', + 'import barnone from "./bar"', + ].join('\n'), options: [ 'always', { json: 'always', js: 'never', jsx: 'never' } ], settings: { 'import/resolve': { 'extensions': [ '.js', '.jsx', '.json' ] } }, errors: [ @@ -272,6 +306,11 @@ ruleTester.run('extensions', rule, { 'import barjson from "./bar.json"', 'import barnone from "./bar"', ].join('\n'), + output: [ + 'import barjs from "./bar"', + 'import barjson from "./bar.json"', + 'import barnone from "./bar"', + ].join('\n'), options: [ 'never', { json: 'always', js: 'never', jsx: 'never' } ], settings: { 'import/resolve': { 'extensions': [ '.js', '.jsx', '.json' ] } }, errors: [ @@ -286,6 +325,7 @@ ruleTester.run('extensions', rule, { // unresolved (#271/#295) test({ code: 'import thing from "./fake-file.js"', + output: 'import thing from "./fake-file"', options: [ 'never' ], errors: [ { @@ -309,6 +349,7 @@ ruleTester.run('extensions', rule, { test({ code: 'import thing from "@name/pkg/test"', + output: 'import thing from "@name/pkg/test"', options: [ 'always' ], errors: [ { @@ -321,6 +362,7 @@ ruleTester.run('extensions', rule, { test({ code: 'import thing from "@name/pkg/test.js"', + output: 'import thing from "@name/pkg/test"', options: [ 'never' ], errors: [ { @@ -342,6 +384,15 @@ ruleTester.run('extensions', rule, { import chart from '@/configs/chart' import express from 'express' `, + output: ` + import foo from './foo.js' + import bar from './bar.json' + import Component from './Component' + import baz from 'foo/baz' + import baw from '@scoped/baw/import' + import chart from '@/configs/chart' + import express from 'express' + `, options: [ 'always', { ignorePackages: true } ], errors: [ { @@ -367,6 +418,15 @@ ruleTester.run('extensions', rule, { import chart from '@/configs/chart' import express from 'express' `, + output: ` + import foo from './foo.js' + import bar from './bar.json' + import Component from './Component' + import baz from 'foo/baz' + import baw from '@scoped/baw/import' + import chart from '@/configs/chart' + import express from 'express' + `, options: [ 'ignorePackages' ], errors: [ { @@ -389,6 +449,12 @@ ruleTester.run('extensions', rule, { import Component from './Component.jsx' import express from 'express' `, + output: ` + import foo from './foo' + import bar from './bar.json' + import Component from './Component' + import express from 'express' + `, errors: [ { message: 'Unexpected use of file extension "js" for "./foo.js"', @@ -409,6 +475,11 @@ ruleTester.run('extensions', rule, { import bar from './bar.json' import Component from './Component.jsx' `, + output: ` + import foo from './foo.js' + import bar from './bar.json' + import Component from './Component' + `, errors: [ { message: 'Unexpected use of file extension "jsx" for "./Component.jsx"', @@ -425,6 +496,10 @@ ruleTester.run('extensions', rule, { 'export { foo } from "./foo"', 'let bar; export { bar }', ].join('\n'), + output: [ + 'export { foo } from "./foo"', + 'let bar; export { bar }', + ].join('\n'), options: [ 'always' ], errors: [ { @@ -439,6 +514,10 @@ ruleTester.run('extensions', rule, { 'export { foo } from "./foo.js"', 'let bar; export { bar }', ].join('\n'), + output: [ + 'export { foo } from "./foo"', + 'let bar; export { bar }', + ].join('\n'), options: [ 'never' ], errors: [ { @@ -452,6 +531,7 @@ ruleTester.run('extensions', rule, { // Query strings. test({ code: 'import withExtension from "./foo.js?a=True"', + output: 'import withExtension from "./foo?a=True"', options: [ 'never' ], errors: [ { @@ -463,6 +543,7 @@ ruleTester.run('extensions', rule, { }), test({ code: 'import withoutExtension from "./foo?a=True.ext"', + output: 'import withoutExtension from "./foo?a=True.ext"', options: [ 'always' ], errors: [ { @@ -478,6 +559,10 @@ ruleTester.run('extensions', rule, { 'const { foo } = require("./foo")', 'export { foo }', ].join('\n'), + output: [ + 'const { foo } = require("./foo")', + 'export { foo }', + ].join('\n'), options: [ 'always' ], errors: [ { @@ -492,6 +577,10 @@ ruleTester.run('extensions', rule, { 'const { foo } = require("./foo.js")', 'export { foo }', ].join('\n'), + output: [ + 'const { foo } = require("./foo")', + 'export { foo }', + ].join('\n'), options: [ 'never' ], errors: [ { @@ -505,6 +594,7 @@ ruleTester.run('extensions', rule, { // export { } from test({ code: 'export { foo } from "./foo"', + output: 'export { foo } from "./foo"', options: [ 'always' ], errors: [ { @@ -519,6 +609,10 @@ ruleTester.run('extensions', rule, { import foo from "@/ImNotAScopedModule"; import chart from '@/configs/chart'; `, + output: ` + import foo from "@/ImNotAScopedModule"; + import chart from '@/configs/chart'; + `, options: ['always'], errors: [ { @@ -533,6 +627,7 @@ ruleTester.run('extensions', rule, { }), test({ code: 'export { foo } from "./foo.js"', + output: 'export { foo } from "./foo"', options: [ 'never' ], errors: [ { @@ -546,6 +641,7 @@ ruleTester.run('extensions', rule, { // export * from test({ code: 'export * from "./foo"', + output: 'export * from "./foo"', options: [ 'always' ], errors: [ { @@ -557,6 +653,7 @@ ruleTester.run('extensions', rule, { }), test({ code: 'export * from "./foo.js"', + output: 'export * from "./foo"', options: [ 'never' ], errors: [ { @@ -568,6 +665,7 @@ ruleTester.run('extensions', rule, { }), test({ code: 'import foo from "@/ImNotAScopedModule.js"', + output: 'import foo from "@/ImNotAScopedModule"', options: ['never'], errors: [ { @@ -583,6 +681,12 @@ ruleTester.run('extensions', rule, { import bar from './bar'; `, + output: ` + import _ from 'lodash'; + import m from '@test-scope/some-module/index'; + + import bar from './bar'; + `, options: ['never'], settings: { 'import/resolver': 'webpack',