diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md
index 7ed22981926..0428cff43fc 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 00000000000..ca2926d6c82
--- /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 c736840a3ff..ec65f143c59 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 901d31a8b9f..63c402297e3 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 00000000000..8c4ff1d38ff
--- /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 00000000000..987caebb1e4
--- /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 ea60d9b3169..ac8700d42e5 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';