diff --git a/src/rules/no-duplicates.js b/src/rules/no-duplicates.js
index 15515e6757..3f781c22d6 100644
--- a/src/rules/no-duplicates.js
+++ b/src/rules/no-duplicates.js
@@ -27,11 +27,203 @@ function checkImports(imported, context) {
message,
});
}
+
}
}
}
-function getFix(first, rest, sourceCode, context) {
+function checkTypeImports(imported, context) {
+ Array.from(imported).forEach(([module, nodes]) => {
+ const typeImports = nodes.filter((node) => node.importKind === 'type');
+ if (nodes.length > 1) {
+ const someInlineTypeImports = nodes.some((node) => node.specifiers.some((spec) => spec.importKind === 'type'));
+ if (typeImports.length > 0 && someInlineTypeImports) {
+ const message = `'${module}' imported multiple times.`;
+ const sourceCode = context.getSourceCode();
+ const fix = getTypeFix(nodes, sourceCode, context);
+
+ const [first, ...rest] = nodes;
+ context.report({
+ node: first.source,
+ message,
+ fix, // Attach the autofix (if any) to the first import.
+ });
+
+ rest.forEach((node) => {
+ context.report({
+ node: node.source,
+ message,
+ });
+ });
+ }
+ }
+ });
+}
+
+function checkInlineTypeImports(imported, context) {
+ Array.from(imported).forEach(([module, nodes]) => {
+ if (nodes.length > 1) {
+ const message = `'${module}' imported multiple times.`;
+ const sourceCode = context.getSourceCode();
+ const fix = getInlineTypeFix(nodes, sourceCode);
+
+ const [first, ...rest] = nodes;
+ context.report({
+ node: first.source,
+ message,
+ fix, // Attach the autofix (if any) to the first import.
+ });
+
+ rest.forEach((node) => {
+ context.report({
+ node: node.source,
+ message,
+ });
+ });
+ }
+ });
+}
+
+function isComma(token) {
+ return token.type === 'Punctuator' && token.value === ',';
+}
+
+function getInlineTypeFix(nodes, sourceCode) {
+ return fixer => {
+ const fixes = [];
+
+ // push to first import
+ let [firstImport, ...rest] = nodes;
+ // const valueImport = nodes.find((n) => n.specifiers.every((spec) => spec.importKind === 'value')) || nodes.find((n) => n.specifiers.some((spec) => spec.type === 'ImportDefaultSpecifier'));
+ const valueImport = nodes.find((n) => n.specifiers.some((spec) => spec.type === 'ImportDefaultSpecifier'));
+ if (valueImport) {
+ firstImport = valueImport;
+ rest = nodes.filter((n) => n !== firstImport);
+ }
+
+ const nodeTokens = sourceCode.getTokens(firstImport);
+ // we are moving the rest of the Type or Inline Type imports here.
+ const nodeClosingBraceIndex = nodeTokens.findIndex(token => isPunctuator(token, '}'));
+ const nodeClosingBrace = nodeTokens[nodeClosingBraceIndex];
+ const tokenBeforeClosingBrace = nodeTokens[nodeClosingBraceIndex - 1];
+ if (nodeClosingBrace) {
+ const specifiers = [];
+ rest.forEach((node) => {
+ // these will be all Type imports, no Value specifiers
+ // then add inline type specifiers to importKind === 'type' import
+ node.specifiers.forEach((specifier) => {
+ if (specifier.importKind === 'type') {
+ specifiers.push(`type ${specifier.local.name}`);
+ } else {
+ specifiers.push(specifier.local.name);
+ }
+ });
+
+ fixes.push(fixer.remove(node));
+ });
+
+ if (isComma(tokenBeforeClosingBrace)) {
+ fixes.push(fixer.insertTextBefore(nodeClosingBrace, ` ${specifiers.join(', ')}`));
+ } else {
+ fixes.push(fixer.insertTextBefore(nodeClosingBrace, `, ${specifiers.join(', ')}`));
+ }
+ } else {
+ // we have a default import only
+ const defaultSpecifier = firstImport.specifiers.find((spec) => spec.type === 'ImportDefaultSpecifier');
+ const inlineTypeImports = [];
+ for (const node of rest) {
+ // these will be all Type imports, no Value specifiers
+ // then add inline type specifiers to importKind === 'type' import
+ for (const specifier of node.specifiers) {
+ if (specifier.importKind === 'type') {
+ inlineTypeImports.push(`type ${specifier.local.name}`);
+ } else {
+ inlineTypeImports.push(specifier.local.name);
+ }
+ }
+
+ fixes.push(fixer.remove(node));
+ }
+
+ fixes.push(fixer.insertTextAfter(defaultSpecifier, `, {${inlineTypeImports.join(', ')}}`));
+ }
+
+ return fixes;
+ };
+}
+
+function getTypeFix(nodes, sourceCode, context) {
+ return fixer => {
+ const fixes = [];
+
+ const preferInline = context.options[0] && context.options[0]['prefer-inline'];
+
+ if (preferInline) {
+ if (typescriptPkg && !semver.satisfies(typescriptPkg.version, '>= 4.5')) {
+ throw new Error('Your version of TypeScript does not support inline type imports.');
+ }
+
+ // collapse all type imports to the inline type import
+ const typeImports = nodes.filter((node) => node.importKind === 'type');
+ const someInlineTypeImports = nodes.filter((node) => node.specifiers.some((spec) => spec.importKind === 'type'));
+ // push to first import
+ const firstImport = someInlineTypeImports[0];
+
+ if (firstImport) {
+ const nodeTokens = sourceCode.getTokens(firstImport);
+ // we are moving the rest of the Type imports here
+ const nodeClosingBrace = nodeTokens.find(token => isPunctuator(token, '}'));
+
+ for (const node of typeImports) {
+ for (const specifier of node.specifiers) {
+ fixes.push(fixer.insertTextBefore(nodeClosingBrace, `, type ${specifier.local.name}`));
+ }
+
+ fixes.push(fixer.remove(node));
+ }
+ }
+ } else {
+ // move inline types to type imports
+ const typeImports = nodes.filter((node) => node.importKind === 'type');
+ const someInlineTypeImports = nodes.filter((node) => node.specifiers.some((spec) => spec.importKind === 'type'));
+
+ const firstImport = typeImports[0];
+
+ if (firstImport) {
+ const nodeTokens = sourceCode.getTokens(firstImport);
+ // we are moving the rest of the Type imports here
+ const nodeClosingBrace = nodeTokens.find(token => isPunctuator(token, '}'));
+
+ for (const node of someInlineTypeImports) {
+ for (const specifier of node.specifiers) {
+ if (specifier.importKind === 'type') {
+ fixes.push(fixer.insertTextBefore(nodeClosingBrace, `, ${specifier.local.name}`));
+ }
+ }
+
+ if (node.specifiers.every((spec) => spec.importKind === 'type')) {
+ fixes.push(fixer.remove(node));
+ } else {
+ for (const specifier of node.specifiers) {
+ if (specifier.importKind === 'type') {
+ const maybeComma = sourceCode.getTokenAfter(specifier);
+ if (isComma(maybeComma)) {
+ fixes.push(fixer.remove(maybeComma));
+ }
+ // TODO: remove `type`?
+ fixes.push(fixer.remove(specifier));
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return fixes;
+ };
+}
+
+function getFix(first, rest, sourceCode) {
// Sorry ESLint <= 3 users, no autofix for you. Autofixing duplicate imports
// requires multiple `fixer.whatever()` calls in the `fix`: We both need to
// update the first one, and remove the rest. Support for multiple
@@ -119,22 +311,13 @@ function getFix(first, rest, sourceCode, context) {
const [specifiersText] = specifiers.reduce(
([result, needsComma, existingIdentifiers], specifier) => {
- const isTypeSpecifier = specifier.importNode.importKind === 'type';
-
- const preferInline = context.options[0] && context.options[0]['prefer-inline'];
- // a user might set prefer-inline but not have a supporting TypeScript version. Flow does not support inline types so this should fail in that case as well.
- if (preferInline && (!typescriptPkg || !semver.satisfies(typescriptPkg.version, '>= 4.5'))) {
- throw new Error('Your version of TypeScript does not support inline type imports.');
- }
-
// Add *only* the new identifiers that don't already exist, and track any new identifiers so we don't add them again in the next loop
const [specifierText, updatedExistingIdentifiers] = specifier.identifiers.reduce(([text, set], cur) => {
const trimmed = cur.trim(); // Trim whitespace before/after to compare to our set of existing identifiers
- const curWithType = trimmed.length > 0 && preferInline && isTypeSpecifier ? `type ${cur}` : cur;
if (existingIdentifiers.has(trimmed)) {
return [text, set];
}
- return [text.length > 0 ? `${text},${curWithType}` : curWithType, set.add(trimmed)];
+ return [text.length > 0 ? `${text},${cur}` : cur, set.add(trimmed)];
}, ['', existingIdentifiers]);
return [
@@ -173,7 +356,7 @@ function getFix(first, rest, sourceCode, context) {
// `import def from './foo'` → `import def, {...} from './foo'`
fixes.push(fixer.insertTextAfter(first.specifiers[0], `, {${specifiersText}}`));
}
- } else if (!shouldAddDefault && openBrace != null && closeBrace != null) {
+ } else if (!shouldAddDefault && openBrace != null && closeBrace != null && specifiersText) {
// `import {...} './foo'` → `import {..., ...} from './foo'`
fixes.push(fixer.insertTextBefore(closeBrace, specifiersText));
}
@@ -318,14 +501,18 @@ module.exports = {
nsImported: new Map(),
defaultTypesImported: new Map(),
namedTypesImported: new Map(),
+ inlineTypesImported: new Map(),
});
}
const map = moduleMaps.get(n.parent);
if (n.importKind === 'type') {
+ // import type Foo | import type { foo }
return n.specifiers.length > 0 && n.specifiers[0].type === 'ImportDefaultSpecifier' ? map.defaultTypesImported : map.namedTypesImported;
}
+
if (n.specifiers.some((spec) => spec.importKind === 'type')) {
- return map.namedTypesImported;
+ // import { type foo }
+ return map.inlineTypesImported;
}
return hasNamespace(n) ? map.nsImported : map.imported;
@@ -350,6 +537,26 @@ module.exports = {
checkImports(map.nsImported, context);
checkImports(map.defaultTypesImported, context);
checkImports(map.namedTypesImported, context);
+
+ const duplicatedImports = new Map(map.inlineTypesImported);
+ map.imported.forEach((value, key) => {
+ if (duplicatedImports.has(key)) {
+ duplicatedImports.get(key).push(...value);
+ } else {
+ duplicatedImports.set(key, [value]);
+ }
+ });
+ checkInlineTypeImports(duplicatedImports, context);
+
+ const duplicatedTypeImports = new Map(map.inlineTypesImported);
+ map.namedTypesImported.forEach((value, key) => {
+ if (duplicatedTypeImports.has(key)) {
+ duplicatedTypeImports.get(key).push(...value);
+ } else {
+ duplicatedTypeImports.set(key, value);
+ }
+ });
+ checkTypeImports(duplicatedTypeImports, context);
}
},
};
diff --git a/tests/src/rules/no-duplicates.js b/tests/src/rules/no-duplicates.js
index ac76c3070a..a0fc51811d 100644
--- a/tests/src/rules/no-duplicates.js
+++ b/tests/src/rules/no-duplicates.js
@@ -1,5 +1,5 @@
import * as path from 'path';
-import { test as testUtil, getNonDefaultParsers, parsers, tsVersionSatisfies, typescriptEslintParserSatisfies } from '../utils';
+import { test as testUtil, parsers, tsVersionSatisfies, typescriptEslintParserSatisfies, testFilePath } from '../utils';
import jsxConfig from '../../../config/react';
import { RuleTester } from 'eslint';
@@ -107,7 +107,7 @@ ruleTester.run('no-duplicates', rule, {
}),
test({
- code: "import type { x } from './foo'; import type { y } from './foo'",
+ code: "import type { x } from './foo'; import type { y } from './foo';",
output: "import type { x , y } from './foo'; ",
parser: parsers.BABEL_OLD,
errors: ['\'./foo\' imported multiple times.', '\'./foo\' imported multiple times.'],
@@ -455,12 +455,12 @@ import {x,y} from './foo'
import {
BULK_ACTIONS_ENABLED
} from '../constants';
-
+
const TestComponent = () => {
return
;
}
-
+
export default TestComponent;
`,
output: `
@@ -471,12 +471,12 @@ import {x,y} from './foo'
BULK_ACTIONS_ENABLED
} from '../constants';
import React from 'react';
-
+
const TestComponent = () => {
return
;
}
-
+
export default TestComponent;
`,
errors: ["'../constants' imported multiple times.", "'../constants' imported multiple times."],
@@ -485,237 +485,578 @@ import {x,y} from './foo'
],
});
-context('TypeScript', function () {
- getNonDefaultParsers()
- // Type-only imports were added in TypeScript ESTree 2.23.0
- .filter((parser) => parser !== parsers.TS_OLD)
- .forEach((parser) => {
- const parserConfig = {
- parser,
- settings: {
- 'import/parsers': { [parser]: ['.ts'] },
- 'import/resolver': { 'eslint-import-resolver-typescript': true },
- },
- };
-
- const valid = [
- // #1667: ignore duplicate if is a typescript type import
- test({
- code: "import type { x } from './foo'; import y from './foo'",
- ...parserConfig,
- }),
- test({
- code: "import type x from './foo'; import type y from './bar'",
- ...parserConfig,
- }),
- test({
- code: "import type {x} from './foo'; import type {y} from './bar'",
- ...parserConfig,
- }),
- test({
- code: "import type x from './foo'; import type {y} from './foo'",
- ...parserConfig,
- }),
- test({
- code: `
- import type {} from './module';
- import {} from './module2';
- `,
- ...parserConfig,
- }),
- test({
- code: `
- import type { Identifier } from 'module';
-
- declare module 'module2' {
- import type { Identifier } from 'module';
- }
-
- declare module 'module3' {
- import type { Identifier } from 'module';
- }
- `,
- ...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,
- }),
- ]);
+context('with types', () => {
+ const COMMON_TYPE_TESTS = {
+ valid: [
+ test({
+ code: "import type { x } from './foo'; import y from './foo'",
+ }),
+ test({
+ code: "import type x from './foo'; import type y from './bar'",
+ }),
+ test({
+ code: "import type {x} from './foo'; import type {y} from './bar'",
+ }),
+ test({
+ code: "import type x from './foo'; import type {y} from './foo'",
+ }),
+ test({
+ code: `
+ import type {} from './module';
+ import {} from './module2';
+ `,
+ }),
+ test({
+ code: `
+ import type { Identifier } from 'module';
- const invalid = [
- test({
- code: "import type x from './foo'; import type y from './foo'",
- output: "import type x from './foo'; import type y from './foo'",
- ...parserConfig,
- errors: [
- {
- line: 1,
- column: 20,
- message: "'./foo' imported multiple times.",
- },
- {
- line: 1,
- column: 48,
- message: "'./foo' imported multiple times.",
- },
- ],
- }),
- test({
- code: "import type x from './foo'; import type x from './foo'",
- output: "import type x from './foo'; ",
- ...parserConfig,
- errors: [
- {
- line: 1,
- column: 20,
- message: "'./foo' imported multiple times.",
- },
- {
- line: 1,
- column: 48,
- message: "'./foo' imported multiple times.",
- },
- ],
- }),
- test({
- code: "import type {x} from './foo'; import type {y} from './foo'",
- ...parserConfig,
- output: `import type {x,y} from './foo'; `,
- errors: [
- {
- line: 1,
- column: 22,
- message: "'./foo' imported multiple times.",
- },
- {
- line: 1,
- column: 52,
- message: "'./foo' imported multiple times.",
- },
- ],
- }),
- ].concat(!tsVersionSatisfies('>= 4.5') || !typescriptEslintParserSatisfies('>= 5.7.0') ? [] : [
- test({
- code: "import {type x} from './foo'; import type {y} from './foo'",
- ...parserConfig,
- options: [{ 'prefer-inline': false }],
- output: `import {type x,y} from './foo'; `,
- errors: [
- {
- line: 1,
- column: 22,
- message: "'./foo' imported multiple times.",
- },
- {
- line: 1,
- column: 52,
- message: "'./foo' imported multiple times.",
- },
- ],
- }),
- test({
- code: "import {type x} from 'foo'; import type {y} from 'foo'",
- ...parserConfig,
- options: [{ 'prefer-inline': true }],
- output: `import {type x,type y} from 'foo'; `,
- errors: [
- {
- line: 1,
- column: 22,
- message: "'foo' imported multiple times.",
- },
- {
- line: 1,
- column: 50,
- message: "'foo' imported multiple times.",
- },
- ],
- }),
- test({
- code: "import {type x} from 'foo'; import type {y} from 'foo'",
- ...parserConfig,
- output: `import {type x,y} from 'foo'; `,
- errors: [
- {
- line: 1,
- column: 22,
- message: "'foo' imported multiple times.",
- },
- {
- line: 1,
- column: 50,
- message: "'foo' imported multiple times.",
- },
- ],
- }),
- test({
- code: "import {type x} from './foo'; import {type y} from './foo'",
- ...parserConfig,
- options: [{ 'prefer-inline': true }],
- output: `import {type x,type y} from './foo'; `,
- errors: [
- {
- line: 1,
- column: 22,
- message: "'./foo' imported multiple times.",
- },
- {
- line: 1,
- column: 52,
- message: "'./foo' imported multiple times.",
- },
- ],
- }),
- test({
- code: "import {type x} from './foo'; import {type y} from './foo'",
- ...parserConfig,
- output: `import {type x,type y} from './foo'; `,
- errors: [
- {
- line: 1,
- column: 22,
- message: "'./foo' imported multiple times.",
- },
- {
- line: 1,
- column: 52,
- message: "'./foo' imported multiple times.",
- },
- ],
- }),
- test({
- code: "import {AValue, type x, BValue} from './foo'; import {type y} from './foo'",
- ...parserConfig,
- output: `import {AValue, type x, BValue,type y} from './foo'; `,
- errors: [
- {
- line: 1,
- column: 38,
- message: "'./foo' imported multiple times.",
- },
- {
- line: 1,
- column: 68,
- message: "'./foo' imported multiple times.",
- },
- ],
- }),
- ]);
+ declare module 'module2' {
+ import type { Identifier } from 'module';
+ }
+
+ declare module 'module3' {
+ import type { Identifier } from 'module';
+ }
+ `,
+ }),
+ ],
+ invalid: [
+ // if this is what we find, then inline regardless of version. We don't convert back to `type { x }`
+ test({
+ code: "import { type x } from './foo'; import y from './foo';",
+ output: " import y, {type x} from './foo';",
+ errors: [
+ {
+ line: 1,
+ column: 24,
+ message: "'./foo' imported multiple times.",
+ },
+ {
+ line: 1,
+ column: 47,
+ message: "'./foo' imported multiple times.",
+ },
+ ],
+ }),
+ test({
+ code: "import type x from './foo'; import type y from './foo'",
+ output: "import type x from './foo'; import type y from './foo'", // warn but no fixes
+ errors: [
+ {
+ line: 1,
+ column: 20,
+ message: "'./foo' imported multiple times.",
+ },
+ {
+ line: 1,
+ column: 48,
+ message: "'./foo' imported multiple times.",
+ },
+ ],
+ }),
+ test({
+ code: "import type x from './foo'; import type x from './foo'",
+ output: "import type x from './foo'; ",
+ errors: [
+ {
+ line: 1,
+ column: 20,
+ message: "'./foo' imported multiple times.",
+ },
+ {
+ line: 1,
+ column: 48,
+ message: "'./foo' imported multiple times.",
+ },
+ ],
+ }),
+ test({
+ code: "import type {x} from './foo'; import type {y} from './foo'",
+ output: `import type {x,y} from './foo'; `,
+ errors: [
+ {
+ line: 1,
+ column: 22,
+ message: "'./foo' imported multiple times.",
+ },
+ {
+ line: 1,
+ column: 52,
+ message: "'./foo' imported multiple times.",
+ },
+ ],
+ }),
+ test({
+ code: "import {type x} from './foo'; import {y} from './foo'",
+ output: `import {type x, y} from './foo'; `,
+ errors: [
+ {
+ line: 1,
+ column: 22,
+ message: "'./foo' imported multiple times.",
+ },
+ {
+ line: 1,
+ column: 47,
+ message: "'./foo' imported multiple times.",
+ },
+ ],
+ }),
+ ],
+ };
+
+ const COMMON_INLINE_TYPE_TESTS = {
+ valid: [
+ test({
+ code: "import { type x } from './foo'; import type y from 'foo'",
+ }),
+ ],
+ invalid: [
+ // without prefer-inline, will dedupe with type import kind
+ test({
+ code: "import type {x} from './foo'; import {type y} from './foo'",
+ options: [{ 'prefer-inline': false }],
+ output: `import type {x, y} from './foo'; `,
+ errors: [
+ {
+ line: 1,
+ column: 22,
+ message: "'./foo' imported multiple times.",
+ },
+ {
+ line: 1,
+ column: 52,
+ message: "'./foo' imported multiple times.",
+ },
+ ],
+ }),
+ // with prefer-inline, will dedupe with inline type specifier
+ test({
+ code: "import type {x} from './foo';import {type y} from './foo';",
+ options: [{ 'prefer-inline': true }],
+ output: `import {type y, type x} from './foo';`,
+ errors: [
+ {
+ line: 1,
+ column: 22,
+ message: "'./foo' imported multiple times.",
+ },
+ {
+ line: 1,
+ column: 51,
+ message: "'./foo' imported multiple times.",
+ },
+ ],
+ }),
+ // (same as above with imports switched up) without prefer-inline, will dedupe with type import kind
+ test({
+ code: "import {type x} from './foo'; import type {y} from './foo';",
+ options: [{ 'prefer-inline': false }],
+ output: ` import type {y, x} from './foo';`,
+ errors: [
+ {
+ line: 1,
+ column: 22,
+ message: "'./foo' imported multiple times.",
+ },
+ {
+ line: 1,
+ column: 52,
+ message: "'./foo' imported multiple times.",
+ },
+ ],
+ }),
+ // with prefer-inline, will dedupe with inline type specifier
+ test({
+ code: "import {type x} from 'foo'; import type {y} from 'foo'",
+ options: [{ 'prefer-inline': true }],
+ output: `import {type x, type y} from 'foo'; `,
+ errors: [
+ {
+ line: 1,
+ column: 22,
+ message: "'foo' imported multiple times.",
+ },
+ {
+ line: 1,
+ column: 50,
+ message: "'foo' imported multiple times.",
+ },
+ ],
+ }),
+ // throw in a Value import
+ test({
+ code: "import {type x, C} from './foo'; import type {y} from './foo';",
+ options: [{ 'prefer-inline': false }],
+ output: `import { C} from './foo'; import type {y, x} from './foo';`,
+ errors: [
+ {
+ line: 1,
+ column: 25,
+ message: "'./foo' imported multiple times.",
+ },
+ {
+ line: 1,
+ column: 55,
+ message: "'./foo' imported multiple times.",
+ },
+ ],
+ }),
+ // (same as above but import statements switched)
+ test({
+ code: "import type {y} from './foo'; import {type x, C} from './foo';",
+ options: [{ 'prefer-inline': false }],
+ output: `import type {y, x} from './foo'; import { C} from './foo';`,
+ errors: [
+ {
+ line: 1,
+ column: 22,
+ message: "'./foo' imported multiple times.",
+ },
+ {
+ line: 1,
+ column: 55,
+ message: "'./foo' imported multiple times.",
+ },
+ ],
+ }),
+ // with prefer-inline, will dedupe with inline type specifier
+ test({
+ code: "import {type x, C} from 'foo';import type {y} from 'foo';",
+ options: [{ 'prefer-inline': true }],
+ output: `import {type x, C, type y} from 'foo';`,
+ errors: [
+ {
+ line: 1,
+ column: 25,
+ message: "'foo' imported multiple times.",
+ },
+ {
+ line: 1,
+ column: 52,
+ message: "'foo' imported multiple times.",
+ },
+ ],
+ }),
+ // (same as above but import statements switched)
+ test({
+ code: "import type {y} from './foo'; import {type x, C} from './foo';",
+ options: [{ 'prefer-inline': true }],
+ output: ` import {type x, C, type y} from './foo';`,
+ errors: [
+ {
+ line: 1,
+ column: 22,
+ message: "'./foo' imported multiple times.",
+ },
+ {
+ line: 1,
+ column: 55,
+ message: "'./foo' imported multiple times.",
+ },
+ ],
+ }),
+ // inlines types will still dedupe without prefer-inline
+ test({
+ code: `import {type x} from './foo';import {type y} from './foo';`,
+ options: [{ 'prefer-inline': false }],
+ output: `import {type x, type y} from './foo';`,
+ errors: [
+ {
+ line: 1,
+ column: 22,
+ message: "'./foo' imported multiple times.",
+ },
+ {
+ line: 1,
+ column: 51,
+ message: "'./foo' imported multiple times.",
+ },
+ ],
+ }),
+ // inlines types will dedupe with prefer-inline
+ test({
+ code: `import {type x} from './foo';import {type y} from './foo';`,
+ options: [{ 'prefer-inline': true }],
+ output: `import {type x, type y} from './foo';`,
+ errors: [
+ {
+ line: 1,
+ column: 22,
+ message: "'./foo' imported multiple times.",
+ },
+ {
+ line: 1,
+ column: 51,
+ message: "'./foo' imported multiple times.",
+ },
+ ],
+ }),
+ // 3 imports
+ test({
+ code: `import {type x} from './foo';import {type y} from './foo';import {type z} from './foo';`,
+ output: "import {type x, type y, type z} from './foo';",
+ errors: [
+ {
+ line: 1,
+ column: 22,
+ message: "'./foo' imported multiple times.",
+ },
+ {
+ line: 1,
+ column: 51,
+ message: "'./foo' imported multiple times.",
+ },
+ {
+ line: 1,
+ column: 80,
+ message: "'./foo' imported multiple times.",
+ },
+ ],
+ }),
+ // 3 imports with default import
+ test({
+ code: "import {type x} from './foo';import {type y} from './foo';import A from './foo';",
+ output: "import A, {type x, type y} from './foo';",
+ errors: [
+ {
+ line: 1,
+ column: 22,
+ message: "'./foo' imported multiple times.",
+ },
+ {
+ line: 1,
+ column: 51,
+ message: "'./foo' imported multiple times.",
+ },
+ {
+ line: 1,
+ column: 73,
+ message: "'./foo' imported multiple times.",
+ },
+ ],
+ }),
+ // 3 imports with default import + value import
+ test({
+ code: "import {type x} from './foo';import {type y} from './foo';import A, { C } from './foo';",
+ output: "import A, { C , type x, type y} from './foo';",
+ errors: [
+ {
+ line: 1,
+ column: 22,
+ message: "'./foo' imported multiple times.",
+ },
+ {
+ line: 1,
+ column: 51,
+ message: "'./foo' imported multiple times.",
+ },
+ {
+ line: 1,
+ column: 80,
+ message: "'./foo' imported multiple times.",
+ },
+ ],
+ }),
+ // mixed imports - dedupe existing inline types without prefer-inline
+ test({
+ code: "import {AValue, type x, BValue} from './foo';import {type y, CValue} from './foo';",
+ output: "import {AValue, type x, BValue, type y, CValue} from './foo';",
+ errors: [
+ {
+ line: 1,
+ column: 38,
+ message: "'./foo' imported multiple times.",
+ },
+ {
+ line: 1,
+ column: 75,
+ message: "'./foo' imported multiple times.",
+ },
+ ],
+ }),
+ test({
+ code: "import AValue from './foo'; import {type y} from './foo';",
+ output: "import AValue, {type y} from './foo'; ",
+ errors: [
+ {
+ line: 1,
+ column: 20,
+ message: "'./foo' imported multiple times.",
+ },
+ {
+ line: 1,
+ column: 50,
+ message: "'./foo' imported multiple times.",
+ },
+ ],
+ }),
+ // switch it up
+ test({
+ code: "import {type y} from './foo';import AValue from './foo';",
+ output: `import AValue, {type y} from './foo';`,
+ errors: [
+ {
+ line: 1,
+ column: 22,
+ message: "'./foo' imported multiple times.",
+ },
+ {
+ line: 1,
+ column: 49,
+ message: "'./foo' imported multiple times.",
+ },
+ ],
+ }),
+ test({
+ code: "import AValue, {BValue} from './foo'; import {type y, CValue} from './foo';",
+ output: "import AValue, {BValue, type y, CValue} from './foo'; ",
+ errors: [
+ {
+ line: 1,
+ column: 30,
+ message: "'./foo' imported multiple times.",
+ },
+ {
+ line: 1,
+ column: 68,
+ message: "'./foo' imported multiple times.",
+ },
+ ],
+ }),
+ // will unfurl inline types to type import if not prefer-inline
+ test({
+ code: "import {AValue, type x, BValue} from './foo'; import type {y} from './foo';",
+ output: "import {AValue, BValue} from './foo'; import type {y, x} from './foo';",
+ errors: [
+ {
+ line: 1,
+ column: 38,
+ message: "'./foo' imported multiple times.",
+ },
+ {
+ line: 1,
+ column: 68,
+ message: "'./foo' imported multiple times.",
+ },
+ ],
+ }),
+ // will dedupe inline type imports with prefer-inline
+ test({
+ code: "import {AValue, type x, BValue} from './foo'; import type {y} from './foo'",
+ options: [{ 'prefer-inline': true }],
+ output: "import {AValue, type x, BValue, type y} from './foo'; ",
+ errors: [
+ {
+ line: 1,
+ column: 38,
+ message: "'./foo' imported multiple times.",
+ },
+ {
+ line: 1,
+ column: 68,
+ message: "'./foo' imported multiple times.",
+ },
+ ],
+ }),
+ test({
+ code: "import AValue, {type x, BValue} from './foo';import type {y} from './foo';",
+ options: [{ 'prefer-inline': false }],
+ // don't want to lose type information
+ output: "import AValue, { BValue} from './foo';import type {y, x} from './foo';",
+ errors: [
+ {
+ line: 1,
+ column: 38,
+ message: "'./foo' imported multiple times.",
+ },
+ {
+ line: 1,
+ column: 67,
+ message: "'./foo' imported multiple times.",
+ },
+ ],
+ }),
+ test({
+ code: "import AValue, {type x, BValue} from './foo'; import type {y} from './foo'",
+ options: [{ 'prefer-inline': true }],
+ output: `import AValue, {type x, BValue, type y} from './foo'; `,
+ errors: [
+ {
+ line: 1,
+ column: 38,
+ message: "'./foo' imported multiple times.",
+ },
+ {
+ line: 1,
+ column: 68,
+ message: "'./foo' imported multiple times.",
+ },
+ ],
+ }),
+ test({
+ code: "import { type C, } from './foo';import {AValue, BValue, } from './foo';",
+ options: [{ 'prefer-inline': true }],
+ output: "import { type C, AValue, BValue} from './foo';",
+ errors: [
+ {
+ line: 1,
+ column: 25,
+ message: "'./foo' imported multiple times.",
+ },
+ {
+ line: 1,
+ column: 64,
+ message: "'./foo' imported multiple times.",
+ },
+ ],
+ }),
+ ],
+ };
- ruleTester.run('no-duplicates', rule, {
- valid,
- invalid,
- });
+ context('Babel/Flow', () => {
+ const ruleTester = new RuleTester({
+ parser: parsers.BABEL_OLD,
+ settings: {
+ 'import/parsers': { [parsers.BABEL_OLD]: ['.js'] },
+ },
+ });
+
+ ruleTester.run('no-duplicates', rule, {
+ valid: COMMON_TYPE_TESTS.valid.concat(COMMON_INLINE_TYPE_TESTS.valid),
+ invalid: COMMON_TYPE_TESTS.invalid.concat(COMMON_INLINE_TYPE_TESTS.invalid),
+ });
+ });
+
+ context('TypeScript', () => {
+ if (!parsers.TS_NEW) {
+ return;
+ }
+
+ const ruleTester = new RuleTester({
+ parser: parsers.TS_NEW,
+ settings: {
+ 'import/parsers': { [parsers.TS_NEW]: ['.ts'] },
+ 'import/resolver': { 'eslint-import-resolver-typescript': true },
+ },
+ });
+
+ const tsSupportsInlineTypes = tsVersionSatisfies('>= 4.5') && typescriptEslintParserSatisfies('>= 5.7.0');
+
+ const withFileName = (t) => ({ ...t, filename: testFilePath('foo.ts') });
+
+ ruleTester.run('no-duplicates', rule, {
+ valid: COMMON_TYPE_TESTS.valid.concat(
+ tsSupportsInlineTypes
+ ? COMMON_INLINE_TYPE_TESTS.valid
+ : [],
+ ).map(withFileName),
+ invalid: COMMON_TYPE_TESTS.invalid.concat(
+ tsSupportsInlineTypes
+ ? COMMON_INLINE_TYPE_TESTS.invalid
+ : [],
+ ).map(withFileName),
});
+ });
+
});