diff --git a/CHANGELOG.md b/CHANGELOG.md index 06d0ebf2f..86df0edd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange - [`no-extraneous-dependencies`]: Add `includeTypes` option ([#2543], thanks [@bdwain]) - [`order`]: new `alphabetize.orderImportKind` option to sort imports with same path based on their kind (`type`, `typeof`) ([#2544], thanks [@stropho]) - [`consistent-type-specifier-style`]: add rule ([#2473], thanks [@bradzacher]) +- Add [`no-empty-named-blocks`] rule ([#2568], thanks [@guilhermelimak]) ### Fixed - [`order`]: move nested imports closer to main import entry ([#2396], thanks [@pri1311]) @@ -990,6 +991,7 @@ for info on changes for earlier releases. [`no-deprecated`]: ./docs/rules/no-deprecated.md [`no-duplicates`]: ./docs/rules/no-duplicates.md [`no-dynamic-require`]: ./docs/rules/no-dynamic-require.md +[`no-empty-named-blocks`]: ./docs/rules/no-empty-named-blocks.md [`no-extraneous-dependencies`]: ./docs/rules/no-extraneous-dependencies.md [`no-import-module-exports`]: ./docs/rules/no-import-module-exports.md [`no-internal-modules`]: ./docs/rules/no-internal-modules.md @@ -1016,6 +1018,7 @@ for info on changes for earlier releases. [`memo-parser`]: ./memo-parser/README.md [#2570]: https://github.com/import-js/eslint-plugin-import/pull/2570 +[#2568]: https://github.com/import-js/eslint-plugin-import/pull/2568 [#2546]: https://github.com/import-js/eslint-plugin-import/pull/2546 [#2541]: https://github.com/import-js/eslint-plugin-import/pull/2541 [#2531]: https://github.com/import-js/eslint-plugin-import/pull/2531 @@ -1603,13 +1606,14 @@ for info on changes for earlier releases. [@futpib]: https://github.com/futpib [@gajus]: https://github.com/gajus [@gausie]: https://github.com/gausie -[@georeith]: https://github.com/georeith [@gavriguy]: https://github.com/gavriguy +[@georeith]: https://github.com/georeith [@giodamelio]: https://github.com/giodamelio [@golopot]: https://github.com/golopot [@GoodForOneFare]: https://github.com/GoodForOneFare [@graingert]: https://github.com/graingert [@grit96]: https://github.com/grit96 +[@guilhermelimak]: https://github.com/guilhermelimak [@guillaumewuip]: https://github.com/guillaumewuip [@hayes]: https://github.com/hayes [@himynameisdave]: https://github.com/himynameisdave diff --git a/README.md b/README.md index 775fc198b..0e550f8f8 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a * Forbid the use of extraneous packages ([`no-extraneous-dependencies`]) * Forbid the use of mutable exports with `var` or `let`. ([`no-mutable-exports`]) * Report modules without exports, or exports without matching import in another module ([`no-unused-modules`]) +* Prevent empty named import blocks ([`no-empty-named-blocks`]) [`export`]: ./docs/rules/export.md [`no-named-as-default`]: ./docs/rules/no-named-as-default.md @@ -63,6 +64,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a [`no-extraneous-dependencies`]: ./docs/rules/no-extraneous-dependencies.md [`no-mutable-exports`]: ./docs/rules/no-mutable-exports.md [`no-unused-modules`]: ./docs/rules/no-unused-modules.md +[`no-empty-named-blocks`]: ./docs/rules/no-empty-named-blocks.md ### Module systems diff --git a/docs/rules/no-empty-named-blocks.md b/docs/rules/no-empty-named-blocks.md new file mode 100644 index 000000000..229d92747 --- /dev/null +++ b/docs/rules/no-empty-named-blocks.md @@ -0,0 +1,39 @@ +# import/no-empty-named-blocks + +Reports the use of empty named import blocks. + +## Rule Details + +### Valid +```js +import { mod } from 'mod' +import Default, { mod } from 'mod' +``` + +When using typescript +```js +import type { mod } from 'mod' +``` + +When using flow +```js +import typeof { mod } from 'mod' +``` + +### Invalid +```js +import {} from 'mod' +import Default, {} from 'mod' +``` + +When using typescript +```js +import type Default, {} from 'mod' +import type {} from 'mod' +``` + +When using flow +```js +import typeof {} from 'mod' +import typeof Default, {} from 'mod' +``` \ No newline at end of file diff --git a/src/index.js b/src/index.js index fd83a4aaf..15f98d96f 100644 --- a/src/index.js +++ b/src/index.js @@ -42,6 +42,7 @@ export const rules = { 'no-useless-path-segments': require('./rules/no-useless-path-segments'), 'dynamic-import-chunkname': require('./rules/dynamic-import-chunkname'), 'no-import-module-exports': require('./rules/no-import-module-exports'), + 'no-empty-named-blocks': require('./rules/no-empty-named-blocks'), // export 'exports-last': require('./rules/exports-last'), diff --git a/src/rules/no-empty-named-blocks.js b/src/rules/no-empty-named-blocks.js new file mode 100644 index 000000000..65a8515cf --- /dev/null +++ b/src/rules/no-empty-named-blocks.js @@ -0,0 +1,91 @@ +import docsUrl from '../docsUrl'; + +function getEmptyBlockRange(tokens, index) { + const token = tokens[index]; + const nextToken = tokens[index + 1]; + const prevToken = tokens[index - 1]; + let start = token.range[0]; + const end = nextToken.range[1]; + + // Remove block tokens and the previous comma + if (prevToken.value === ','|| prevToken.value === 'type' || prevToken.value === 'typeof') { + start = prevToken.range[0]; + } + + return [start, end]; +} + +module.exports = { + meta: { + type: 'suggestion', + docs: { + url: docsUrl('no-empty-named-blocks'), + }, + fixable: 'code', + schema: [], + hasSuggestions: true, + }, + + create(context) { + return { + Program(node) { + node.tokens.forEach((token, idx) => { + const nextToken = node.tokens[idx + 1]; + + if (nextToken && token.value === '{' && nextToken.value === '}') { + const hasOtherIdentifiers = node.tokens.some((token) => ( + token.type === 'Identifier' + && token.value !== 'from' + && token.value !== 'type' + && token.value !== 'typeof' + )); + + // If it has no other identifiers it's the only thing in the import, so we can either remove the import + // completely or transform it in a side-effects only import + if (!hasOtherIdentifiers) { + context.report({ + node, + message: 'Unexpected empty named import block', + suggest: [ + { + desc: 'Remove unused import', + fix(fixer) { + // Remove the whole import + return fixer.remove(node); + }, + }, + { + desc: 'Remove empty import block', + fix(fixer) { + // Remove the empty block and the 'from' token, leaving the import only for its side + // effects, e.g. `import 'mod'` + const sourceCode = context.getSourceCode(); + const fromToken = node.tokens.find(t => t.value === 'from'); + const importToken = node.tokens.find(t => t.value === 'import'); + const hasSpaceAfterFrom = sourceCode.isSpaceBetween(fromToken, sourceCode.getTokenAfter(fromToken)); + const hasSpaceAfterImport = sourceCode.isSpaceBetween(importToken, sourceCode.getTokenAfter(fromToken)); + + const [start] = getEmptyBlockRange(node.tokens, idx); + const [, end] = fromToken.range; + const range = [start, hasSpaceAfterFrom ? end + 1 : end]; + + return fixer.replaceTextRange(range, hasSpaceAfterImport ? '' : ' '); + }, + }, + ], + }); + } else { + context.report({ + node, + message: 'Unexpected empty named import block', + fix(fixer) { + return fixer.removeRange(getEmptyBlockRange(node.tokens, idx)); + }, + }); + } + } + }); + }, + }; + }, +}; diff --git a/tests/files/empty-named-blocks.js b/tests/files/empty-named-blocks.js new file mode 100644 index 000000000..4640c7f8d --- /dev/null +++ b/tests/files/empty-named-blocks.js @@ -0,0 +1 @@ +import {} from './bar.js'; diff --git a/tests/src/rules/no-empty-named-blocks.js b/tests/src/rules/no-empty-named-blocks.js new file mode 100644 index 000000000..ee21db347 --- /dev/null +++ b/tests/src/rules/no-empty-named-blocks.js @@ -0,0 +1,98 @@ +import { parsers, test } from '../utils'; + +import { RuleTester } from 'eslint'; + +const ruleTester = new RuleTester(); +const rule = require('rules/no-empty-named-blocks'); + + +function generateSuggestionsTestCases(cases, parser) { + return cases.map(code => test({ + code, + parser, + errors: [{ + suggestions: [ + { + desc: 'Remove unused import', + output: '', + }, + { + desc: 'Remove empty import block', + output: `import 'mod';`, + }, + ], + }], + })); +} + +ruleTester.run('no-empty-named-blocks', rule, { + valid: [].concat( + test({ code: `import 'mod';` }), + test({ code: `import Default from 'mod';` }), + test({ code: `import { Named } from 'mod';` }), + test({ code: `import Default, { Named } from 'mod';` }), + test({ code: `import * as Namespace from 'mod';` }), + + // Typescript + parsers.TS_NEW ? [ + test({ code: `import type Default from 'mod';`, parser: parsers.TS_NEW }), + test({ code: `import type { Named } from 'mod';`, parser: parsers.TS_NEW }), + test({ code: `import type Default, { Named } from 'mod';`, parser: parsers.TS_NEW }), + test({ code: `import type * as Namespace from 'mod';`, parser: parsers.TS_NEW }), + ] : [], + + // Flow + test({ code: `import typeof Default from 'mod';`, parser: parsers.BABEL_OLD }), + test({ code: `import typeof { Named } from 'mod';`, parser: parsers.BABEL_OLD }), + test({ code: `import typeof Default, { Named } from 'mod';`, parser: parsers.BABEL_OLD }), + ), + invalid: [].concat( + test({ + code: `import Default, {} from 'mod';`, + output: `import Default from 'mod';`, + errors: ['Unexpected empty named import block'], + }), + generateSuggestionsTestCases([ + `import {} from 'mod';`, + `import{}from'mod';`, + `import {} from'mod';`, + `import {}from 'mod';`, + ]), + + // Typescript + parsers.TS_NEW ? [].concat( + generateSuggestionsTestCases( + [ + `import type {} from 'mod';`, + `import type {}from 'mod';`, + `import type{}from 'mod';`, + `import type {}from'mod';`, + ], + parsers.TS_NEW, + ), + test({ + code: `import type Default, {} from 'mod';`, + output: `import type Default from 'mod';`, + parser: parsers.TS_NEW, + errors: ['Unexpected empty named import block'], + }), + ) : [], + + // Flow + generateSuggestionsTestCases( + [ + `import typeof {} from 'mod';`, + `import typeof {}from 'mod';`, + `import typeof {} from'mod';`, + `import typeof{}from'mod';`, + ], + parsers.BABEL_OLD, + ), + test({ + code: `import typeof Default, {} from 'mod';`, + output: `import typeof Default from 'mod';`, + parser: parsers.BABEL_OLD, + errors: ['Unexpected empty named import block'], + }), + ), +});