diff --git a/packages/eslint-plugin/src/rules/consistent-type-imports.ts b/packages/eslint-plugin/src/rules/consistent-type-imports.ts index d549ab8f728..538c89c3cf3 100644 --- a/packages/eslint-plugin/src/rules/consistent-type-imports.ts +++ b/packages/eslint-plugin/src/rules/consistent-type-imports.ts @@ -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( @@ -106,7 +107,7 @@ export default util.createRule({ ...(prefer === 'type-imports' ? { // prefer type imports - ImportDeclaration(node: TSESTree.ImportDeclaration): void { + ImportDeclaration(node): void { const source = node.source.value; const sourceImports = sourceImportsMap[source] ?? @@ -139,9 +140,18 @@ export default util.createRule({ } 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); @@ -229,6 +239,7 @@ export default util.createRule({ typeSpecifiers, valueSpecifiers, unusedSpecifiers, + inlineTypeSpecifiers, }); } }, @@ -247,7 +258,11 @@ export default util.createRule({ node: report.node, messageId: 'typeOverValue', *fix(fixer) { - yield* fixToTypeImport(fixer, report, sourceImports); + yield* fixToTypeImportDeclaration( + fixer, + report, + sourceImports, + ); }, }); } else { @@ -298,13 +313,17 @@ export default util.createRule({ ...message, *fix(fixer) { if (isTypeImport) { - yield* fixToValueImportInDecoMeta( + yield* fixToValueImportDeclaration( fixer, report, sourceImports, ); } else { - yield* fixToTypeImport(fixer, report, sourceImports); + yield* fixToTypeImportDeclaration( + fixer, + report, + sourceImports, + ); } }, }); @@ -322,7 +341,21 @@ export default util.createRule({ 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); }, }); }, @@ -345,20 +378,19 @@ export default util.createRule({ 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, @@ -387,7 +419,6 @@ export default util.createRule({ 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( @@ -496,7 +527,7 @@ export default util.createRule({ * import type { Already, Type1, Type2 } from 'foo' * ^^^^^^^^^^^^^ insert */ - function insertToNamedImport( + function fixInsertNamedSpecifiersInNamedSpecifierList( fixer: TSESLint.RuleFixer, target: TSESTree.ImportDeclaration, insertText: string, @@ -511,12 +542,12 @@ export default util.createRule({ ); 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, @@ -527,9 +558,8 @@ export default util.createRule({ 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 ( @@ -537,9 +567,8 @@ export default util.createRule({ namedSpecifiers.length === 0 && !namespaceSpecifier ) { - // e.g. // import Type from 'foo' - yield* fixToTypeImportByInsertType(fixer, node, true); + yield* fixInsertTypeSpecifierForImportDeclaration(fixer, node, true); return; } } else { @@ -549,9 +578,8 @@ export default util.createRule({ ) && !namespaceSpecifier ) { - // e.g. // import {Type1, Type2} from 'foo' - yield* fixToTypeImportByInsertType(fixer, node, false); + yield* fixInsertTypeSpecifierForImportDeclaration(fixer, node, false); return; } } @@ -569,11 +597,12 @@ export default util.createRule({ 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 { @@ -594,7 +623,6 @@ export default util.createRule({ 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' @@ -665,7 +693,7 @@ export default util.createRule({ yield* afterFixes; } - function* fixToTypeImportByInsertType( + function* fixInsertTypeSpecifierForImportDeclaration( fixer: TSESLint.RuleFixer, node: TSESTree.ImportDeclaration, isDefaultImport: boolean, @@ -722,9 +750,19 @@ export default util.createRule({ } } } + + // 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, @@ -735,18 +773,16 @@ export default util.createRule({ 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 { @@ -755,9 +791,8 @@ export default util.createRule({ report.valueSpecifiers.includes(specifier), ) ) { - // e.g. // import type {Type1, Type2} from 'foo' - yield* fixToValueImport(fixer, node); + yield* fixRemoveTypeSpecifierFromImportDeclaration(fixer, node); return; } } @@ -775,11 +810,12 @@ export default util.createRule({ 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 { @@ -800,7 +836,7 @@ export default util.createRule({ yield* afterFixes; } - function* fixToValueImport( + function* fixRemoveTypeSpecifierFromImportDeclaration( fixer: TSESLint.RuleFixer, node: TSESTree.ImportDeclaration, ): IterableIterator { @@ -824,5 +860,20 @@ export default util.createRule({ ); yield fixer.removeRange([typeToken.range[0], afterToken.range[0]]); } + + function* fixRemoveTypeSpecifierFromImportSpecifier( + fixer: TSESLint.RuleFixer, + node: TSESTree.ImportSpecifier, + ): IterableIterator { + 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]]); + } }, }); diff --git a/packages/eslint-plugin/tests/rules/consistent-type-imports.test.ts b/packages/eslint-plugin/tests/rules/consistent-type-imports.test.ts index 07005180ce0..cc841bcdfa5 100644 --- a/packages/eslint-plugin/tests/rules/consistent-type-imports.test.ts +++ b/packages/eslint-plugin/tests/rules/consistent-type-imports.test.ts @@ -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'; @@ -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, + }, + ], + }, ], });