From 4045eed7664e06094557dfb274a64cca17d5668c Mon Sep 17 00:00:00 2001 From: Todor Andonov Date: Wed, 25 Jun 2025 21:47:36 +0300 Subject: [PATCH 1/7] fix --- src/rules/no-duplicates.js | 12 ++++++++++++ tests/src/rules/no-duplicates.js | 8 ++++++++ 2 files changed, 20 insertions(+) diff --git a/src/rules/no-duplicates.js b/src/rules/no-duplicates.js index b8c8d848ca..4b669b96b7 100644 --- a/src/rules/no-duplicates.js +++ b/src/rules/no-duplicates.js @@ -261,8 +261,20 @@ function getFix(first, rest, sourceCode, context) { /** @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 (preferInline) { + const typeImports = nodes.filter((node) => node.importKind === 'type'); + const sideEffectImports = nodes.filter((node) => node.specifiers.length === 0); + const valueImports = nodes.filter((node) => !typeImports.includes(node) && !sideEffectImports.includes(node)); + + if (typeImports.length > 0 && sideEffectImports.length > 0 && valueImports.length === 0) { + 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 cf57a3d599..e1f7b77df4 100644 --- a/tests/src/rules/no-duplicates.js +++ b/tests/src/rules/no-duplicates.js @@ -552,6 +552,14 @@ context('TypeScript', function () { 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, + }), ]); const invalid = [ From 761574758eceae6f47809312ea19c30661fa269d Mon Sep 17 00:00:00 2001 From: Todor Andonov Date: Sun, 29 Jun 2025 14:34:58 +0300 Subject: [PATCH 2/7] apply suggested change --- src/rules/no-duplicates.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/rules/no-duplicates.js b/src/rules/no-duplicates.js index 4b669b96b7..f8835b8593 100644 --- a/src/rules/no-duplicates.js +++ b/src/rules/no-duplicates.js @@ -266,11 +266,21 @@ function checkImports(imported, context) { for (const [module, nodes] of imported.entries()) { if (nodes.length > 1) { if (preferInline) { - const typeImports = nodes.filter((node) => node.importKind === 'type'); - const sideEffectImports = nodes.filter((node) => node.specifiers.length === 0); - const valueImports = nodes.filter((node) => !typeImports.includes(node) && !sideEffectImports.includes(node)); + let hasType = false; + let hasSideEffect = false; + let hasOther = false; + for (let i = 0; !hasOther && i < nodes.length; i += 1) { + const node = nodes[i]; + if (node.importKind === 'type') { + hasType = true; + } else if (node.specifiers.length === 0) { + hasSideEffect = true; + } else { + hasOther = true; + } + } - if (typeImports.length > 0 && sideEffectImports.length > 0 && valueImports.length === 0) { + if (!hasOther && hasType && hasSideEffect) { continue; } } From f7cd8bdbe35894ad2428b82adcb23570564c924e Mon Sep 17 00:00:00 2001 From: Todor Andonov Date: Tue, 1 Jul 2025 20:39:41 +0300 Subject: [PATCH 3/7] add has default --- src/rules/no-duplicates.js | 5 ++++- tests/src/rules/no-duplicates.js | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/rules/no-duplicates.js b/src/rules/no-duplicates.js index f8835b8593..10d152ac52 100644 --- a/src/rules/no-duplicates.js +++ b/src/rules/no-duplicates.js @@ -269,18 +269,21 @@ function checkImports(imported, context) { let hasType = false; let hasSideEffect = false; let hasOther = false; + let hasDefault = false; for (let i = 0; !hasOther && i < nodes.length; i += 1) { const node = nodes[i]; if (node.importKind === 'type') { hasType = true; } else if (node.specifiers.length === 0) { hasSideEffect = true; + } else if (node.specifiers.length === 1 && node.specifiers[0].type === 'ImportDefaultSpecifier') { + hasDefault = true; } else { hasOther = true; } } - if (!hasOther && hasType && hasSideEffect) { + if (!hasOther && hasType && (hasSideEffect || hasDefault)) { continue; } } diff --git a/tests/src/rules/no-duplicates.js b/tests/src/rules/no-duplicates.js index e1f7b77df4..324768b276 100644 --- a/tests/src/rules/no-duplicates.js +++ b/tests/src/rules/no-duplicates.js @@ -560,6 +560,14 @@ context('TypeScript', function () { options: [{ 'prefer-inline': true }], ...parserConfig, }), + test({ + code: ` + import type { A } from 'a'; + import B from 'a'; + `, + options: [{ 'prefer-inline': true }], + ...parserConfig, + }), ]); const invalid = [ From 44bd80a4c0cc191555b6e08cec75bd71988b4752 Mon Sep 17 00:00:00 2001 From: Todor Andonov Date: Tue, 1 Jul 2025 20:48:40 +0300 Subject: [PATCH 4/7] fix --- src/rules/no-duplicates.js | 8 +++++++- tests/src/rules/no-duplicates.js | 8 ++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/rules/no-duplicates.js b/src/rules/no-duplicates.js index 10d152ac52..7830841c72 100644 --- a/src/rules/no-duplicates.js +++ b/src/rules/no-duplicates.js @@ -276,7 +276,13 @@ function checkImports(imported, context) { hasType = true; } else if (node.specifiers.length === 0) { hasSideEffect = true; - } else if (node.specifiers.length === 1 && node.specifiers[0].type === 'ImportDefaultSpecifier') { + } else if ( + node.specifiers.length === 1 + && ( + node.specifiers[0].type === 'ImportDefaultSpecifier' + || node.specifiers.some((spec) => spec.importKind === 'type') + ) + ) { hasDefault = true; } else { hasOther = true; diff --git a/tests/src/rules/no-duplicates.js b/tests/src/rules/no-duplicates.js index 324768b276..26a716d482 100644 --- a/tests/src/rules/no-duplicates.js +++ b/tests/src/rules/no-duplicates.js @@ -568,6 +568,14 @@ context('TypeScript', function () { options: [{ 'prefer-inline': true }], ...parserConfig, }), + test({ + code: ` + import type B from 'a'; + import { type A } from 'a'; + `, + options: [{ 'prefer-inline': true }], + ...parserConfig, + }), ]); const invalid = [ From 0f812e4dd5f940521be1c3e71523cfbea22049c8 Mon Sep 17 00:00:00 2001 From: Todor Andonov Date: Wed, 2 Jul 2025 08:47:52 +0300 Subject: [PATCH 5/7] remove duplicate test case --- src/rules/no-duplicates.js | 11 ++++++----- tests/src/rules/no-duplicates.js | 8 -------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/rules/no-duplicates.js b/src/rules/no-duplicates.js index 7830841c72..121eb45bd8 100644 --- a/src/rules/no-duplicates.js +++ b/src/rules/no-duplicates.js @@ -269,6 +269,7 @@ function checkImports(imported, context) { let hasType = false; let hasSideEffect = false; let hasOther = false; + let hasTypeSpecifier = false; let hasDefault = false; for (let i = 0; !hasOther && i < nodes.length; i += 1) { const node = nodes[i]; @@ -278,18 +279,18 @@ function checkImports(imported, context) { hasSideEffect = true; } else if ( node.specifiers.length === 1 - && ( - node.specifiers[0].type === 'ImportDefaultSpecifier' - || node.specifiers.some((spec) => spec.importKind === 'type') - ) + && node.specifiers[0].type === 'ImportDefaultSpecifier' + ) { hasDefault = true; + } else if (node.specifiers.some((spec) => spec.importKind === 'type')) { + hasTypeSpecifier = true; } else { hasOther = true; } } - if (!hasOther && hasType && (hasSideEffect || hasDefault)) { + if (!hasOther && hasType && !hasTypeSpecifier && (hasSideEffect || hasDefault)) { continue; } } diff --git a/tests/src/rules/no-duplicates.js b/tests/src/rules/no-duplicates.js index 26a716d482..324768b276 100644 --- a/tests/src/rules/no-duplicates.js +++ b/tests/src/rules/no-duplicates.js @@ -568,14 +568,6 @@ context('TypeScript', function () { options: [{ 'prefer-inline': true }], ...parserConfig, }), - test({ - code: ` - import type B from 'a'; - import { type A } from 'a'; - `, - options: [{ 'prefer-inline': true }], - ...parserConfig, - }), ]); const invalid = [ From 0afe310611cd0e68eccaf5108cae0a45689f6fee Mon Sep 17 00:00:00 2001 From: Todor Andonov Date: Wed, 2 Jul 2025 08:52:29 +0300 Subject: [PATCH 6/7] extract to function --- src/rules/no-duplicates.js | 60 ++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/src/rules/no-duplicates.js b/src/rules/no-duplicates.js index 121eb45bd8..73ef063f0b 100644 --- a/src/rules/no-duplicates.js +++ b/src/rules/no-duplicates.js @@ -259,40 +259,44 @@ function getFix(first, rest, sourceCode, context) { }; } +function shouldSkipDuplicateCheckForInlineTypes(nodes) { + const importTypes = { + hasType: false, + hasSideEffect: false, + hasDefault: false, + hasTypeSpecifier: false, + hasOther: false, + }; + + for (const node of nodes) { + if (node.importKind === 'type') { + importTypes.hasType = 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.hasTypeSpecifier = true; + } else { + importTypes.hasOther = true; + break; + } + } + + return !importTypes.hasOther + && importTypes.hasType + && !importTypes.hasTypeSpecifier + && (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 (preferInline) { - let hasType = false; - let hasSideEffect = false; - let hasOther = false; - let hasTypeSpecifier = false; - let hasDefault = false; - for (let i = 0; !hasOther && i < nodes.length; i += 1) { - const node = nodes[i]; - if (node.importKind === 'type') { - hasType = true; - } else if (node.specifiers.length === 0) { - hasSideEffect = true; - } else if ( - node.specifiers.length === 1 - && node.specifiers[0].type === 'ImportDefaultSpecifier' - - ) { - hasDefault = true; - } else if (node.specifiers.some((spec) => spec.importKind === 'type')) { - hasTypeSpecifier = true; - } else { - hasOther = true; - } - } - - if (!hasOther && hasType && !hasTypeSpecifier && (hasSideEffect || hasDefault)) { - continue; - } + if (preferInline && shouldSkipDuplicateCheckForInlineTypes(nodes)) { + continue; } const message = `'${module}' imported multiple times.`; From 6ab02d0f1abd09956550ed94a859980049a7836e Mon Sep 17 00:00:00 2001 From: Todor Andonov Date: Wed, 2 Jul 2025 09:15:40 +0300 Subject: [PATCH 7/7] fix --- src/rules/no-duplicates.js | 18 +++++++--- tests/src/rules/no-duplicates.js | 61 +++++++++++++++++++------------- 2 files changed, 49 insertions(+), 30 deletions(-) diff --git a/src/rules/no-duplicates.js b/src/rules/no-duplicates.js index 73ef063f0b..f2745c7817 100644 --- a/src/rules/no-duplicates.js +++ b/src/rules/no-duplicates.js @@ -259,33 +259,41 @@ function getFix(first, rest, sourceCode, context) { }; } -function shouldSkipDuplicateCheckForInlineTypes(nodes) { +function shouldSkipDuplicateCheckForInlineTypes(nodes, preferInline = false) { const importTypes = { hasType: false, hasSideEffect: false, hasDefault: false, - hasTypeSpecifier: 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.hasTypeSpecifier = true; + importTypes.hasTypeImportSpecifier = true; } else { importTypes.hasOther = true; break; } } + if (!preferInline && importTypes.hasNamespaceTypeImport && importTypes.hasTypeImportSpecifier) { + return true; + } + return !importTypes.hasOther && importTypes.hasType - && !importTypes.hasTypeSpecifier + && !importTypes.hasTypeImportSpecifier && (importTypes.hasSideEffect || importTypes.hasDefault); } @@ -295,7 +303,7 @@ function checkImports(imported, context) { for (const [module, nodes] of imported.entries()) { if (nodes.length > 1) { - if (preferInline && shouldSkipDuplicateCheckForInlineTypes(nodes)) { + if (shouldSkipDuplicateCheckForInlineTypes(nodes, preferInline)) { continue; } diff --git a/tests/src/rules/no-duplicates.js b/tests/src/rules/no-duplicates.js index 324768b276..df4ffe8eaa 100644 --- a/tests/src/rules/no-duplicates.js +++ b/tests/src/rules/no-duplicates.js @@ -538,37 +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: ` + 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: ` + options: [{ 'prefer-inline': true }], + ...parserConfig, + }), + test({ + code: ` import type { A } from 'a'; import B from 'a'; `, - options: [{ 'prefer-inline': true }], - ...parserConfig, - }), - ]); + options: [{ 'prefer-inline': true }], + ...parserConfig, + }), + ]); const invalid = [ test(withoutAutofixOutput({ @@ -766,7 +777,7 @@ context('TypeScript', function () { }), ]); - ruleTester.run('no-duplicates', rule, { + ruleTester.run(`no-duplicates${parser}`, rule, { valid, invalid, });