Skip to content

Commit d2292bb

Browse files
committed
[Fix]: no-duplicates with type imports
1 parent d5fc8b6 commit d2292bb

File tree

2 files changed

+551
-42
lines changed

2 files changed

+551
-42
lines changed

src/rules/no-duplicates.js

+215-13
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,198 @@ function checkImports(imported, context) {
2727
message,
2828
});
2929
}
30+
3031
}
3132
}
3233
}
3334

34-
function getFix(first, rest, sourceCode, context) {
35+
function checkTypeImports(imported, context) {
36+
for (const [module, nodes] of imported.entries()) {
37+
const typeImports = nodes.filter((node) => node.importKind === 'type');
38+
if (nodes.length > 1) {
39+
const someInlineTypeImports = nodes.filter((node) => node.specifiers.some((spec) => spec.importKind === 'type'));
40+
if (typeImports.length > 0 && someInlineTypeImports.length > 0) {
41+
const message = `'${module}' imported multiple times.`;
42+
const sourceCode = context.getSourceCode();
43+
const fix = getTypeFix(nodes, sourceCode, context);
44+
45+
const [first, ...rest] = nodes;
46+
context.report({
47+
node: first.source,
48+
message,
49+
fix, // Attach the autofix (if any) to the first import.
50+
});
51+
52+
for (const node of rest) {
53+
context.report({
54+
node: node.source,
55+
message,
56+
});
57+
}
58+
}
59+
}
60+
}
61+
}
62+
63+
function checkInlineTypeImports(imported, context) {
64+
for (const [module, nodes] of imported.entries()) {
65+
if (nodes.length > 1) {
66+
const message = `'${module}' imported multiple times.`;
67+
const sourceCode = context.getSourceCode();
68+
const fix = getInlineTypeFix(nodes, sourceCode);
69+
70+
const [first, ...rest] = nodes;
71+
context.report({
72+
node: first.source,
73+
message,
74+
fix, // Attach the autofix (if any) to the first import.
75+
});
76+
77+
for (const node of rest) {
78+
context.report({
79+
node: node.source,
80+
message,
81+
});
82+
}
83+
}
84+
}
85+
}
86+
87+
function isComma(token) {
88+
return token.type === 'Punctuator' && token.value === ',';
89+
}
90+
91+
function getInlineTypeFix(nodes, sourceCode) {
92+
return fixer => {
93+
const fixes = [];
94+
95+
// if (!semver.satisfies(typescriptPkg.version, '>= 4.5')) {
96+
// throw new Error('Your version of TypeScript does not support inline type imports.');
97+
// }
98+
99+
// push to first import
100+
let [firstImport, ...rest] = nodes;
101+
const valueImport = nodes.find((n) => n.specifiers.every((spec) => spec.importKind === 'value')) || nodes.find((n) => n.specifiers.some((spec) => spec.type === 'ImportDefaultSpecifier'));
102+
if (valueImport) {
103+
firstImport = valueImport;
104+
rest = nodes.filter((n) => n !== firstImport);
105+
}
106+
107+
const nodeTokens = sourceCode.getTokens(firstImport);
108+
// we are moving the rest of the Type or Inline Type imports here.
109+
const nodeClosingBrace = nodeTokens.find(token => isPunctuator(token, '}'));
110+
// const preferInline = context.options[0] && context.options[0]['prefer-inline'];
111+
if (nodeClosingBrace) {
112+
for (const node of rest) {
113+
// these will be all Type imports, no Value specifiers
114+
// then add inline type specifiers to importKind === 'type' import
115+
for (const specifier of node.specifiers) {
116+
if (specifier.importKind === 'type') {
117+
fixes.push(fixer.insertTextBefore(nodeClosingBrace, `, type ${specifier.local.name}`));
118+
} else {
119+
fixes.push(fixer.insertTextBefore(nodeClosingBrace, `, ${specifier.local.name}`));
120+
}
121+
}
122+
123+
fixes.push(fixer.remove(node));
124+
}
125+
} else {
126+
// we have a default import only
127+
const defaultSpecifier = firstImport.specifiers.find((spec) => spec.type === 'ImportDefaultSpecifier');
128+
const inlineTypeImports = [];
129+
for (const node of rest) {
130+
// these will be all Type imports, no Value specifiers
131+
// then add inline type specifiers to importKind === 'type' import
132+
for (const specifier of node.specifiers) {
133+
if (specifier.importKind === 'type') {
134+
inlineTypeImports.push(`type ${specifier.local.name}`);
135+
} else {
136+
inlineTypeImports.push(specifier.local.name);
137+
}
138+
}
139+
140+
fixes.push(fixer.remove(node));
141+
}
142+
143+
fixes.push(fixer.insertTextAfter(defaultSpecifier, `, {${inlineTypeImports.join(', ')}}`));
144+
}
145+
146+
return fixes;
147+
};
148+
}
149+
150+
function getTypeFix(nodes, sourceCode, context) {
151+
return fixer => {
152+
const fixes = [];
153+
154+
const preferInline = context.options[0] && context.options[0]['prefer-inline'];
155+
156+
if (preferInline) {
157+
if (!semver.satisfies(typescriptPkg.version, '>= 4.5')) {
158+
throw new Error('Your version of TypeScript does not support inline type imports.');
159+
}
160+
161+
// collapse all type imports to the inline type import
162+
const typeImports = nodes.filter((node) => node.importKind === 'type');
163+
const someInlineTypeImports = nodes.filter((node) => node.specifiers.some((spec) => spec.importKind === 'type'));
164+
// push to first import
165+
const firstImport = someInlineTypeImports[0];
166+
167+
if (firstImport) {
168+
const nodeTokens = sourceCode.getTokens(firstImport);
169+
// we are moving the rest of the Type imports here
170+
const nodeClosingBrace = nodeTokens.find(token => isPunctuator(token, '}'));
171+
172+
for (const node of typeImports) {
173+
for (const specifier of node.specifiers) {
174+
fixes.push(fixer.insertTextBefore(nodeClosingBrace, `, type ${specifier.local.name}`));
175+
}
176+
177+
fixes.push(fixer.remove(node));
178+
}
179+
}
180+
} else {
181+
// move inline types to type imports
182+
const typeImports = nodes.filter((node) => node.importKind === 'type');
183+
const someInlineTypeImports = nodes.filter((node) => node.specifiers.some((spec) => spec.importKind === 'type'));
184+
185+
const firstImport = typeImports[0];
186+
187+
if (firstImport) {
188+
const nodeTokens = sourceCode.getTokens(firstImport);
189+
// we are moving the rest of the Type imports here
190+
const nodeClosingBrace = nodeTokens.find(token => isPunctuator(token, '}'));
191+
192+
for (const node of someInlineTypeImports) {
193+
for (const specifier of node.specifiers) {
194+
if (specifier.importKind === 'type') {
195+
fixes.push(fixer.insertTextBefore(nodeClosingBrace, `, ${specifier.local.name}`));
196+
}
197+
}
198+
199+
if (node.specifiers.every((spec) => spec.importKind === 'type')) {
200+
fixes.push(fixer.remove(node));
201+
} else {
202+
for (const specifier of node.specifiers) {
203+
if (specifier.importKind === 'type') {
204+
const maybeComma = sourceCode.getTokenAfter(specifier);
205+
if (isComma(maybeComma)) {
206+
fixes.push(fixer.remove(maybeComma));
207+
}
208+
// TODO: remove `type`?
209+
fixes.push(fixer.remove(specifier));
210+
}
211+
}
212+
}
213+
}
214+
}
215+
}
216+
217+
return fixes;
218+
};
219+
}
220+
221+
function getFix(first, rest, sourceCode) {
35222
// Sorry ESLint <= 3 users, no autofix for you. Autofixing duplicate imports
36223
// requires multiple `fixer.whatever()` calls in the `fix`: We both need to
37224
// update the first one, and remove the rest. Support for multiple
@@ -119,22 +306,13 @@ function getFix(first, rest, sourceCode, context) {
119306

120307
const [specifiersText] = specifiers.reduce(
121308
([result, needsComma, existingIdentifiers], specifier) => {
122-
const isTypeSpecifier = specifier.importNode.importKind === 'type';
123-
124-
const preferInline = context.options[0] && context.options[0]['prefer-inline'];
125-
// 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.
126-
if (preferInline && (!typescriptPkg || !semver.satisfies(typescriptPkg.version, '>= 4.5'))) {
127-
throw new Error('Your version of TypeScript does not support inline type imports.');
128-
}
129-
130309
// 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
131310
const [specifierText, updatedExistingIdentifiers] = specifier.identifiers.reduce(([text, set], cur) => {
132311
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;
134312
if (existingIdentifiers.has(trimmed)) {
135313
return [text, set];
136314
}
137-
return [text.length > 0 ? `${text},${curWithType}` : curWithType, set.add(trimmed)];
315+
return [text.length > 0 ? `${text},${cur}` : cur, set.add(trimmed)];
138316
}, ['', existingIdentifiers]);
139317

140318
return [
@@ -173,7 +351,7 @@ function getFix(first, rest, sourceCode, context) {
173351
// `import def from './foo'` → `import def, {...} from './foo'`
174352
fixes.push(fixer.insertTextAfter(first.specifiers[0], `, {${specifiersText}}`));
175353
}
176-
} else if (!shouldAddDefault && openBrace != null && closeBrace != null) {
354+
} else if (!shouldAddDefault && openBrace != null && closeBrace != null && specifiersText) {
177355
// `import {...} './foo'` → `import {..., ...} from './foo'`
178356
fixes.push(fixer.insertTextBefore(closeBrace, specifiersText));
179357
}
@@ -318,14 +496,18 @@ module.exports = {
318496
nsImported: new Map(),
319497
defaultTypesImported: new Map(),
320498
namedTypesImported: new Map(),
499+
inlineTypesImported: new Map(),
321500
});
322501
}
323502
const map = moduleMaps.get(n.parent);
324503
if (n.importKind === 'type') {
504+
// import type Foo | import type { foo }
325505
return n.specifiers.length > 0 && n.specifiers[0].type === 'ImportDefaultSpecifier' ? map.defaultTypesImported : map.namedTypesImported;
326506
}
507+
327508
if (n.specifiers.some((spec) => spec.importKind === 'type')) {
328-
return map.namedTypesImported;
509+
// import { type foo }
510+
return map.inlineTypesImported;
329511
}
330512

331513
return hasNamespace(n) ? map.nsImported : map.imported;
@@ -350,6 +532,26 @@ module.exports = {
350532
checkImports(map.nsImported, context);
351533
checkImports(map.defaultTypesImported, context);
352534
checkImports(map.namedTypesImported, context);
535+
536+
const duplicatedImports = new Map([...map.inlineTypesImported]);
537+
map.imported.forEach((value, key) => {
538+
if (duplicatedImports.has(key)) {
539+
duplicatedImports.get(key).push(...value);
540+
} else {
541+
duplicatedImports.set(key, [value]);
542+
}
543+
});
544+
checkInlineTypeImports(duplicatedImports, context);
545+
546+
const duplicatedTypeImports = new Map([...map.inlineTypesImported]);
547+
map.namedTypesImported.forEach((value, key) => {
548+
if (duplicatedTypeImports.has(key)) {
549+
duplicatedTypeImports.get(key).push(...value);
550+
} else {
551+
duplicatedTypeImports.set(key, value);
552+
}
553+
});
554+
checkTypeImports(duplicatedTypeImports, context);
353555
}
354556
},
355557
};

0 commit comments

Comments
 (0)