diff --git a/.github/workflows/node-4+.yml b/.github/workflows/node-4+.yml index 6762bf0bbd..1ef29039e6 100644 --- a/.github/workflows/node-4+.yml +++ b/.github/workflows/node-4+.yml @@ -34,6 +34,14 @@ jobs: - 3 - 2 include: + - node-version: 'lts/*' + eslint: 8 + env: + TS: '~3.9.5' + - node-version: 'lts/*' + eslint: 7 + env: + TS: '~3.9.5' - node-version: 'lts/*' eslint: 7 ts-parser: 4 diff --git a/CHANGELOG.md b/CHANGELOG.md index e349c4a0bf..d416619c23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange - [`no-restricted-paths`]: support arrays for `from` and `target` options ([#2466], thanks [@AdriAt360]) - [`no-anonymous-default-export`]: add `allowNew` option ([#2505], thanks [@DamienCassou]) - [`order`]: Add `distinctGroup` option ([#2395], thanks [@hyperupcall]) +- [`no-duplicates`]: support inline type import with `inlineTypeImport` option ([#2475], thanks [@snewcomer]) ### Fixed - [`order`]: move nested imports closer to main import entry ([#2396], thanks [@pri1311]) @@ -1715,6 +1716,7 @@ for info on changes for earlier releases. [@singles]: https://github.com/singles [@skozin]: https://github.com/skozin [@skyrpex]: https://github.com/skyrpex +[@snewcomer]: https://github.com/snewcomer [@sompylasar]: https://github.com/sompylasar [@soryy708]: https://github.com/soryy708 [@sosukesuzuki]: https://github.com/sosukesuzuki diff --git a/docs/rules/no-duplicates.md b/docs/rules/no-duplicates.md index 5252db1b79..741a125188 100644 --- a/docs/rules/no-duplicates.md +++ b/docs/rules/no-duplicates.md @@ -61,6 +61,27 @@ import SomeDefaultClass from './mod?minify' import * from './mod.js?minify' ``` +### Inline Type imports + +TypeScript 4.5 introduced a new [feature](https://devblogs.microsoft.com/typescript/announcing-typescript-4-5/#type-on-import-names) that allows mixing of named value and type imports. In order to support fixing to an inline type import when duplicate imports are detected, `inlineTypeImport` can be set to true. + +Config: + +```json +"import/no-duplicates": ["error", {"inlineTypeImport": true}] +``` + +```js +import { AValue, type AType } from './mama-mia' +import type { BType } from './mama-mia' +``` + +will fix to + +```js +import { AValue, type AType, type BType } from './mama-mia' +``` + ## When Not To Use It If the core ESLint version is good enough (i.e. you're _not_ using Flow and you _are_ using [`import/extensions`](./extensions.md)), keep it and don't use this. diff --git a/package.json b/package.json index ed87332304..ec4f2c2114 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "escope": "^3.6.0", "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8", "eslint-import-resolver-node": "file:./resolvers/node", - "eslint-import-resolver-typescript": "^1.0.2 || ^1.1.1", + "eslint-import-resolver-typescript": "^1.0.2 || ^1.1.1 || ^2.7.0", "eslint-import-resolver-webpack": "file:./resolvers/webpack", "eslint-import-test-order-redirect": "file:./tests/files/order-redirect", "eslint-module-utils": "file:./utils", @@ -92,7 +92,7 @@ "safe-publish-latest": "^2.0.0", "semver": "^6.3.0", "sinon": "^2.4.1", - "typescript": "^2.8.1 || ~3.9.5", + "typescript": "^2.8.1 || ~3.9.5 || ~4.7.3", "typescript-eslint-parser": "^15 || ^20 || ^22" }, "peerDependencies": { diff --git a/src/rules/no-duplicates.js b/src/rules/no-duplicates.js index efd9583fbc..08f2a1165f 100644 --- a/src/rules/no-duplicates.js +++ b/src/rules/no-duplicates.js @@ -7,7 +7,7 @@ function checkImports(imported, context) { const message = `'${module}' imported multiple times.`; const [first, ...rest] = nodes; const sourceCode = context.getSourceCode(); - const fix = getFix(first, rest, sourceCode); + const fix = getFix(first, rest, sourceCode, context); context.report({ node: first.source, @@ -25,7 +25,7 @@ function checkImports(imported, context) { } } -function getFix(first, rest, sourceCode) { +function getFix(first, rest, sourceCode, context) { // 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 @@ -45,7 +45,7 @@ function getFix(first, rest, sourceCode) { } const defaultImportNames = new Set( - [first, ...rest].map(getDefaultImportName).filter(Boolean), + [first].concat(rest).map(getDefaultImportName).filter(Boolean), ); // Bail if there are multiple different default import names – it's up to the @@ -108,10 +108,13 @@ function getFix(first, rest, sourceCode) { const [specifiersText] = specifiers.reduce( ([result, needsComma], specifier) => { + const isTypeSpecifier = specifier.importNode.importKind === 'type'; + const inlineTypeImport = context.options[0] && context.options[0].inlineTypeImport; + const insertText = `${inlineTypeImport && isTypeSpecifier ? 'type ' : ''}${specifier.text}`; return [ needsComma && !specifier.isEmpty - ? `${result},${specifier.text}` - : `${result}${specifier.text}`, + ? `${result},${insertText}` + : `${result}${insertText}`, specifier.isEmpty ? needsComma : true, ]; }, @@ -255,6 +258,9 @@ module.exports = { considerQueryString: { type: 'boolean', }, + inlineTypeImport: { + type: 'boolean', + }, }, additionalProperties: false, }, @@ -289,6 +295,9 @@ module.exports = { if (n.importKind === 'type') { 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; + } return hasNamespace(n) ? map.nsImported : map.imported; } diff --git a/tests/dep-time-travel.sh b/tests/dep-time-travel.sh index 665ca1ccf1..40a0e6c898 100755 --- a/tests/dep-time-travel.sh +++ b/tests/dep-time-travel.sh @@ -31,7 +31,7 @@ if [[ "$ESLINT_VERSION" -lt "4" ]]; then npm i --no-save babel-eslint@8.0.3 echo "Downgrading TypeScript dependencies..." - npm i --no-save typescript-eslint-parser@15 typescript@2.8.1 + npm i --no-save typescript-eslint-parser@15 "typescript@${TS:-2.8.1}" elif [[ "$ESLINT_VERSION" -lt "7" ]]; then echo "Downgrading TypeScript dependencies..." npm i --no-save typescript-eslint-parser@20 diff --git a/tests/src/rules/no-duplicates.js b/tests/src/rules/no-duplicates.js index cde41b3a07..97dfe3413d 100644 --- a/tests/src/rules/no-duplicates.js +++ b/tests/src/rules/no-duplicates.js @@ -430,7 +430,7 @@ context('TypeScript', function () { ruleTester.run('no-duplicates', rule, { valid: [ - // #1667: ignore duplicate if is a typescript type import + // #1667: ignore duplicate if is a typescript type import test({ code: "import type { x } from './foo'; import y from './foo'", ...parserConfig, @@ -468,6 +468,19 @@ context('TypeScript', function () { `, ...parserConfig, }), + // #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, + }), ], invalid: [ test({ @@ -520,6 +533,76 @@ context('TypeScript', function () { }, ], }), + test({ + code: "import {type x} from './foo'; import type {y} from './foo'", + ...parserConfig, + options: [{ 'inlineTypeImport': 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: [{ 'inlineTypeImport': 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,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.", + }, + ], + }), ], }); });