Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat(eslint-plugin): [consistent-type-imports] support TS4.5 inline i…
…mport specifiers (#4237)
  • Loading branch information
bradzacher committed Dec 6, 2021
1 parent be4d976 commit f61af7c
Show file tree
Hide file tree
Showing 2 changed files with 139 additions and 43 deletions.
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]]);
}
},
});
45 changes: 45 additions & 0 deletions packages/eslint-plugin/tests/rules/consistent-type-imports.test.ts
Expand Up @@ -114,6 +114,11 @@ ruleTester.run('consistent-type-imports', rule, {
`,
options: [{ prefer: 'no-type-imports' }],
},
`
import { type A, B } from 'foo';
type T = A;
const b = B;
`,
// exports
`
import Type from 'foo';
Expand Down Expand Up @@ -1517,5 +1522,45 @@ const a: Default = '';
],
parserOptions: withMetaParserOptions,
},
{
code: `
import { type A, B } from 'foo';
type T = A;
const b = B;
`,
output: `
import { A, B } from 'foo';
type T = A;
const b = B;
`,
options: [{ prefer: 'no-type-imports' }],
errors: [
{
messageId: 'valueOverType',
line: 2,
},
],
},
{
code: `
import { A, B, type C } from 'foo';
type T = A | C;
const b = B;
`,
output: noFormat`
import type { A} from 'foo';
import { B, type C } from 'foo';
type T = A | C;
const b = B;
`,
options: [{ prefer: 'type-imports' }],
errors: [
{
messageId: 'aImportIsOnlyTypes',
data: { typeImports: '"A"' },
line: 2,
},
],
},
],
});

0 comments on commit f61af7c

Please sign in to comment.