Skip to content

Commit

Permalink
[New] no-duplicates: support inline type import with `inlineTypeImp…
Browse files Browse the repository at this point in the history
…ort` option
  • Loading branch information
snewcomer authored and ljharb committed Jun 10, 2022
1 parent 0dada1c commit 7cc47f8
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 9 deletions.
8 changes: 8 additions & 0 deletions .github/workflows/node-4+.yml
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -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])
Expand Down Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions docs/rules/no-duplicates.md
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions package.json
Expand Up @@ -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",
Expand All @@ -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": {
Expand Down
19 changes: 14 additions & 5 deletions src/rules/no-duplicates.js
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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,
];
},
Expand Down Expand Up @@ -255,6 +258,9 @@ module.exports = {
considerQueryString: {
type: 'boolean',
},
inlineTypeImport: {
type: 'boolean',
},
},
additionalProperties: false,
},
Expand Down Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion tests/dep-time-travel.sh
Expand Up @@ -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
Expand Down
85 changes: 84 additions & 1 deletion tests/src/rules/no-duplicates.js
Expand Up @@ -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,
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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.",
},
],
}),
],
});
});
Expand Down

0 comments on commit 7cc47f8

Please sign in to comment.