diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 7ed22981926c..0428cff43fc5 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -183,6 +183,7 @@ In these cases, we create what we call an extension rule; a rule within our plug | [`@typescript-eslint/default-param-last`](./docs/rules/default-param-last.md) | Enforce default parameters to be last | | | | | [`@typescript-eslint/func-call-spacing`](./docs/rules/func-call-spacing.md) | Require or disallow spacing between function identifiers and their invocations | | :wrench: | | | [`@typescript-eslint/indent`](./docs/rules/indent.md) | Enforce consistent indentation | | :wrench: | | +| [`@typescript-eslint/keyword-spacing`](./docs/rules/keyword-spacing.md) | Enforce consistent spacing before and after keywords | | :wrench: | | | [`@typescript-eslint/no-array-constructor`](./docs/rules/no-array-constructor.md) | Disallow generic `Array` constructors | :heavy_check_mark: | :wrench: | | | [`@typescript-eslint/no-dupe-class-members`](./docs/rules/no-dupe-class-members.md) | Disallow duplicate class members | | | | | [`@typescript-eslint/no-empty-function`](./docs/rules/no-empty-function.md) | Disallow empty functions | :heavy_check_mark: | | | diff --git a/packages/eslint-plugin/docs/rules/keyword-spacing.md b/packages/eslint-plugin/docs/rules/keyword-spacing.md new file mode 100644 index 000000000000..ca2926d6c825 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/keyword-spacing.md @@ -0,0 +1,22 @@ +# Enforce consistent spacing before and after keywords (`keyword-spacing`) + +## Rule Details + +This rule extends the base [`eslint/keyword-spacing`](https://eslint.org/docs/rules/keyword-spacing) rule. +This version adds support for generic type parameters on function calls. + +## How to use + +```cjson +{ + // note you must disable the base rule as it can report incorrect errors + "keyword-spacing": "off", + "@typescript-eslint/keyword-spacing": ["error"] +} +``` + +## Options + +See [`eslint/keyword-spacing` options](https://eslint.org/docs/rules/keyword-spacing#options). + +Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/keyword-spacing.md) diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json index c736840a3fff..ec65f143c590 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -22,6 +22,8 @@ "@typescript-eslint/func-call-spacing": "error", "indent": "off", "@typescript-eslint/indent": "error", + "keyword-spacing": "off", + "@typescript-eslint/keyword-spacing": "error", "@typescript-eslint/member-delimiter-style": "error", "@typescript-eslint/member-ordering": "error", "@typescript-eslint/method-signature-style": "error", diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 901d31a8b9fb..63c402297e39 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -19,6 +19,7 @@ import funcCallSpacing from './func-call-spacing'; import genericTypeNaming from './generic-type-naming'; import indent from './indent'; import interfaceNamePrefix from './interface-name-prefix'; +import keywordSpacing from './keyword-spacing'; import memberDelimiterStyle from './member-delimiter-style'; import memberNaming from './member-naming'; import memberOrdering from './member-ordering'; @@ -31,10 +32,10 @@ import noDynamicDelete from './no-dynamic-delete'; import noEmptyFunction from './no-empty-function'; import noEmptyInterface from './no-empty-interface'; import noExplicitAny from './no-explicit-any'; +import noExtraneousClass from './no-extraneous-class'; import noExtraNonNullAssertion from './no-extra-non-null-assertion'; import noExtraParens from './no-extra-parens'; import noExtraSemi from './no-extra-semi'; -import noExtraneousClass from './no-extraneous-class'; import noFloatingPromises from './no-floating-promises'; import noForInArray from './no-for-in-array'; import noImpliedEval from './no-implied-eval'; @@ -118,6 +119,7 @@ export default { 'generic-type-naming': genericTypeNaming, indent: indent, 'interface-name-prefix': interfaceNamePrefix, + 'keyword-spacing': keywordSpacing, 'member-delimiter-style': memberDelimiterStyle, 'member-naming': memberNaming, 'member-ordering': memberOrdering, diff --git a/packages/eslint-plugin/src/rules/keyword-spacing.ts b/packages/eslint-plugin/src/rules/keyword-spacing.ts new file mode 100644 index 000000000000..8c4ff1d38ff6 --- /dev/null +++ b/packages/eslint-plugin/src/rules/keyword-spacing.ts @@ -0,0 +1,52 @@ +import { AST_TOKEN_TYPES } from '@typescript-eslint/experimental-utils'; +import baseRule from 'eslint/lib/rules/keyword-spacing'; +import * as util from '../util'; + +export type Options = util.InferOptionsTypeFromRule; +export type MessageIds = util.InferMessageIdsTypeFromRule; + +export default util.createRule({ + name: 'keyword-spacing', + meta: { + type: 'layout', + docs: { + description: 'Enforce consistent spacing before and after keywords', + category: 'Stylistic Issues', + recommended: false, + extendsBaseRule: true, + }, + fixable: 'whitespace', + schema: baseRule.meta.schema, + messages: baseRule.meta.messages, + }, + defaultOptions: [{}], + + create(context) { + const sourceCode = context.getSourceCode(); + const baseRules = baseRule.create(context); + return { + ...baseRules, + TSAsExpression(node): void { + const asToken = util.nullThrows( + sourceCode.getTokenAfter( + node.expression, + token => token.value === 'as', + ), + util.NullThrowsReasons.MissingToken('as', node.type), + ); + const oldTokenType = asToken.type; + // as is a contextual keyword, so it's always reported as an Identifier + // the rule looks for keyword tokens, so we temporarily override it + // we mutate it at the token level because the rule calls sourceCode.getFirstToken, + // so mutating a copy would not change the underlying copy returned by that method + asToken.type = AST_TOKEN_TYPES.Keyword; + + // use this selector just because it is just a call to `checkSpacingAroundFirstToken` + baseRules.DebuggerStatement(asToken as never); + + // make sure to reset the type afterward so we don't permanently mutate the AST + asToken.type = oldTokenType; + }, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/keyword-spacing.test.ts b/packages/eslint-plugin/tests/rules/keyword-spacing.test.ts new file mode 100644 index 000000000000..987caebb1e43 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/keyword-spacing.test.ts @@ -0,0 +1,153 @@ +/* eslint-disable eslint-comments/no-use */ +// this rule tests the spacing, which prettier will want to fix and break the tests +/* eslint "@typescript-eslint/internal/plugin-test-formatting": ["error", { formatWithPrettier: false }] */ +/* eslint-enable eslint-comments/no-use */ +import { TSESLint } from '@typescript-eslint/experimental-utils'; +import rule, { MessageIds, Options } from '../../src/rules/keyword-spacing'; +import { RuleTester } from '../RuleTester'; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const BOTH = { before: true, after: true }; +const NEITHER = { before: false, after: false }; + +/** + * Creates an option object to test an 'overrides' option. + * + * e.g. + * + * override('as', BOTH) + * + * returns + * + * { + * before: false, + * after: false, + * overrides: {as: {before: true, after: true}} + * } + * @param keyword A keyword to be overridden. + * @param value A value to override. + * @returns An option object to test an 'overrides' option. + */ +function overrides(keyword: string, value: Options[0]): Options[0] { + return { + before: value.before === false, + after: value.after === false, + overrides: { [keyword]: value }, + }; +} + +/** + * Gets an error message that expected space(s) before a specified keyword. + * @param keyword A keyword. + * @returns An error message. + */ +function expectedBefore(keyword: string): TSESLint.TestCaseError[] { + return [{ messageId: 'expectedBefore', data: { value: keyword } }]; +} + +/** + * Gets an error message that expected space(s) after a specified keyword. + * @param keyword A keyword. + * @returns An error message. + */ +function expectedAfter(keyword: string): TSESLint.TestCaseError[] { + return [{ messageId: 'expectedAfter', data: { value: keyword } }]; +} + +/** + * Gets an error message that unexpected space(s) before a specified keyword. + * @param keyword A keyword. + * @returns An error message. + */ +function unexpectedBefore( + keyword: string, +): TSESLint.TestCaseError[] { + return [{ messageId: 'unexpectedBefore', data: { value: keyword } }]; +} + +/** + * Gets an error message that unexpected space(s) after a specified keyword. + * @param keyword A keyword. + * @returns An error message. + */ +function unexpectedAfter( + keyword: string, +): TSESLint.TestCaseError[] { + return [{ messageId: 'unexpectedAfter', data: { value: keyword } }]; +} + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('keyword-spacing', rule, { + valid: [ + //---------------------------------------------------------------------- + // as (typing) + //---------------------------------------------------------------------- + { + code: 'const foo = {} as {};', + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + }, + { + code: 'const foo = {}as{};', + options: [NEITHER], + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + }, + { + code: 'const foo = {} as {};', + options: [overrides('as', BOTH)], + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + }, + { + code: 'const foo = {}as{};', + options: [overrides('as', NEITHER)], + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + }, + { + code: 'const foo = {} as {};', + options: [{ overrides: { as: {} } }], + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + }, + ], + invalid: [ + //---------------------------------------------------------------------- + // as (typing) + //---------------------------------------------------------------------- + { + code: 'const foo = {}as {};', + output: 'const foo = {} as {};', + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: expectedBefore('as'), + }, + { + code: 'const foo = {} as{};', + output: 'const foo = {}as{};', + options: [NEITHER], + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: unexpectedBefore('as'), + }, + { + code: 'const foo = {} as{};', + output: 'const foo = {} as {};', + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: expectedAfter('as'), + }, + { + code: 'const foo = {}as {};', + output: 'const foo = {}as{};', + options: [NEITHER], + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: unexpectedAfter('as'), + }, + { + code: 'const foo = {} as{};', + options: [{ overrides: { as: {} } }], + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: expectedAfter('as'), + }, + ], +}); diff --git a/packages/eslint-plugin/typings/eslint-rules.d.ts b/packages/eslint-plugin/typings/eslint-rules.d.ts index ea60d9b31697..ac8700d42e56 100644 --- a/packages/eslint-plugin/typings/eslint-rules.d.ts +++ b/packages/eslint-plugin/typings/eslint-rules.d.ts @@ -142,6 +142,85 @@ declare module 'eslint/lib/rules/indent' { export = rule; } +declare module 'eslint/lib/rules/keyword-spacing' { + import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; + import { RuleFunction } from '@typescript-eslint/experimental-utils/dist/ts-eslint'; + + type Options = [ + { + before?: boolean; + after?: boolean; + overrides?: Record< + string, + { + before?: boolean; + after?: boolean; + } + >; + }, + ]; + type MessageIds = + | 'expectedBefore' + | 'expectedAfter' + | 'unexpectedBefore' + | 'unexpectedAfter'; + + const rule: TSESLint.RuleModule< + MessageIds, + Options, + { + // Statements + DebuggerStatement: RuleFunction; + WithStatement: RuleFunction; + + // Statements - Control flow + BreakStatement: RuleFunction; + ContinueStatement: RuleFunction; + ReturnStatement: RuleFunction; + ThrowStatement: RuleFunction; + TryStatement: RuleFunction; + + // Statements - Choice + IfStatement: RuleFunction; + SwitchStatement: RuleFunction; + SwitchCase: RuleFunction; + + // Statements - Loops + DoWhileStatement: RuleFunction; + ForInStatement: RuleFunction; + ForOfStatement: RuleFunction; + ForStatement: RuleFunction; + WhileStatement: RuleFunction; + + // Statements - Declarations + ClassDeclaration: RuleFunction; + ExportNamedDeclaration: RuleFunction; + ExportDefaultDeclaration: RuleFunction; + ExportAllDeclaration: RuleFunction; + FunctionDeclaration: RuleFunction; + ImportDeclaration: RuleFunction; + VariableDeclaration: RuleFunction; + + // Expressions + ArrowFunctionExpression: RuleFunction; + AwaitExpression: RuleFunction; + ClassExpression: RuleFunction; + FunctionExpression: RuleFunction; + NewExpression: RuleFunction; + Super: RuleFunction; + ThisExpression: RuleFunction; + UnaryExpression: RuleFunction; + YieldExpression: RuleFunction; + + // Others + ImportNamespaceSpecifier: RuleFunction; + MethodDefinition: RuleFunction; + Property: RuleFunction; + } + >; + export = rule; +} + declare module 'eslint/lib/rules/no-dupe-class-members' { import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy