diff --git a/docs/rules/consistent-type-specifier-style.md b/docs/rules/consistent-type-specifier-style.md index 41d98e4e1..ce8400fb6 100644 --- a/docs/rules/consistent-type-specifier-style.md +++ b/docs/rules/consistent-type-specifier-style.md @@ -38,7 +38,8 @@ This rule includes a fixer that will automatically convert your specifiers to th The rule accepts a single string option which may be one of: - `'prefer-inline'` - enforces that named type-only specifiers are only ever written with an inline marker; and never as part of a top-level, type-only import. - - `'prefer-top-level'` - enforces that named type-only specifiers only ever written as part of a top-level, type-only import; and never with an inline marker. + - `'prefer-top-level'` - enforces that named type-only specifiers are only ever written as part of a top-level, type-only import; and never with an inline marker. + - `'prefer-top-level-if-only-type-imports'` - enforces that named type-only specifiers must use a top-level, type-only import when all named imports are types; if some are values, then inline markers are allowed. This is useful when you generally prefer inline but you are using TypeScript `verbatimModuleSyntax` and want all-type imports omitted by bundlers. By default the rule will use the `prefer-inline` option. @@ -64,6 +65,27 @@ import type Foo, {Bar} from 'Foo'; import typeof {Foo} from 'Foo'; ``` +### `prefer-top-level-if-only-type-imports` + +❌ Invalid with `["error", "prefer-top-level-if-only-type-imports"]` + +```ts +import {type Foo} from 'Foo'; +import {type Foo,type Bar} from 'Foo'; +// flow only +import {typeof Foo} from 'Foo'; +``` + +✅ Valid with `["error", "prefer-top-level-if-only-type-imports"]` + +```ts +import type {Foo} from 'Foo'; +import { type Foo, someValue } from 'Foo'; +import type Foo, {Bar} from 'Foo'; +// flow only +import typeof {Foo} from 'Foo'; +``` + ### `prefer-inline` ❌ Invalid with `["error", "prefer-inline"]` diff --git a/src/rules/consistent-type-specifier-style.js b/src/rules/consistent-type-specifier-style.js index 84c33ecd8..18c85bb9d 100644 --- a/src/rules/consistent-type-specifier-style.js +++ b/src/rules/consistent-type-specifier-style.js @@ -58,7 +58,11 @@ module.exports = { schema: [ { type: 'string', - enum: ['prefer-inline', 'prefer-top-level'], + enum: [ + 'prefer-inline', + 'prefer-top-level', + 'prefer-top-level-if-only-type-imports', + ], default: 'prefer-inline', }, ], @@ -66,8 +70,9 @@ module.exports = { create(context) { const sourceCode = getSourceCode(context); + const preference = context.options[0]; - if (context.options[0] === 'prefer-inline') { + if (preference === 'prefer-inline') { return { ImportDeclaration(node) { if (node.importKind === 'value' || node.importKind == null) { @@ -108,7 +113,7 @@ module.exports = { }; } - // prefer-top-level + // prefer-top-level or prefer-top-level-if-only-type-imports return { /** @param {import('estree').ImportDeclaration} node */ ImportDeclaration(node) { @@ -164,9 +169,10 @@ module.exports = { typeofSpecifiers.length > 0 ? 'typeof' : [], ); + const messageSuffix = preference === 'prefer-top-level-if-only-type-imports' ? ' when there are only type imports' : ''; context.report({ node, - message: 'Prefer using a top-level {{kind}}-only import instead of inline {{kind}} specifiers.', + message: `Prefer using a top-level {{kind}}-only import instead of inline {{kind}} specifiers${messageSuffix}.`, data: { kind: kind.join('/'), }, @@ -174,7 +180,7 @@ module.exports = { return fixer.replaceText(node, newImports); }, }); - } else { + } else if (preference !== 'prefer-top-level-if-only-type-imports') { // remove specific specifiers and insert new imports for them typeSpecifiers.concat(typeofSpecifiers).forEach((specifier) => { context.report({ diff --git a/tests/src/rules/consistent-type-specifier-style.js b/tests/src/rules/consistent-type-specifier-style.js index 139457ff6..3d0f6e92c 100644 --- a/tests/src/rules/consistent-type-specifier-style.js +++ b/tests/src/rules/consistent-type-specifier-style.js @@ -53,6 +53,70 @@ const COMMON_TESTS = { options: ['prefer-top-level'], }), + // + // prefer-top-level-if-only-type-imports + // + test({ + code: "import Foo from 'Foo';", + options: ['prefer-top-level-if-only-type-imports'], + }), + test({ + code: "import type Foo from 'Foo';", + options: ['prefer-top-level-if-only-type-imports'], + }), + test({ + code: "import { Foo } from 'Foo';", + options: ['prefer-top-level-if-only-type-imports'], + }), + test({ + code: "import { Foo as Bar } from 'Foo';", + options: ['prefer-top-level-if-only-type-imports'], + }), + test({ + code: "import * as Foo from 'Foo';", + options: ['prefer-top-level-if-only-type-imports'], + }), + test({ + code: "import 'Foo';", + options: ['prefer-top-level-if-only-type-imports'], + }), + test({ + code: "import {} from 'Foo';", + options: ['prefer-top-level-if-only-type-imports'], + }), + test({ + code: "import type {} from 'Foo';", + options: ['prefer-top-level-if-only-type-imports'], + }), + test({ + code: "import type { Foo } from 'Foo';", + options: ['prefer-top-level-if-only-type-imports'], + }), + test({ + code: "import type { Foo as Bar } from 'Foo';", + options: ['prefer-top-level-if-only-type-imports'], + }), + test({ + code: "import type { Foo, Bar, Baz, Bam } from 'Foo';", + options: ['prefer-top-level-if-only-type-imports'], + }), + test({ + code: "import { Foo, type Bar } from 'Foo';", + options: ['prefer-top-level-if-only-type-imports'], + }), + test({ + code: "import { type Foo, Bar } from 'Foo';", + options: ['prefer-top-level-if-only-type-imports'], + }), + test({ + code: "import Foo, { type Bar } from 'Foo';", + options: ['prefer-top-level-if-only-type-imports'], + }), + test({ + code: "import Foo, { type Bar, Baz } from 'Foo';", + options: ['prefer-top-level-if-only-type-imports'], + }), + // // prefer-inline // @@ -196,6 +260,37 @@ import { }], }, + // + // prefer-top-level-if-only-type-imports + // + { + code: "import { type Foo } from 'Foo';", + output: "import type {Foo} from 'Foo';", + options: ['prefer-top-level-if-only-type-imports'], + errors: [{ + message: 'Prefer using a top-level type-only import instead of inline type specifiers when there are only type imports.', + type: 'ImportDeclaration', + }], + }, + { + code: "import { type Foo as Bar } from 'Foo';", + output: "import type {Foo as Bar} from 'Foo';", + options: ['prefer-top-level-if-only-type-imports'], + errors: [{ + message: 'Prefer using a top-level type-only import instead of inline type specifiers when there are only type imports.', + type: 'ImportDeclaration', + }], + }, + { + code: "import { type Foo, type Bar } from 'Foo';", + output: "import type {Foo, Bar} from 'Foo';", + options: ['prefer-top-level-if-only-type-imports'], + errors: [{ + message: 'Prefer using a top-level type-only import instead of inline type specifiers when there are only type imports.', + type: 'ImportDeclaration', + }], + }, + // // prefer-inline //