diff --git a/package.json b/package.json index fe8d5e25da3..7c532099e9b 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", 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, + }, + ], + }, ], }); diff --git a/packages/eslint-plugin/tests/rules/naming-convention.test.ts b/packages/eslint-plugin/tests/rules/naming-convention.test.ts index d35afe5ed30..b8ad03a53a6 100644 --- a/packages/eslint-plugin/tests/rules/naming-convention.test.ts +++ b/packages/eslint-plugin/tests/rules/naming-convention.test.ts @@ -1222,7 +1222,7 @@ ruleTester.run('naming-convention', rule, { { code: ` class Ignored { - private static abstract readonly some_name = 1; + private static abstract readonly some_name; IgnoredDueToModifiers = 1; } `, @@ -2060,7 +2060,7 @@ ruleTester.run('naming-convention', rule, { { code: ` class Ignored { - private static abstract readonly some_name = 1; + private static abstract readonly some_name; IgnoredDueToModifiers = 1; } `, diff --git a/yarn.lock b/yarn.lock index cf83b08f8e6..889f67f8132 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3770,7 +3770,7 @@ resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.3.tgz#e7b5aebbac150f8b5fdd4a46e7f0bd8e65e19109" integrity sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw== -"@types/prettier@*", "@types/prettier@^2.1.5", "@types/prettier@^2.3.2": +"@types/prettier@*", "@types/prettier@^2.1.5", "@types/prettier@^2.4.2": version "2.4.2" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.4.2.tgz#4c62fae93eb479660c3bd93f9d24d561597a8281" integrity sha512-ekoj4qOQYp7CvjX8ZDBgN86w3MqQhLE1hczEJbEIjgFEumDy+na/4AJAbLXfgEWFNB2pKadM5rPFtuSGMWK7xA== @@ -11706,10 +11706,10 @@ prepend-http@^2.0.0: resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= -prettier@*, prettier@2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.4.1.tgz#671e11c89c14a4cfc876ce564106c4a6726c9f5c" - integrity sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA== +prettier@*, prettier@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.0.tgz#a6370e2d4594e093270419d9cc47f7670488f893" + integrity sha512-FM/zAKgWTxj40rH03VxzIPdXmj39SwSjwG0heUcNFwI+EMZJnY93yAiKXM3dObIKAM5TA88werc8T/EwhB45eg== pretty-error@^4.0.0: version "4.0.0"