diff --git a/src/rules/no-duplicates.js b/src/rules/no-duplicates.js index b8c8d848c..f2745c781 100644 --- a/src/rules/no-duplicates.js +++ b/src/rules/no-duplicates.js @@ -259,10 +259,54 @@ function getFix(first, rest, sourceCode, context) { }; } +function shouldSkipDuplicateCheckForInlineTypes(nodes, preferInline = false) { + const importTypes = { + hasType: false, + hasSideEffect: false, + hasDefault: false, + hasNamespaceTypeImport: false, + hasTypeImportSpecifier: false, + hasOther: false, + }; + + for (const node of nodes) { + if (node.importKind === 'type') { + importTypes.hasType = true; + if (node.specifiers.length === 1 && node.specifiers[0].type === 'ImportNamespaceSpecifier') { + importTypes.hasNamespaceTypeImport = true; + } + } else if (node.specifiers.length === 0) { + importTypes.hasSideEffect = true; + } else if (node.specifiers.length === 1 && node.specifiers[0].type === 'ImportDefaultSpecifier') { + importTypes.hasDefault = true; + } else if (node.specifiers.some((spec) => spec.importKind === 'type')) { + importTypes.hasTypeImportSpecifier = true; + } else { + importTypes.hasOther = true; + break; + } + } + + if (!preferInline && importTypes.hasNamespaceTypeImport && importTypes.hasTypeImportSpecifier) { + return true; + } + + return !importTypes.hasOther + && importTypes.hasType + && !importTypes.hasTypeImportSpecifier + && (importTypes.hasSideEffect || importTypes.hasDefault); +} + /** @type {(imported: Map, context: import('eslint').Rule.RuleContext) => void} */ function checkImports(imported, context) { + const preferInline = context.options[0] && context.options[0]['prefer-inline']; + for (const [module, nodes] of imported.entries()) { if (nodes.length > 1) { + if (shouldSkipDuplicateCheckForInlineTypes(nodes, preferInline)) { + continue; + } + const message = `'${module}' imported multiple times.`; const [first, ...rest] = nodes; const sourceCode = getSourceCode(context); diff --git a/tests/src/rules/no-duplicates.js b/tests/src/rules/no-duplicates.js index cf57a3d59..df4ffe8ea 100644 --- a/tests/src/rules/no-duplicates.js +++ b/tests/src/rules/no-duplicates.js @@ -538,21 +538,48 @@ context('TypeScript', function () { `, ...parserConfig, }), - ].concat(!tsVersionSatisfies('>= 4.5') || !typescriptEslintParserSatisfies('>= 5.7.0') ? [] : [ + ] + // babel-eslint does not support `import type * as` + .concat(parser !== parsers.BABEL_OLD ? [ + test({ + code: ` + import type * as A from 'a'; + import { type B } from 'a'; + `, + options: [{ 'prefer-inline': false }], + ...parserConfig, + })] : []) + .concat(!tsVersionSatisfies('>= 4.5') || !typescriptEslintParserSatisfies('>= 5.7.0') ? [] : [ // #2470: ignore duplicate if is a typescript inline type import - test({ - code: "import { type x } from './foo'; import y from './foo'", - ...parserConfig, - }), - test({ - code: "import { type x } from './foo'; import { y } from './foo'", - ...parserConfig, - }), - test({ - code: "import { type x } from './foo'; import type y from 'foo'", - ...parserConfig, - }), - ]); + test({ + code: "import { type x } from './foo'; import y from './foo'", + ...parserConfig, + }), + test({ + code: "import { type x } from './foo'; import { y } from './foo'", + ...parserConfig, + }), + test({ + code: "import { type x } from './foo'; import type y from 'foo'", + ...parserConfig, + }), + test({ + code: ` + import type { A } from 'a'; + import 'a'; + `, + options: [{ 'prefer-inline': true }], + ...parserConfig, + }), + test({ + code: ` + import type { A } from 'a'; + import B from 'a'; + `, + options: [{ 'prefer-inline': true }], + ...parserConfig, + }), + ]); const invalid = [ test(withoutAutofixOutput({ @@ -750,7 +777,7 @@ context('TypeScript', function () { }), ]); - ruleTester.run('no-duplicates', rule, { + ruleTester.run(`no-duplicates${parser}`, rule, { valid, invalid, });