Skip to content

Commit

Permalink
feat(eslint-plugin): [consistent-type-imports] support TS4.5 inline i…
Browse files Browse the repository at this point in the history
…mport specifiers
  • Loading branch information
bradzacher committed Nov 30, 2021
1 parent 9f58eab commit 36c6003
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 52 deletions.
4 changes: 2 additions & 2 deletions package.json
Expand Up @@ -76,7 +76,7 @@
"@types/marked": "^3.0.2",
"@types/ncp": "^2.0.5",
"@types/node": "^16.11.4",
"@types/prettier": "^2.3.2",
"@types/prettier": "^2.4.2",
"@types/rimraf": "^3.0.2",
"@types/semver": "^7.3.9",
"@types/tmp": "^0.2.2",
Expand All @@ -102,7 +102,7 @@
"markdownlint-cli": "^0.29.0",
"ncp": "^2.0.0",
"node-fetch": "^3.0.0",
"prettier": "2.4.1",
"prettier": "2.5.0",
"pretty-format": "^27.3.1",
"rimraf": "^3.0.2",
"tmp": "^0.2.1",
Expand Down
137 changes: 94 additions & 43 deletions packages/eslint-plugin/src/rules/consistent-type-imports.ts
Expand Up @@ -28,6 +28,7 @@ interface ReportValueImport {
typeSpecifiers: TSESTree.ImportClause[]; // It has at least one element.
valueSpecifiers: TSESTree.ImportClause[];
unusedSpecifiers: TSESTree.ImportClause[];
inlineTypeSpecifiers: TSESTree.ImportSpecifier[];
}

function isImportToken(
Expand Down Expand Up @@ -106,7 +107,7 @@ export default util.createRule<Options, MessageIds>({
...(prefer === 'type-imports'
? {
// prefer type imports
ImportDeclaration(node: TSESTree.ImportDeclaration): void {
ImportDeclaration(node): void {
const source = node.source.value;
const sourceImports =
sourceImportsMap[source] ??
Expand Down Expand Up @@ -139,9 +140,18 @@ export default util.createRule<Options, MessageIds>({
}

const typeSpecifiers: TSESTree.ImportClause[] = [];
const inlineTypeSpecifiers: TSESTree.ImportSpecifier[] = [];
const valueSpecifiers: TSESTree.ImportClause[] = [];
const unusedSpecifiers: TSESTree.ImportClause[] = [];
for (const specifier of node.specifiers) {
if (
specifier.type === AST_NODE_TYPES.ImportSpecifier &&
specifier.importKind === 'type'
) {
inlineTypeSpecifiers.push(specifier);
continue;
}

const [variable] = context.getDeclaredVariables(specifier);
if (variable.references.length === 0) {
unusedSpecifiers.push(specifier);
Expand Down Expand Up @@ -229,6 +239,7 @@ export default util.createRule<Options, MessageIds>({
typeSpecifiers,
valueSpecifiers,
unusedSpecifiers,
inlineTypeSpecifiers,
});
}
},
Expand All @@ -247,7 +258,11 @@ export default util.createRule<Options, MessageIds>({
node: report.node,
messageId: 'typeOverValue',
*fix(fixer) {
yield* fixToTypeImport(fixer, report, sourceImports);
yield* fixToTypeImportDeclaration(
fixer,
report,
sourceImports,
);
},
});
} else {
Expand Down Expand Up @@ -298,13 +313,17 @@ export default util.createRule<Options, MessageIds>({
...message,
*fix(fixer) {
if (isTypeImport) {
yield* fixToValueImportInDecoMeta(
yield* fixToValueImportDeclaration(
fixer,
report,
sourceImports,
);
} else {
yield* fixToTypeImport(fixer, report, sourceImports);
yield* fixToTypeImportDeclaration(
fixer,
report,
sourceImports,
);
}
},
});
Expand All @@ -322,7 +341,21 @@ export default util.createRule<Options, MessageIds>({
node,
messageId: 'valueOverType',
fix(fixer) {
return fixToValueImport(fixer, node);
return fixRemoveTypeSpecifierFromImportDeclaration(
fixer,
node,
);
},
});
},
'ImportSpecifier[importKind = "type"]'(
node: TSESTree.ImportSpecifier,
): void {
context.report({
node,
messageId: 'valueOverType',
fix(fixer) {
return fixRemoveTypeSpecifierFromImportSpecifier(fixer, node);
},
});
},
Expand All @@ -345,20 +378,19 @@ export default util.createRule<Options, MessageIds>({
namespaceSpecifier: TSESTree.ImportNamespaceSpecifier | null;
namedSpecifiers: TSESTree.ImportSpecifier[];
} {
const defaultSpecifier: TSESTree.ImportDefaultSpecifier | null =
const defaultSpecifier =
node.specifiers[0].type === AST_NODE_TYPES.ImportDefaultSpecifier
? node.specifiers[0]
: null;
const namespaceSpecifier: TSESTree.ImportNamespaceSpecifier | null =
const namespaceSpecifier =
node.specifiers.find(
(specifier): specifier is TSESTree.ImportNamespaceSpecifier =>
specifier.type === AST_NODE_TYPES.ImportNamespaceSpecifier,
) ?? null;
const namedSpecifiers: TSESTree.ImportSpecifier[] =
node.specifiers.filter(
(specifier): specifier is TSESTree.ImportSpecifier =>
specifier.type === AST_NODE_TYPES.ImportSpecifier,
);
const namedSpecifiers = node.specifiers.filter(
(specifier): specifier is TSESTree.ImportSpecifier =>
specifier.type === AST_NODE_TYPES.ImportSpecifier,
);
return {
defaultSpecifier,
namespaceSpecifier,
Expand Down Expand Up @@ -387,7 +419,6 @@ export default util.createRule<Options, MessageIds>({
const typeNamedSpecifiersTexts: string[] = [];
const removeTypeNamedSpecifiers: TSESLint.RuleFix[] = [];
if (typeNamedSpecifiers.length === allNamedSpecifiers.length) {
// e.g.
// import Foo, {Type1, Type2} from 'foo'
// import DefType, {Type1, Type2} from 'foo'
const openingBraceToken = util.nullThrows(
Expand Down Expand Up @@ -496,7 +527,7 @@ export default util.createRule<Options, MessageIds>({
* import type { Already, Type1, Type2 } from 'foo'
* ^^^^^^^^^^^^^ insert
*/
function insertToNamedImport(
function fixInsertNamedSpecifiersInNamedSpecifierList(
fixer: TSESLint.RuleFixer,
target: TSESTree.ImportDeclaration,
insertText: string,
Expand All @@ -511,12 +542,12 @@ export default util.createRule<Options, MessageIds>({
);
const before = sourceCode.getTokenBefore(closingBraceToken)!;
if (!util.isCommaToken(before) && !util.isOpeningBraceToken(before)) {
insertText = ',' + insertText;
insertText = `,${insertText}`;
}
return fixer.insertTextBefore(closingBraceToken, insertText);
return fixer.insertTextBefore(closingBraceToken, `${insertText}`);
}

function* fixToTypeImport(
function* fixToTypeImportDeclaration(
fixer: TSESLint.RuleFixer,
report: ReportValueImport,
sourceImports: SourceImports,
Expand All @@ -527,19 +558,17 @@ export default util.createRule<Options, MessageIds>({
classifySpecifier(node);

if (namespaceSpecifier && !defaultSpecifier) {
// e.g.
// import * as types from 'foo'
yield* fixToTypeImportByInsertType(fixer, node, false);
yield* fixInsertTypeSpecifierForImportDeclaration(fixer, node, false);
return;
} else if (defaultSpecifier) {
if (
report.typeSpecifiers.includes(defaultSpecifier) &&
namedSpecifiers.length === 0 &&
!namespaceSpecifier
) {
// e.g.
// import Type from 'foo'
yield* fixToTypeImportByInsertType(fixer, node, true);
yield* fixInsertTypeSpecifierForImportDeclaration(fixer, node, true);
return;
}
} else {
Expand All @@ -549,9 +578,8 @@ export default util.createRule<Options, MessageIds>({
) &&
!namespaceSpecifier
) {
// e.g.
// import {Type1, Type2} from 'foo'
yield* fixToTypeImportByInsertType(fixer, node, false);
yield* fixInsertTypeSpecifierForImportDeclaration(fixer, node, false);
return;
}
}
Expand All @@ -569,11 +597,12 @@ export default util.createRule<Options, MessageIds>({
const afterFixes: TSESLint.RuleFix[] = [];
if (typeNamedSpecifiers.length) {
if (sourceImports.typeOnlyNamedImport) {
const insertTypeNamedSpecifiers = insertToNamedImport(
fixer,
sourceImports.typeOnlyNamedImport,
fixesNamedSpecifiers.typeNamedSpecifiersText,
);
const insertTypeNamedSpecifiers =
fixInsertNamedSpecifiersInNamedSpecifierList(
fixer,
sourceImports.typeOnlyNamedImport,
fixesNamedSpecifiers.typeNamedSpecifiersText,
);
if (sourceImports.typeOnlyNamedImport.range[1] <= node.range[0]) {
yield insertTypeNamedSpecifiers;
} else {
Expand All @@ -594,7 +623,6 @@ export default util.createRule<Options, MessageIds>({
namespaceSpecifier &&
report.typeSpecifiers.includes(namespaceSpecifier)
) {
// e.g.
// import Foo, * as Type from 'foo'
// import DefType, * as Type from 'foo'
// import DefType, * as Type from 'foo'
Expand Down Expand Up @@ -665,7 +693,7 @@ export default util.createRule<Options, MessageIds>({
yield* afterFixes;
}

function* fixToTypeImportByInsertType(
function* fixInsertTypeSpecifierForImportDeclaration(
fixer: TSESLint.RuleFixer,
node: TSESTree.ImportDeclaration,
isDefaultImport: boolean,
Expand Down Expand Up @@ -722,9 +750,19 @@ export default util.createRule<Options, MessageIds>({
}
}
}

// make sure we don't do anything like `import type {type T} from 'foo';`
for (const specifier of node.specifiers) {
if (
specifier.type === AST_NODE_TYPES.ImportSpecifier &&
specifier.importKind === 'type'
) {
yield* fixRemoveTypeSpecifierFromImportSpecifier(fixer, specifier);
}
}
}

function* fixToValueImportInDecoMeta(
function* fixToValueImportDeclaration(
fixer: TSESLint.RuleFixer,
report: ReportValueImport,
sourceImports: SourceImports,
Expand All @@ -735,18 +773,16 @@ export default util.createRule<Options, MessageIds>({
classifySpecifier(node);

if (namespaceSpecifier) {
// e.g.
// import type * as types from 'foo'
yield* fixToValueImport(fixer, node);
yield* fixRemoveTypeSpecifierFromImportDeclaration(fixer, node);
return;
} else if (defaultSpecifier) {
if (
report.valueSpecifiers.includes(defaultSpecifier) &&
namedSpecifiers.length === 0
) {
// e.g.
// import type Type from 'foo'
yield* fixToValueImport(fixer, node);
yield* fixRemoveTypeSpecifierFromImportDeclaration(fixer, node);
return;
}
} else {
Expand All @@ -755,9 +791,8 @@ export default util.createRule<Options, MessageIds>({
report.valueSpecifiers.includes(specifier),
)
) {
// e.g.
// import type {Type1, Type2} from 'foo'
yield* fixToValueImport(fixer, node);
yield* fixRemoveTypeSpecifierFromImportDeclaration(fixer, node);
return;
}
}
Expand All @@ -775,11 +810,12 @@ export default util.createRule<Options, MessageIds>({
const afterFixes: TSESLint.RuleFix[] = [];
if (valueNamedSpecifiers.length) {
if (sourceImports.valueOnlyNamedImport) {
const insertTypeNamedSpecifiers = insertToNamedImport(
fixer,
sourceImports.valueOnlyNamedImport,
fixesNamedSpecifiers.typeNamedSpecifiersText,
);
const insertTypeNamedSpecifiers =
fixInsertNamedSpecifiersInNamedSpecifierList(
fixer,
sourceImports.valueOnlyNamedImport,
fixesNamedSpecifiers.typeNamedSpecifiersText,
);
if (sourceImports.valueOnlyNamedImport.range[1] <= node.range[0]) {
yield insertTypeNamedSpecifiers;
} else {
Expand All @@ -800,7 +836,7 @@ export default util.createRule<Options, MessageIds>({
yield* afterFixes;
}

function* fixToValueImport(
function* fixRemoveTypeSpecifierFromImportDeclaration(
fixer: TSESLint.RuleFixer,
node: TSESTree.ImportDeclaration,
): IterableIterator<TSESLint.RuleFix> {
Expand All @@ -824,5 +860,20 @@ export default util.createRule<Options, MessageIds>({
);
yield fixer.removeRange([typeToken.range[0], afterToken.range[0]]);
}

function* fixRemoveTypeSpecifierFromImportSpecifier(
fixer: TSESLint.RuleFixer,
node: TSESTree.ImportSpecifier,
): IterableIterator<TSESLint.RuleFix> {
const typeToken = util.nullThrows(
sourceCode.getFirstToken(node, isTypeToken),
util.NullThrowsReasons.MissingToken('type', node.type),
);
const afterToken = util.nullThrows(
sourceCode.getTokenAfter(typeToken, { includeComments: true }),
util.NullThrowsReasons.MissingToken('any token', node.type),
);
yield fixer.removeRange([typeToken.range[0], afterToken.range[0]]);
}
},
});

0 comments on commit 36c6003

Please sign in to comment.