From 58b1c2d463f34895798b9a61340e49ffc3ec4f1a Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Mon, 17 Aug 2020 03:47:48 +0900 Subject: [PATCH] feat(eslint-plugin): add `consistent-type-imports` rule (#2367) --- packages/eslint-plugin/README.md | 1 + .../docs/rules/consistent-type-imports.md | 64 + packages/eslint-plugin/src/configs/all.ts | 1 + .../src/rules/consistent-type-imports.ts | 570 +++++++++ packages/eslint-plugin/src/rules/index.ts | 2 + .../rules/consistent-type-imports.test.ts | 1044 +++++++++++++++++ 6 files changed, 1682 insertions(+) create mode 100644 packages/eslint-plugin/docs/rules/consistent-type-imports.md create mode 100644 packages/eslint-plugin/src/rules/consistent-type-imports.ts create mode 100644 packages/eslint-plugin/tests/rules/consistent-type-imports.test.ts diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 502551bf8d9..fd77a7dceba 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -106,6 +106,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int | [`@typescript-eslint/class-literal-property-style`](./docs/rules/class-literal-property-style.md) | Ensures that literals on classes are exposed in a consistent style | | :wrench: | | | [`@typescript-eslint/consistent-type-assertions`](./docs/rules/consistent-type-assertions.md) | Enforces consistent usage of type assertions | | | | | [`@typescript-eslint/consistent-type-definitions`](./docs/rules/consistent-type-definitions.md) | Consistent with type definition either `interface` or `type` | | :wrench: | | +| [`@typescript-eslint/consistent-type-imports`](./docs/rules/consistent-type-imports.md) | Enforces consistent usage of type imports | | :wrench: | | | [`@typescript-eslint/explicit-function-return-type`](./docs/rules/explicit-function-return-type.md) | Require explicit return types on functions and class methods | | | | | [`@typescript-eslint/explicit-member-accessibility`](./docs/rules/explicit-member-accessibility.md) | Require explicit accessibility modifiers on class properties and methods | | :wrench: | | | [`@typescript-eslint/explicit-module-boundary-types`](./docs/rules/explicit-module-boundary-types.md) | Require explicit return and argument types on exported functions' and classes' public class methods | :heavy_check_mark: | | | diff --git a/packages/eslint-plugin/docs/rules/consistent-type-imports.md b/packages/eslint-plugin/docs/rules/consistent-type-imports.md new file mode 100644 index 00000000000..3487bf9b5ee --- /dev/null +++ b/packages/eslint-plugin/docs/rules/consistent-type-imports.md @@ -0,0 +1,64 @@ +# Enforces consistent usage of type imports (`consistent-type-imports`) + +TypeScript 3.8 added support for type-only imports. +Type-only imports allow you to specify that an import can only be used in a type location, allowing certain optimizations within compilers. + +## Rule Details + +This rule aims to standardize the use of type imports style across the codebase. + +## Options + +```ts +type Options = { + prefer: 'type-imports' | 'no-type-imports'; + disallowTypeAnnotations: boolean; +}; + +const defaultOptions: Options = { + prefer: 'type-imports', + disallowTypeAnnotations: true, +}; +``` + +### `prefer` + +This option defines the expected import kind for type-only imports. Valid values for `prefer` are: + +- `type-imports` will enforce that you always use `import type Foo from '...'`. It is default. +- `no-type-imports` will enforce that you always use `import Foo from '...'`. + +Examples of **correct** code with `{prefer: 'type-imports'}`, and **incorrect** code with `{prefer: 'no-type-imports'}`. + +```ts +import type { Foo } from 'Foo'; +import type Bar from 'Bar'; +type T = Foo; +const x: Bar = 1; +``` + +Examples of **incorrect** code with `{prefer: 'type-imports'}`, and **correct** code with `{prefer: 'no-type-imports'}`. + +```ts +import { Foo } from 'Foo'; +import Bar from 'Bar'; +type T = Foo; +const x: Bar = 1; +``` + +### `disallowTypeAnnotations` + +If `true`, type imports in type annotations (`import()`) is not allowed. +Default is `true`. + +Examples of **incorrect** code with `{disallowTypeAnnotations: true}`. + +```ts +type T = import('Foo').Foo; +const x: import('Bar') = 1; +``` + +## When Not To Use It + +- If you are not using TypeScript 3.8 (or greater), then you will not be able to use this rule, as type-only imports are not allowed. +- If you specifically want to use both import kinds for stylistic reasons, you can disable this rule. diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index 10654255245..c3edea830e8 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -18,6 +18,7 @@ export = { '@typescript-eslint/comma-spacing': 'error', '@typescript-eslint/consistent-type-assertions': 'error', '@typescript-eslint/consistent-type-definitions': 'error', + '@typescript-eslint/consistent-type-imports': 'error', 'default-param-last': 'off', '@typescript-eslint/default-param-last': 'error', 'dot-notation': 'off', diff --git a/packages/eslint-plugin/src/rules/consistent-type-imports.ts b/packages/eslint-plugin/src/rules/consistent-type-imports.ts new file mode 100644 index 00000000000..830c7b6669a --- /dev/null +++ b/packages/eslint-plugin/src/rules/consistent-type-imports.ts @@ -0,0 +1,570 @@ +import { + TSESLint, + TSESTree, + AST_TOKEN_TYPES, + AST_NODE_TYPES, +} from '@typescript-eslint/experimental-utils'; +import * as util from '../util'; + +type Prefer = 'type-imports' | 'no-type-imports'; + +type Options = [ + { + prefer?: Prefer; + disallowTypeAnnotations?: boolean; + }, +]; + +interface SourceImports { + source: string; + reportValueImports: ReportValueImport[]; + // ImportDeclaration for type-only import only with named imports. + typeOnlyNamedImport: TSESTree.ImportDeclaration | null; +} +interface ReportValueImport { + node: TSESTree.ImportDeclaration; + typeSpecifiers: TSESTree.ImportClause[]; // It has at least one element. + valueSpecifiers: TSESTree.ImportClause[]; + unusedSpecifiers: TSESTree.ImportClause[]; +} + +function isImportToken( + token: TSESTree.Token | TSESTree.Comment, +): token is TSESTree.KeywordToken & { value: 'import' } { + return token.type === AST_TOKEN_TYPES.Keyword && token.value === 'import'; +} + +function isTypeToken( + token: TSESTree.Token | TSESTree.Comment, +): token is TSESTree.IdentifierToken & { value: 'type' } { + return token.type === AST_TOKEN_TYPES.Identifier && token.value === 'type'; +} +type MessageIds = + | 'typeOverValue' + | 'someImportsAreOnlyTypes' + | 'aImportIsOnlyTypes' + | 'valueOverType' + | 'noImportTypeAnnotations'; +export default util.createRule({ + name: 'consistent-type-imports', + meta: { + type: 'suggestion', + docs: { + description: 'Enforces consistent usage of type imports', + category: 'Stylistic Issues', + recommended: false, + }, + messages: { + typeOverValue: + 'All imports in the declaration are only used as types. Use `import type`', + someImportsAreOnlyTypes: 'Imports {{typeImports}} are only used as types', + aImportIsOnlyTypes: 'Import {{typeImports}} is only used as types', + valueOverType: 'Use an `import` instead of an `import type`.', + noImportTypeAnnotations: '`import()` type annotations are forbidden.', + }, + schema: [ + { + type: 'object', + properties: { + prefer: { + enum: ['type-imports', 'no-type-imports'], + }, + disallowTypeAnnotations: { + type: 'boolean', + }, + }, + additionalProperties: false, + }, + ], + fixable: 'code', + }, + + defaultOptions: [ + { + prefer: 'type-imports', + disallowTypeAnnotations: true, + }, + ], + + create(context, [option]) { + const prefer = option.prefer ?? 'type-imports'; + const disallowTypeAnnotations = option.disallowTypeAnnotations !== false; + const sourceCode = context.getSourceCode(); + + const sourceImportsMap: { [key: string]: SourceImports } = {}; + + return { + ...(prefer === 'type-imports' + ? { + // prefer type imports + ImportDeclaration(node: TSESTree.ImportDeclaration): void { + const source = node.source.value as string; + const sourceImports = + sourceImportsMap[source] ?? + (sourceImportsMap[source] = { + source, + reportValueImports: [], + typeOnlyNamedImport: null, + }); + if (node.importKind === 'type') { + if ( + !sourceImports.typeOnlyNamedImport && + node.specifiers.every( + specifier => + specifier.type === AST_NODE_TYPES.ImportSpecifier, + ) + ) { + sourceImports.typeOnlyNamedImport = node; + } + return; + } + // if importKind === 'value' + const typeSpecifiers: TSESTree.ImportClause[] = []; + const valueSpecifiers: TSESTree.ImportClause[] = []; + const unusedSpecifiers: TSESTree.ImportClause[] = []; + for (const specifier of node.specifiers) { + const [variable] = context.getDeclaredVariables(specifier); + if (variable.references.length === 0) { + unusedSpecifiers.push(specifier); + } else { + const onlyHasTypeReferences = variable.references.every( + ref => { + if (ref.isValueReference) { + // `type T = typeof foo` will create a value reference because "foo" must be a value type + // however this value reference is safe to use with type-only imports + let parent = ref.identifier.parent; + while (parent) { + if (parent.type === AST_NODE_TYPES.TSTypeQuery) { + return true; + } + // TSTypeQuery must have a TSESTree.EntityName as its child, so we can filter here and break early + if (parent.type !== AST_NODE_TYPES.TSQualifiedName) { + break; + } + parent = parent.parent; + } + return false; + } + + return ref.isTypeReference; + }, + ); + if (onlyHasTypeReferences) { + typeSpecifiers.push(specifier); + } else { + valueSpecifiers.push(specifier); + } + } + } + + if (typeSpecifiers.length) { + sourceImports.reportValueImports.push({ + node, + typeSpecifiers, + valueSpecifiers, + unusedSpecifiers, + }); + } + }, + 'Program:exit'(): void { + for (const sourceImports of Object.values(sourceImportsMap)) { + if (sourceImports.reportValueImports.length === 0) { + continue; + } + for (const report of sourceImports.reportValueImports) { + if ( + report.valueSpecifiers.length === 0 && + report.unusedSpecifiers.length === 0 + ) { + // import is all type-only, convert the entire import to `import type` + context.report({ + node: report.node, + messageId: 'typeOverValue', + *fix(fixer) { + yield* fixToTypeImport(fixer, report, sourceImports); + }, + }); + } else { + // we have a mixed type/value import, so we need to split them out into multiple exports + const typeImportNames: string[] = report.typeSpecifiers.map( + specifier => `"${specifier.local.name}"`, + ); + context.report({ + node: report.node, + messageId: + typeImportNames.length === 1 + ? 'aImportIsOnlyTypes' + : 'someImportsAreOnlyTypes', + data: { + typeImports: + typeImportNames.length === 1 + ? typeImportNames[0] + : [ + typeImportNames.slice(0, -1).join(', '), + typeImportNames.slice(-1)[0], + ].join(' and '), + }, + *fix(fixer) { + yield* fixToTypeImport(fixer, report, sourceImports); + }, + }); + } + } + } + }, + } + : { + // prefer no type imports + 'ImportDeclaration[importKind = "type"]'( + node: TSESTree.ImportDeclaration, + ): void { + context.report({ + node, + messageId: 'valueOverType', + fix(fixer) { + return fixToValueImport(fixer, node); + }, + }); + }, + }), + ...(disallowTypeAnnotations + ? { + // disallow `import()` type + TSImportType(node: TSESTree.TSImportType): void { + context.report({ + node, + messageId: 'noImportTypeAnnotations', + }); + }, + } + : {}), + }; + + function* fixToTypeImport( + fixer: TSESLint.RuleFixer, + report: ReportValueImport, + sourceImports: SourceImports, + ): IterableIterator { + const { node } = report; + + const defaultSpecifier: TSESTree.ImportDefaultSpecifier | null = + node.specifiers[0].type === AST_NODE_TYPES.ImportDefaultSpecifier + ? node.specifiers[0] + : null; + const namespaceSpecifier: TSESTree.ImportNamespaceSpecifier | null = + node.specifiers[0].type === AST_NODE_TYPES.ImportNamespaceSpecifier + ? node.specifiers[0] + : null; + const namedSpecifiers: TSESTree.ImportSpecifier[] = node.specifiers.filter( + (specifier): specifier is TSESTree.ImportSpecifier => + specifier.type === AST_NODE_TYPES.ImportSpecifier, + ); + + if (namespaceSpecifier) { + // e.g. + // import * as types from 'foo' + yield* fixToTypeImportByInsertType(fixer, node, false); + return; + } else if (defaultSpecifier) { + if ( + report.typeSpecifiers.includes(defaultSpecifier) && + namedSpecifiers.length === 0 + ) { + // e.g. + // import Type from 'foo' + yield* fixToTypeImportByInsertType(fixer, node, true); + return; + } + } else { + if ( + namedSpecifiers.every(specifier => + report.typeSpecifiers.includes(specifier), + ) + ) { + // e.g. + // import {Type1, Type2} from 'foo' + yield* fixToTypeImportByInsertType(fixer, node, false); + return; + } + } + + const typeNamedSpecifiers = namedSpecifiers.filter(specifier => + report.typeSpecifiers.includes(specifier), + ); + + const fixesNamedSpecifiers = getFixesNamedSpecifiers( + typeNamedSpecifiers, + namedSpecifiers, + ); + const afterFixes: TSESLint.RuleFix[] = []; + if (typeNamedSpecifiers.length) { + if (sourceImports.typeOnlyNamedImport) { + const closingBraceToken = util.nullThrows( + sourceCode.getFirstTokenBetween( + sourceCode.getFirstToken(sourceImports.typeOnlyNamedImport)!, + sourceImports.typeOnlyNamedImport.source, + util.isClosingBraceToken, + ), + util.NullThrowsReasons.MissingToken( + '}', + sourceImports.typeOnlyNamedImport.type, + ), + ); + let insertText = fixesNamedSpecifiers.typeNamedSpecifiersText; + const before = sourceCode.getTokenBefore(closingBraceToken)!; + if (!util.isCommaToken(before) && !util.isOpeningBraceToken(before)) { + insertText = ',' + insertText; + } + // import type { Already, Type1, Type2 } from 'foo' + // ^^^^^^^^^^^^^ insert + const insertTypeNamedSpecifiers = fixer.insertTextBefore( + closingBraceToken, + insertText, + ); + if (sourceImports.typeOnlyNamedImport.range[1] <= node.range[0]) { + yield insertTypeNamedSpecifiers; + } else { + afterFixes.push(insertTypeNamedSpecifiers); + } + } else { + yield fixer.insertTextBefore( + node, + `import type {${ + fixesNamedSpecifiers.typeNamedSpecifiersText + }} from ${sourceCode.getText(node.source)};\n`, + ); + } + } + + if ( + defaultSpecifier && + report.typeSpecifiers.includes(defaultSpecifier) + ) { + if (typeNamedSpecifiers.length === namedSpecifiers.length) { + const importToken = util.nullThrows( + sourceCode.getFirstToken(node, isImportToken), + util.NullThrowsReasons.MissingToken('import', node.type), + ); + // import type Type from 'foo' + // ^^^^ insert + yield fixer.insertTextAfter(importToken, ' type'); + } else { + yield fixer.insertTextBefore( + node, + `import type ${sourceCode.getText( + defaultSpecifier, + )} from ${sourceCode.getText(node.source)};\n`, + ); + // import Type , {...} from 'foo' + // ^^^^^^ remove + yield fixer.remove(defaultSpecifier); + yield fixer.remove(sourceCode.getTokenAfter(defaultSpecifier)!); + } + } + + yield* fixesNamedSpecifiers.removeTypeNamedSpecifiers; + + yield* afterFixes; + + /** + * Returns information for fixing named specifiers. + */ + function getFixesNamedSpecifiers( + typeNamedSpecifiers: TSESTree.ImportSpecifier[], + allNamedSpecifiers: TSESTree.ImportSpecifier[], + ): { + typeNamedSpecifiersText: string; + removeTypeNamedSpecifiers: TSESLint.RuleFix[]; + } { + 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( + sourceCode.getTokenBefore( + typeNamedSpecifiers[0], + util.isOpeningBraceToken, + ), + util.NullThrowsReasons.MissingToken('{', node.type), + ); + const commaToken = util.nullThrows( + sourceCode.getTokenBefore(openingBraceToken, util.isCommaToken), + util.NullThrowsReasons.MissingToken(',', node.type), + ); + const closingBraceToken = util.nullThrows( + sourceCode.getFirstTokenBetween( + openingBraceToken, + node.source, + util.isClosingBraceToken, + ), + util.NullThrowsReasons.MissingToken('}', node.type), + ); + + // import DefType, {...} from 'foo' + // ^^^^^^^ remove + removeTypeNamedSpecifiers.push( + fixer.removeRange([ + commaToken.range[0], + closingBraceToken.range[1], + ]), + ); + + typeNamedSpecifiersTexts.push( + sourceCode.text.slice( + openingBraceToken.range[1], + closingBraceToken.range[0], + ), + ); + } else { + const typeNamedSpecifierGroups: TSESTree.ImportSpecifier[][] = []; + let group: TSESTree.ImportSpecifier[] = []; + for (const namedSpecifier of allNamedSpecifiers) { + if (typeNamedSpecifiers.includes(namedSpecifier)) { + group.push(namedSpecifier); + } else if (group.length) { + typeNamedSpecifierGroups.push(group); + group = []; + } + } + if (group.length) { + typeNamedSpecifierGroups.push(group); + } + for (const namedSpecifiers of typeNamedSpecifierGroups) { + const { removeRange, textRange } = getNamedSpecifierRanges( + namedSpecifiers, + allNamedSpecifiers, + ); + removeTypeNamedSpecifiers.push(fixer.removeRange(removeRange)); + + typeNamedSpecifiersTexts.push(sourceCode.text.slice(...textRange)); + } + } + return { + typeNamedSpecifiersText: typeNamedSpecifiersTexts.join(','), + removeTypeNamedSpecifiers, + }; + } + + /** + * Returns ranges for fixing named specifier. + */ + function getNamedSpecifierRanges( + namedSpecifierGroup: TSESTree.ImportSpecifier[], + allNamedSpecifiers: TSESTree.ImportSpecifier[], + ): { + textRange: TSESTree.Range; + removeRange: TSESTree.Range; + } { + const first = namedSpecifierGroup[0]; + const last = namedSpecifierGroup[namedSpecifierGroup.length - 1]; + const removeRange: TSESTree.Range = [first.range[0], last.range[1]]; + const textRange: TSESTree.Range = [...removeRange]; + const before = sourceCode.getTokenBefore(first)!; + textRange[0] = before.range[1]; + if (util.isCommaToken(before)) { + removeRange[0] = before.range[0]; + } else { + removeRange[0] = before.range[1]; + } + + const isFirst = allNamedSpecifiers[0] === first; + const isLast = + allNamedSpecifiers[allNamedSpecifiers.length - 1] === last; + const after = sourceCode.getTokenAfter(last)!; + textRange[1] = after.range[0]; + if (isFirst || isLast) { + if (util.isCommaToken(after)) { + removeRange[1] = after.range[1]; + } + } + + return { + textRange, + removeRange, + }; + } + } + + function* fixToTypeImportByInsertType( + fixer: TSESLint.RuleFixer, + node: TSESTree.ImportDeclaration, + isDefaultImport: boolean, + ): IterableIterator { + // import type Foo from 'foo' + // ^^^^^ insert + const importToken = util.nullThrows( + sourceCode.getFirstToken(node, isImportToken), + util.NullThrowsReasons.MissingToken('import', node.type), + ); + yield fixer.insertTextAfter(importToken, ' type'); + + if (isDefaultImport) { + // Has default import + const openingBraceToken = sourceCode.getFirstTokenBetween( + importToken, + node.source, + util.isOpeningBraceToken, + ); + if (openingBraceToken) { + // Only braces. e.g. import Foo, {} from 'foo' + const commaToken = util.nullThrows( + sourceCode.getTokenBefore(openingBraceToken, util.isCommaToken), + util.NullThrowsReasons.MissingToken(',', node.type), + ); + const closingBraceToken = util.nullThrows( + sourceCode.getFirstTokenBetween( + openingBraceToken, + node.source, + util.isClosingBraceToken, + ), + util.NullThrowsReasons.MissingToken('}', node.type), + ); + + // import type Foo, {} from 'foo' + // ^^ remove + yield fixer.removeRange([ + commaToken.range[0], + closingBraceToken.range[1], + ]); + const specifiersText = sourceCode.text.slice( + commaToken.range[1], + closingBraceToken.range[1], + ); + if (node.specifiers.length > 1) { + // import type Foo from 'foo' + // import type {...} from 'foo' // <- insert + yield fixer.insertTextAfter( + node, + `\nimport type${specifiersText} from ${sourceCode.getText( + node.source, + )};`, + ); + } + } + } + } + + function fixToValueImport( + fixer: TSESLint.RuleFixer, + node: TSESTree.ImportDeclaration, + ): TSESLint.RuleFix { + // import type Foo from 'foo' + // ^^^^ remove + const importToken = util.nullThrows( + sourceCode.getFirstToken(node, isImportToken), + util.NullThrowsReasons.MissingToken('import', node.type), + ); + const typeToken = util.nullThrows( + sourceCode.getFirstTokenBetween( + importToken, + node.specifiers[0]?.local ?? node.source, + isTypeToken, + ), + util.NullThrowsReasons.MissingToken('type', node.type), + ); + return fixer.remove(typeToken); + } + }, +}); diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 6386c317e3d..f909be42d4e 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -10,6 +10,7 @@ import commaSpacing from './comma-spacing'; import confusingNonNullAssertionLikeNotEqual from './no-confusing-non-null-assertion'; import consistentTypeAssertions from './consistent-type-assertions'; import consistentTypeDefinitions from './consistent-type-definitions'; +import consistentTypeImports from './consistent-type-imports'; import defaultParamLast from './default-param-last'; import dotNotation from './dot-notation'; import explicitFunctionReturnType from './explicit-function-return-type'; @@ -116,6 +117,7 @@ export default { 'no-confusing-non-null-assertion': confusingNonNullAssertionLikeNotEqual, 'consistent-type-assertions': consistentTypeAssertions, 'consistent-type-definitions': consistentTypeDefinitions, + 'consistent-type-imports': consistentTypeImports, 'default-param-last': defaultParamLast, 'dot-notation': dotNotation, 'explicit-function-return-type': explicitFunctionReturnType, diff --git a/packages/eslint-plugin/tests/rules/consistent-type-imports.test.ts b/packages/eslint-plugin/tests/rules/consistent-type-imports.test.ts new file mode 100644 index 00000000000..13112793f62 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/consistent-type-imports.test.ts @@ -0,0 +1,1044 @@ +import rule from '../../src/rules/consistent-type-imports'; +import { RuleTester, noFormat } from '../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module', + }, +}); + +ruleTester.run('consistent-type-imports', rule, { + valid: [ + ` + import Foo from 'foo'; + const foo: Foo = new Foo(); + `, + ` + import foo from 'foo'; + const foo: foo.Foo = foo.fn(); + `, + ` + import { A, B } from 'foo'; + const foo: A = B(); + const bar = new A(); + `, + ` + import Foo from 'foo'; + `, + ` + import Foo from 'foo'; + type T = Foo; // shadowing + `, + ` + import Foo from 'foo'; + function fn() { + type Foo = {}; // shadowing + let foo: Foo; + } + `, + ` + import { A, B } from 'foo'; + const b = B; + `, + ` + import { A, B, C as c } from 'foo'; + const d = c; + `, + ` + import {} from 'foo'; // empty + `, + { + code: ` + let foo: import('foo'); + let bar: import('foo').Bar; + `, + options: [{ disallowTypeAnnotations: false }], + }, + { + code: ` + import Foo from 'foo'; + let foo: Foo; + `, + options: [{ prefer: 'no-type-imports' }], + }, + // type queries + ` + import type Type from 'foo'; + + type T = typeof Type; + type T = typeof Type.foo; + `, + ` + import type { Type } from 'foo'; + + type T = typeof Type; + type T = typeof Type.foo; + `, + ` + import type * as Type from 'foo'; + + type T = typeof Type; + type T = typeof Type.foo; + `, + { + code: ` + import Type from 'foo'; + + type T = typeof Type; + type T = typeof Type.foo; + `, + options: [{ prefer: 'no-type-imports' }], + }, + { + code: ` + import { Type } from 'foo'; + + type T = typeof Type; + type T = typeof Type.foo; + `, + options: [{ prefer: 'no-type-imports' }], + }, + { + code: ` + import * as Type from 'foo'; + + type T = typeof Type; + type T = typeof Type.foo; + `, + options: [{ prefer: 'no-type-imports' }], + }, + // exports + ` + import Type from 'foo'; + + export { Type }; // is a value export + export default Type; // is a value export + `, + ` + import type Type from 'foo'; + + export { Type }; // is a type-only export + export default Type; // is a type-only export + export type { Type }; // is a type-only export + `, + ` + import { Type } from 'foo'; + + export { Type }; // is a value export + export default Type; // is a value export + `, + ` + import type { Type } from 'foo'; + + export { Type }; // is a type-only export + export default Type; // is a type-only export + export type { Type }; // is a type-only export + `, + ` + import * as Type from 'foo'; + + export { Type }; // is a value export + export default Type; // is a value export + `, + ` + import type * as Type from 'foo'; + + export { Type }; // is a type-only export + export default Type; // is a type-only export + export type { Type }; // is a type-only export + `, + + { + code: ` + import Type from 'foo'; + + export { Type }; // is a type-only export + export default Type; // is a type-only export + export type { Type }; // is a type-only export + `, + options: [{ prefer: 'no-type-imports' }], + }, + { + code: ` + import { Type } from 'foo'; + + export { Type }; // is a type-only export + export default Type; // is a type-only export + export type { Type }; // is a type-only export + `, + options: [{ prefer: 'no-type-imports' }], + }, + { + code: ` + import * as Type from 'foo'; + + export { Type }; // is a type-only export + export default Type; // is a type-only export + export type { Type }; // is a type-only export + `, + options: [{ prefer: 'no-type-imports' }], + }, + ], + invalid: [ + { + code: ` + import Foo from 'foo'; + let foo: Foo; + type Bar = Foo; + interface Baz { + foo: Foo; + } + function fn(a: Foo): Foo {} + `, + output: ` + import type Foo from 'foo'; + let foo: Foo; + type Bar = Foo; + interface Baz { + foo: Foo; + } + function fn(a: Foo): Foo {} + `, + errors: [ + { + messageId: 'typeOverValue', + line: 2, + column: 9, + }, + ], + }, + { + code: ` + import Foo from 'foo'; + let foo: Foo; + `, + output: ` + import type Foo from 'foo'; + let foo: Foo; + `, + options: [{ prefer: 'type-imports' }], + errors: [ + { + messageId: 'typeOverValue', + line: 2, + column: 9, + }, + ], + }, + { + code: ` + import { A, B } from 'foo'; + let foo: A; + let bar: B; + `, + output: ` + import type { A, B } from 'foo'; + let foo: A; + let bar: B; + `, + errors: [ + { + messageId: 'typeOverValue', + line: 2, + column: 9, + }, + ], + }, + { + code: ` + import { A as a, B as b } from 'foo'; + let foo: a; + let bar: b; + `, + output: ` + import type { A as a, B as b } from 'foo'; + let foo: a; + let bar: b; + `, + errors: [ + { + messageId: 'typeOverValue', + line: 2, + column: 9, + }, + ], + }, + { + code: ` + import Foo from 'foo'; + type Bar = typeof Foo; // TSTypeQuery + `, + output: ` + import type Foo from 'foo'; + type Bar = typeof Foo; // TSTypeQuery + `, + errors: [ + { + messageId: 'typeOverValue', + line: 2, + column: 9, + }, + ], + }, + { + code: ` + import foo from 'foo'; + type Bar = foo.Bar; // TSQualifiedName + `, + output: ` + import type foo from 'foo'; + type Bar = foo.Bar; // TSQualifiedName + `, + errors: [ + { + messageId: 'typeOverValue', + line: 2, + column: 9, + }, + ], + }, + { + code: ` + import foo from 'foo'; + type Baz = typeof foo.bar['Baz']; // TSQualifiedName & TSTypeQuery + `, + output: ` + import type foo from 'foo'; + type Baz = typeof foo.bar['Baz']; // TSQualifiedName & TSTypeQuery + `, + errors: [ + { + messageId: 'typeOverValue', + line: 2, + column: 9, + }, + ], + }, + { + code: noFormat` + import * as A from 'foo'; + let foo: A.Foo; + `, + output: noFormat` + import type * as A from 'foo'; + let foo: A.Foo; + `, + errors: [ + { + messageId: 'typeOverValue', + line: 2, + column: 9, + }, + ], + }, + { + // default and named + code: ` + import A, { B } from 'foo'; + let foo: A; + let bar: B; + `, + // eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting + output: noFormat` + import type { B } from 'foo'; +import type A from 'foo'; + let foo: A; + let bar: B; + `, + errors: [ + { + messageId: 'typeOverValue', + line: 2, + column: 9, + }, + ], + }, + { + code: noFormat` + import A, {} from 'foo'; + let foo: A; + `, + output: noFormat` + import type A from 'foo'; + let foo: A; + `, + errors: [ + { + messageId: 'typeOverValue', + line: 2, + column: 9, + }, + ], + }, + { + code: ` + import { A, B } from 'foo'; + const foo: A = B(); + `, + // eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting + output: noFormat` + import type { A} from 'foo'; +import { B } from 'foo'; + const foo: A = B(); + `, + errors: [ + { + messageId: 'aImportIsOnlyTypes', + data: { typeImports: '"A"' }, + line: 2, + column: 9, + }, + ], + }, + { + code: ` + import { A, B, C } from 'foo'; + const foo: A = B(); + let bar: C; + `, + // eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting + output: noFormat` + import type { A, C } from 'foo'; +import { B } from 'foo'; + const foo: A = B(); + let bar: C; + `, + errors: [ + { + messageId: 'someImportsAreOnlyTypes', + data: { typeImports: '"A" and "C"' }, + line: 2, + column: 9, + }, + ], + }, + { + code: ` + import { A, B, C, D } from 'foo'; + const foo: A = B(); + type T = { bar: C; baz: D }; + `, + // eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting + output: noFormat` + import type { A, C, D } from 'foo'; +import { B } from 'foo'; + const foo: A = B(); + type T = { bar: C; baz: D }; + `, + errors: [ + { + messageId: 'someImportsAreOnlyTypes', + data: { typeImports: '"A", "C" and "D"' }, + line: 2, + column: 9, + }, + ], + }, + { + code: ` + import A, { B, C, D } from 'foo'; + B(); + type T = { foo: A; bar: C; baz: D }; + `, + // eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting + output: noFormat` + import type { C, D } from 'foo'; +import type A from 'foo'; +import { B } from 'foo'; + B(); + type T = { foo: A; bar: C; baz: D }; + `, + errors: [ + { + messageId: 'someImportsAreOnlyTypes', + data: { typeImports: '"A", "C" and "D"' }, + line: 2, + column: 9, + }, + ], + }, + { + code: ` + import A, { B } from 'foo'; + B(); + type T = A; + `, + // eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting + output: noFormat` + import type A from 'foo'; +import { B } from 'foo'; + B(); + type T = A; + `, + errors: [ + { + messageId: 'aImportIsOnlyTypes', + data: { typeImports: '"A"' }, + line: 2, + column: 9, + }, + ], + }, + { + code: ` + import type Already1Def from 'foo'; + import type { Already1 } from 'foo'; + import A, { B } from 'foo'; + import { C, D, E } from 'bar'; + import type { Already2 } from 'bar'; + type T = { b: B; c: C; d: D }; + `, + output: noFormat` + import type Already1Def from 'foo'; + import type { Already1 , B } from 'foo'; + import A from 'foo'; + import { E } from 'bar'; + import type { Already2 , C, D} from 'bar'; + type T = { b: B; c: C; d: D }; + `, + errors: [ + { + messageId: 'aImportIsOnlyTypes', + data: { typeImports: '"B"' }, + line: 4, + column: 9, + }, + { + messageId: 'someImportsAreOnlyTypes', + data: { typeImports: '"C" and "D"' }, + line: 5, + column: 9, + }, + ], + }, + { + code: ` + import A, { /* comment */ B } from 'foo'; + type T = B; + `, + // eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting + output: noFormat` + import type { /* comment */ B } from 'foo'; +import A from 'foo'; + type T = B; + `, + errors: [ + { + messageId: 'aImportIsOnlyTypes', + data: { typeImports: '"B"' }, + line: 2, + column: 9, + }, + ], + }, + { + code: noFormat` + import { A, B, C } from 'foo'; + import { D, E, F, } from 'bar'; + type T = A | D; + `, + // eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting + output: noFormat` + import type { A} from 'foo'; +import { B, C } from 'foo'; + import type { D} from 'bar'; +import { E, F, } from 'bar'; + type T = A | D; + `, + errors: [ + { + messageId: 'aImportIsOnlyTypes', + data: { typeImports: '"A"' }, + line: 2, + column: 9, + }, + { + messageId: 'aImportIsOnlyTypes', + data: { typeImports: '"D"' }, + line: 3, + column: 9, + }, + ], + }, + { + code: noFormat` + import { A, B, C } from 'foo'; + import { D, E, F, } from 'bar'; + type T = B | E; + `, + // eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting + output: noFormat` + import type { B} from 'foo'; +import { A, C } from 'foo'; + import type { E} from 'bar'; +import { D, F, } from 'bar'; + type T = B | E; + `, + errors: [ + { + messageId: 'aImportIsOnlyTypes', + data: { typeImports: '"B"' }, + line: 2, + column: 9, + }, + { + messageId: 'aImportIsOnlyTypes', + data: { typeImports: '"E"' }, + line: 3, + column: 9, + }, + ], + }, + { + code: noFormat` + import { A, B, C } from 'foo'; + import { D, E, F, } from 'bar'; + type T = C | F; + `, + // eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting + output: noFormat` + import type { C } from 'foo'; +import { A, B } from 'foo'; + import type { F} from 'bar'; +import { D, E } from 'bar'; + type T = C | F; + `, + errors: [ + { + messageId: 'aImportIsOnlyTypes', + data: { typeImports: '"C"' }, + line: 2, + column: 9, + }, + { + messageId: 'aImportIsOnlyTypes', + data: { typeImports: '"F"' }, + line: 3, + column: 9, + }, + ], + }, + { + // all type fix cases + code: ` + import { Type1, Type2 } from 'named_types'; + import Type from 'default_type'; + import * as Types from 'namespace_type'; + import Default, { Named } from 'default_and_named_type'; + type T = Type1 | Type2 | Type | Types.A | Default | Named; + `, + // eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting + output: noFormat` + import type { Type1, Type2 } from 'named_types'; + import type Type from 'default_type'; + import type * as Types from 'namespace_type'; + import type { Named } from 'default_and_named_type'; +import type Default from 'default_and_named_type'; + type T = Type1 | Type2 | Type | Types.A | Default | Named; + `, + errors: [ + { + messageId: 'typeOverValue', + line: 2, + column: 9, + }, + { + messageId: 'typeOverValue', + line: 3, + column: 9, + }, + { + messageId: 'typeOverValue', + line: 4, + column: 9, + }, + { + messageId: 'typeOverValue', + line: 5, + column: 9, + }, + ], + }, + { + // some type fix cases + code: ` + import { Value1, Type1 } from 'named_import'; + import Type2, { Value2 } from 'default_import'; + import Value3, { Type3 } from 'default_import2'; + import Type4, { Type5, Value4 } from 'default_and_named_import'; + type T = Type1 | Type2 | Type3 | Type4 | Type5; + `, + // eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting + output: noFormat` + import type { Type1 } from 'named_import'; +import { Value1 } from 'named_import'; + import type Type2 from 'default_import'; +import { Value2 } from 'default_import'; + import type { Type3 } from 'default_import2'; +import Value3 from 'default_import2'; + import type { Type5} from 'default_and_named_import'; +import type Type4 from 'default_and_named_import'; +import { Value4 } from 'default_and_named_import'; + type T = Type1 | Type2 | Type3 | Type4 | Type5; + `, + errors: [ + { + messageId: 'aImportIsOnlyTypes', + data: { typeImports: '"Type1"' }, + line: 2, + column: 9, + }, + { + messageId: 'aImportIsOnlyTypes', + data: { typeImports: '"Type2"' }, + line: 3, + column: 9, + }, + { + messageId: 'aImportIsOnlyTypes', + data: { typeImports: '"Type3"' }, + line: 4, + column: 9, + }, + { + messageId: 'someImportsAreOnlyTypes', + data: { typeImports: '"Type4" and "Type5"' }, + line: 5, + column: 9, + }, + ], + }, + // type annotations + { + code: ` + let foo: import('foo'); + let bar: import('foo').Bar; + `, + output: null, + errors: [ + { + messageId: 'noImportTypeAnnotations', + line: 2, + column: 18, + }, + { + messageId: 'noImportTypeAnnotations', + line: 3, + column: 18, + }, + ], + }, + { + code: ` + let foo: import('foo'); + `, + output: null, + options: [{ prefer: 'type-imports' }], + errors: [ + { + messageId: 'noImportTypeAnnotations', + line: 2, + column: 18, + }, + ], + }, + { + code: ` + import type Foo from 'foo'; + let foo: Foo; + `, + options: [{ prefer: 'no-type-imports' }], + output: noFormat` + import Foo from 'foo'; + let foo: Foo; + `, + errors: [ + { + messageId: 'valueOverType', + line: 2, + column: 9, + }, + ], + }, + { + code: ` + import type { Foo } from 'foo'; + let foo: Foo; + `, + options: [{ prefer: 'no-type-imports' }], + output: noFormat` + import { Foo } from 'foo'; + let foo: Foo; + `, + errors: [ + { + messageId: 'valueOverType', + line: 2, + column: 9, + }, + ], + }, + // type queries + { + code: ` + import Type from 'foo'; + + type T = typeof Type; + type T = typeof Type.foo; + `, + output: ` + import type Type from 'foo'; + + type T = typeof Type; + type T = typeof Type.foo; + `, + errors: [ + { + messageId: 'typeOverValue', + line: 2, + column: 9, + }, + ], + }, + { + code: ` + import { Type } from 'foo'; + + type T = typeof Type; + type T = typeof Type.foo; + `, + output: ` + import type { Type } from 'foo'; + + type T = typeof Type; + type T = typeof Type.foo; + `, + errors: [ + { + messageId: 'typeOverValue', + line: 2, + column: 9, + }, + ], + }, + { + code: ` + import * as Type from 'foo'; + + type T = typeof Type; + type T = typeof Type.foo; + `, + output: ` + import type * as Type from 'foo'; + + type T = typeof Type; + type T = typeof Type.foo; + `, + errors: [ + { + messageId: 'typeOverValue', + line: 2, + column: 9, + }, + ], + }, + { + code: ` + import type Type from 'foo'; + + type T = typeof Type; + type T = typeof Type.foo; + `, + options: [{ prefer: 'no-type-imports' }], + output: noFormat` + import Type from 'foo'; + + type T = typeof Type; + type T = typeof Type.foo; + `, + errors: [ + { + messageId: 'valueOverType', + line: 2, + column: 9, + }, + ], + }, + { + code: ` + import type { Type } from 'foo'; + + type T = typeof Type; + type T = typeof Type.foo; + `, + options: [{ prefer: 'no-type-imports' }], + output: noFormat` + import { Type } from 'foo'; + + type T = typeof Type; + type T = typeof Type.foo; + `, + errors: [ + { + messageId: 'valueOverType', + line: 2, + column: 9, + }, + ], + }, + { + code: ` + import type * as Type from 'foo'; + + type T = typeof Type; + type T = typeof Type.foo; + `, + options: [{ prefer: 'no-type-imports' }], + output: noFormat` + import * as Type from 'foo'; + + type T = typeof Type; + type T = typeof Type.foo; + `, + errors: [ + { + messageId: 'valueOverType', + line: 2, + column: 9, + }, + ], + }, + // exports + { + code: ` + import Type from 'foo'; + + export type { Type }; // is a type-only export + `, + output: ` + import type Type from 'foo'; + + export type { Type }; // is a type-only export + `, + errors: [ + { + messageId: 'typeOverValue', + line: 2, + column: 9, + }, + ], + }, + { + code: ` + import { Type } from 'foo'; + + export type { Type }; // is a type-only export + `, + output: ` + import type { Type } from 'foo'; + + export type { Type }; // is a type-only export + `, + errors: [ + { + messageId: 'typeOverValue', + line: 2, + column: 9, + }, + ], + }, + { + code: ` + import * as Type from 'foo'; + + export type { Type }; // is a type-only export + `, + output: ` + import type * as Type from 'foo'; + + export type { Type }; // is a type-only export + `, + errors: [ + { + messageId: 'typeOverValue', + line: 2, + column: 9, + }, + ], + }, + { + code: ` + import type Type from 'foo'; + + export { Type }; // is a type-only export + export default Type; // is a type-only export + export type { Type }; // is a type-only export + `, + options: [{ prefer: 'no-type-imports' }], + output: noFormat` + import Type from 'foo'; + + export { Type }; // is a type-only export + export default Type; // is a type-only export + export type { Type }; // is a type-only export + `, + errors: [ + { + messageId: 'valueOverType', + line: 2, + column: 9, + }, + ], + }, + { + code: ` + import type { Type } from 'foo'; + + export { Type }; // is a type-only export + export default Type; // is a type-only export + export type { Type }; // is a type-only export + `, + options: [{ prefer: 'no-type-imports' }], + output: noFormat` + import { Type } from 'foo'; + + export { Type }; // is a type-only export + export default Type; // is a type-only export + export type { Type }; // is a type-only export + `, + errors: [ + { + messageId: 'valueOverType', + line: 2, + column: 9, + }, + ], + }, + { + code: ` + import type * as Type from 'foo'; + + export { Type }; // is a type-only export + export default Type; // is a type-only export + export type { Type }; // is a type-only export + `, + options: [{ prefer: 'no-type-imports' }], + output: noFormat` + import * as Type from 'foo'; + + export { Type }; // is a type-only export + export default Type; // is a type-only export + export type { Type }; // is a type-only export + `, + errors: [ + { + messageId: 'valueOverType', + line: 2, + column: 9, + }, + ], + }, + ], +});