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), }); + }); + });