diff --git a/packages/eslint-plugin/src/rules/keyword-spacing.ts b/packages/eslint-plugin/src/rules/keyword-spacing.ts index 32fb2aaeb71..aa09fb3d1b8 100644 --- a/packages/eslint-plugin/src/rules/keyword-spacing.ts +++ b/packages/eslint-plugin/src/rules/keyword-spacing.ts @@ -1,5 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; -import { AST_TOKEN_TYPES } from '@typescript-eslint/utils'; +import { AST_NODE_TYPES, AST_TOKEN_TYPES } from '@typescript-eslint/utils'; import * as util from '../util'; import { getESLintCoreRule } from '../util/getESLintCoreRule'; @@ -9,6 +9,25 @@ const baseRule = getESLintCoreRule('keyword-spacing'); export type Options = util.InferOptionsTypeFromRule; export type MessageIds = util.InferMessageIdsTypeFromRule; +// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment +const baseSchema = Array.isArray(baseRule.meta.schema) + ? baseRule.meta.schema[0] + : baseRule.meta.schema; +const schema = util.deepMerge( + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument -- https://github.com/microsoft/TypeScript/issues/17002 + baseSchema, + { + properties: { + overrides: { + properties: { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + type: baseSchema.properties.overrides.properties.import, + }, + }, + }, + }, +); + export default util.createRule({ name: 'keyword-spacing', meta: { @@ -20,12 +39,12 @@ export default util.createRule({ }, fixable: 'whitespace', hasSuggestions: baseRule.meta.hasSuggestions, - schema: baseRule.meta.schema, + schema: [schema], messages: baseRule.meta.messages, }, defaultOptions: [{}], - create(context, [{ after }]) { + create(context, [{ after, overrides }]) { const sourceCode = context.getSourceCode(); const baseRules = baseRule.create(context); return { @@ -54,25 +73,37 @@ export default util.createRule({ 'ImportDeclaration[importKind=type]'( node: TSESTree.ImportDeclaration, ): void { + const { type: typeOptionOverride = {} } = overrides ?? {}; const typeToken = sourceCode.getFirstToken(node, { skip: 1 })!; const punctuatorToken = sourceCode.getTokenAfter(typeToken)!; + if ( + node.specifiers?.[0]?.type === AST_NODE_TYPES.ImportDefaultSpecifier + ) { + return; + } const spacesBetweenTypeAndPunctuator = punctuatorToken.range[0] - typeToken.range[1]; - if (after && spacesBetweenTypeAndPunctuator === 0) { + if ( + (typeOptionOverride.after ?? after) === true && + spacesBetweenTypeAndPunctuator === 0 + ) { context.report({ - loc: punctuatorToken.loc, - messageId: 'expectedBefore', - data: { value: punctuatorToken.value }, + loc: typeToken.loc, + messageId: 'expectedAfter', + data: { value: 'type' }, fix(fixer) { - return fixer.insertTextBefore(punctuatorToken, ' '); + return fixer.insertTextAfter(typeToken, ' '); }, }); } - if (!after && spacesBetweenTypeAndPunctuator > 0) { + if ( + (typeOptionOverride.after ?? after) === false && + spacesBetweenTypeAndPunctuator > 0 + ) { context.report({ - loc: punctuatorToken.loc, - messageId: 'unexpectedBefore', - data: { value: punctuatorToken.value }, + loc: typeToken.loc, + messageId: 'unexpectedAfter', + data: { value: 'type' }, fix(fixer) { return fixer.removeRange([ typeToken.range[1], diff --git a/packages/eslint-plugin/tests/rules/keyword-spacing.test.ts b/packages/eslint-plugin/tests/rules/keyword-spacing.test.ts index b8a1a72d2ca..58c740fbd5c 100644 --- a/packages/eslint-plugin/tests/rules/keyword-spacing.test.ts +++ b/packages/eslint-plugin/tests/rules/keyword-spacing.test.ts @@ -116,12 +116,122 @@ ruleTester.run('keyword-spacing', rule, { }, { code: 'import type { foo } from "foo";', - options: [{ overrides: { as: {} } }], + options: [BOTH], parserOptions: { ecmaVersion: 6, sourceType: 'module' }, }, { code: "import type * as Foo from 'foo'", - options: [{ overrides: { as: {} } }], + options: [BOTH], + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + }, + { + code: 'import type { SavedQueries } from "./SavedQueries.js";', + options: [ + { + before: true, + after: false, + overrides: { + else: { after: true }, + return: { after: true }, + try: { after: true }, + catch: { after: false }, + case: { after: true }, + const: { after: true }, + throw: { after: true }, + let: { after: true }, + do: { after: true }, + of: { after: true }, + as: { after: true }, + finally: { after: true }, + from: { after: true }, + import: { after: true }, + export: { after: true }, + default: { after: true }, + // The new option: + type: { after: true }, + }, + }, + ], + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + }, + { + // Space after import is not configurable from option since it's invalid syntax with import type + code: 'import type { SavedQueries } from "./SavedQueries.js";', + options: [ + { + before: true, + after: true, + overrides: { + import: { after: false }, + }, + }, + ], + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + }, + { + code: "import type{SavedQueries} from './SavedQueries.js';", + options: [ + { + before: true, + after: false, + overrides: { + from: { after: true }, + }, + }, + ], + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + }, + { + code: "import type{SavedQueries} from'./SavedQueries.js';", + options: [ + { + before: true, + after: false, + }, + ], + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + }, + { + code: "import type http from 'node:http';", + options: [ + { + before: true, + after: false, + overrides: { + from: { after: true }, + }, + }, + ], + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + }, + { + code: "import type http from'node:http';", + options: [ + { + before: true, + after: false, + }, + ], + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + }, + { + code: 'import type {} from "foo";', + options: [BOTH], + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + }, + { + code: 'import type { foo1, foo2 } from "foo";', + options: [BOTH], + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + }, + { + code: 'import type { foo1 as _foo1, foo2 as _foo2 } from "foo";', + options: [BOTH], + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + }, + { + code: 'import type { foo as bar } from "foo";', + options: [BOTH], parserOptions: { ecmaVersion: 6, sourceType: 'module' }, }, ], @@ -167,28 +277,58 @@ ruleTester.run('keyword-spacing', rule, { output: 'import type { foo } from "foo";', options: [{ after: true, before: true }], parserOptions: { ecmaVersion: 6, sourceType: 'module' }, - errors: [{ messageId: 'expectedBefore', data: { value: '{' } }], + errors: expectedAfter('type'), }, { code: 'import type { foo } from"foo";', output: 'import type{ foo } from"foo";', options: [{ after: false, before: true }], parserOptions: { ecmaVersion: 6, sourceType: 'module' }, - errors: [{ messageId: 'unexpectedBefore', data: { value: '{' } }], + errors: unexpectedAfter('type'), }, { code: 'import type* as foo from "foo";', output: 'import type * as foo from "foo";', options: [{ after: true, before: true }], parserOptions: { ecmaVersion: 6, sourceType: 'module' }, - errors: [{ messageId: 'expectedBefore', data: { value: '*' } }], + errors: expectedAfter('type'), }, { code: 'import type * as foo from"foo";', output: 'import type* as foo from"foo";', options: [{ after: false, before: true }], parserOptions: { ecmaVersion: 6, sourceType: 'module' }, - errors: [{ messageId: 'unexpectedBefore', data: { value: '*' } }], + errors: unexpectedAfter('type'), + }, + { + code: "import type {SavedQueries} from './SavedQueries.js';", + output: "import type{SavedQueries} from './SavedQueries.js';", + options: [ + { + before: true, + after: false, + overrides: { + from: { after: true }, + }, + }, + ], + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: unexpectedAfter('type'), + }, + { + code: "import type {SavedQueries} from './SavedQueries.js';", + output: "import type{SavedQueries} from'./SavedQueries.js';", + options: [ + { + before: true, + after: false, + }, + ], + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [ + { messageId: 'unexpectedAfter', data: { value: 'type' } }, + { messageId: 'unexpectedAfter', data: { value: 'from' } }, + ], }, ], });