Skip to content

Commit 50b3d23

Browse files
jportnerljharb
authored andcommitted
[Fix] no-duplicates: remove duplicate identifiers in duplicate imports
1 parent 87a6096 commit 50b3d23

File tree

3 files changed

+59
-8
lines changed

3 files changed

+59
-8
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
66

77
## [Unreleased]
88

9+
### Fixed
10+
- [`no-duplicates`]: remove duplicate identifiers in duplicate imports ([#2577], thanks [@joe-matsec])
11+
912
### Changed
1013
- [Docs] [`no-duplicates`]: fix example schema ([#2684], thanks [@simmo])
1114

@@ -1389,6 +1392,7 @@ for info on changes for earlier releases.
13891392
[#2668]: https://github.com/import-js/eslint-plugin-import/issues/2668
13901393
[#2666]: https://github.com/import-js/eslint-plugin-import/issues/2666
13911394
[#2665]: https://github.com/import-js/eslint-plugin-import/issues/2665
1395+
[#2577]: https://github.com/import-js/eslint-plugin-import/issues/2577
13921396
[#2444]: https://github.com/import-js/eslint-plugin-import/issues/2444
13931397
[#2412]: https://github.com/import-js/eslint-plugin-import/issues/2412
13941398
[#2392]: https://github.com/import-js/eslint-plugin-import/issues/2392
@@ -1703,6 +1707,7 @@ for info on changes for earlier releases.
17031707
[@jimbolla]: https://github.com/jimbolla
17041708
[@jkimbo]: https://github.com/jkimbo
17051709
[@joaovieira]: https://github.com/joaovieira
1710+
[@joe-matsec]: https://github.com/joe-matsec
17061711
[@johndevedu]: https://github.com/johndevedu
17071712
[@johnthagen]: https://github.com/johnthagen
17081713
[@jonboiser]: https://github.com/jonboiser

src/rules/no-duplicates.js

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,7 @@ function getFix(first, rest, sourceCode, context) {
7979

8080
return {
8181
importNode: node,
82-
text: sourceCode.text.slice(openBrace.range[1], closeBrace.range[0]),
83-
hasTrailingComma: isPunctuator(sourceCode.getTokenBefore(closeBrace), ','),
82+
identifiers: sourceCode.text.slice(openBrace.range[1], closeBrace.range[0]).split(','), // Split the text into separate identifiers (retaining any whitespace before or after)
8483
isEmpty: !hasSpecifiers(node),
8584
};
8685
})
@@ -111,9 +110,15 @@ function getFix(first, rest, sourceCode, context) {
111110
closeBrace != null &&
112111
isPunctuator(sourceCode.getTokenBefore(closeBrace), ',');
113112
const firstIsEmpty = !hasSpecifiers(first);
113+
const firstExistingIdentifiers = firstIsEmpty
114+
? new Set()
115+
: new Set(sourceCode.text.slice(openBrace.range[1], closeBrace.range[0])
116+
.split(',')
117+
.map((x) => x.trim()),
118+
);
114119

115120
const [specifiersText] = specifiers.reduce(
116-
([result, needsComma], specifier) => {
121+
([result, needsComma, existingIdentifiers], specifier) => {
117122
const isTypeSpecifier = specifier.importNode.importKind === 'type';
118123

119124
const preferInline = context.options[0] && context.options[0]['prefer-inline'];
@@ -122,15 +127,25 @@ function getFix(first, rest, sourceCode, context) {
122127
throw new Error('Your version of TypeScript does not support inline type imports.');
123128
}
124129

125-
const insertText = `${preferInline && isTypeSpecifier ? 'type ' : ''}${specifier.text}`;
130+
// 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
131+
const [specifierText, updatedExistingIdentifiers] = specifier.identifiers.reduce(([text, set], cur) => {
132+
const trimmed = cur.trim(); // Trim whitespace before/after to compare to our set of existing identifiers
133+
const curWithType = trimmed.length > 0 && preferInline && isTypeSpecifier ? `type ${cur}` : cur;
134+
if (existingIdentifiers.has(trimmed)) {
135+
return [text, set];
136+
}
137+
return [text.length > 0 ? `${text},${curWithType}` : curWithType, set.add(trimmed)];
138+
}, ['', existingIdentifiers]);
139+
126140
return [
127-
needsComma && !specifier.isEmpty
128-
? `${result},${insertText}`
129-
: `${result}${insertText}`,
141+
needsComma && !specifier.isEmpty && specifierText.length > 0
142+
? `${result},${specifierText}`
143+
: `${result}${specifierText}`,
130144
specifier.isEmpty ? needsComma : true,
145+
updatedExistingIdentifiers,
131146
];
132147
},
133-
['', !firstHasTrailingComma && !firstIsEmpty],
148+
['', !firstHasTrailingComma && !firstIsEmpty, firstExistingIdentifiers],
134149
);
135150

136151
const fixes = [];

tests/src/rules/no-duplicates.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import jsxConfig from '../../../config/react';
55
import { RuleTester } from 'eslint';
66
import eslintPkg from 'eslint/package.json';
77
import semver from 'semver';
8+
import flatMap from 'array.prototype.flatmap';
89

910
const ruleTester = new RuleTester();
1011
const rule = require('rules/no-duplicates');
@@ -130,6 +131,36 @@ ruleTester.run('no-duplicates', rule, {
130131
errors: ['\'./foo\' imported multiple times.', '\'./foo\' imported multiple times.'],
131132
}),
132133

134+
// These test cases use duplicate import identifiers, which causes a fatal parsing error using ESPREE (default) and TS_OLD.
135+
...flatMap([parsers.BABEL_OLD, parsers.TS_NEW], parser => {
136+
if (!parser) return []; // TS_NEW is not always available
137+
return [
138+
// #2347: duplicate identifiers should be removed
139+
test({
140+
code: "import {a} from './foo'; import { a } from './foo'",
141+
output: "import {a} from './foo'; ",
142+
errors: ['\'./foo\' imported multiple times.', '\'./foo\' imported multiple times.'],
143+
parser,
144+
}),
145+
146+
// #2347: duplicate identifiers should be removed
147+
test({
148+
code: "import {a,b} from './foo'; import { b, c } from './foo'; import {b,c,d} from './foo'",
149+
output: "import {a,b, c ,d} from './foo'; ",
150+
errors: ['\'./foo\' imported multiple times.', '\'./foo\' imported multiple times.', '\'./foo\' imported multiple times.'],
151+
parser,
152+
}),
153+
154+
// #2347: duplicate identifiers should be removed, but not if they are adjacent to comments
155+
test({
156+
code: "import {a} from './foo'; import { a/*,b*/ } from './foo'",
157+
output: "import {a, a/*,b*/ } from './foo'; ",
158+
errors: ['\'./foo\' imported multiple times.', '\'./foo\' imported multiple times.'],
159+
parser,
160+
}),
161+
];
162+
}),
163+
133164
test({
134165
code: "import {x} from './foo'; import {} from './foo'; import {/*c*/} from './foo'; import {y} from './foo'",
135166
output: "import {x/*c*/,y} from './foo'; ",

0 commit comments

Comments
 (0)