Skip to content

Commit

Permalink
[Fix]: no-duplicates with type imports
Browse files Browse the repository at this point in the history
  • Loading branch information
snewcomer committed Feb 12, 2023
1 parent d5fc8b6 commit d2292bb
Show file tree
Hide file tree
Showing 2 changed files with 551 additions and 42 deletions.
228 changes: 215 additions & 13 deletions src/rules/no-duplicates.js
Expand Up @@ -27,11 +27,198 @@ function checkImports(imported, context) {
message,
});
}

}
}
}

function getFix(first, rest, sourceCode, context) {
function checkTypeImports(imported, context) {
for (const [module, nodes] of imported.entries()) {
const typeImports = nodes.filter((node) => node.importKind === 'type');
if (nodes.length > 1) {
const someInlineTypeImports = nodes.filter((node) => node.specifiers.some((spec) => spec.importKind === 'type'));
if (typeImports.length > 0 && someInlineTypeImports.length > 0) {
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.
});

for (const node of rest) {
context.report({
node: node.source,
message,
});
}
}
}
}
}

function checkInlineTypeImports(imported, context) {
for (const [module, nodes] of imported.entries()) {
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.
});

for (const node of rest) {
context.report({
node: node.source,
message,
});
}
}
}
}

function isComma(token) {
return token.type === 'Punctuator' && token.value === ',';
}

function getInlineTypeFix(nodes, sourceCode) {
return fixer => {
const fixes = [];

// if (!semver.satisfies(typescriptPkg.version, '>= 4.5')) {
// throw new Error('Your version of TypeScript does not support inline type imports.');
// }

// 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'));
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 nodeClosingBrace = nodeTokens.find(token => isPunctuator(token, '}'));
// const preferInline = context.options[0] && context.options[0]['prefer-inline'];
if (nodeClosingBrace) {
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') {
fixes.push(fixer.insertTextBefore(nodeClosingBrace, `, type ${specifier.local.name}`));
} else {
fixes.push(fixer.insertTextBefore(nodeClosingBrace, `, ${specifier.local.name}`));
}
}

fixes.push(fixer.remove(node));
}
} 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 (!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
Expand Down Expand Up @@ -119,22 +306,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 [
Expand Down Expand Up @@ -173,7 +351,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));
}
Expand Down Expand Up @@ -318,14 +496,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;
Expand All @@ -350,6 +532,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);
}
},
};
Expand Down

0 comments on commit d2292bb

Please sign in to comment.