Skip to content

Commit c5106dd

Browse files
authored
feat(eslint-plugin): add extension rule keyword-spacing (typescript-eslint#1739)
1 parent 369978e commit c5106dd

File tree

7 files changed

+312
-1
lines changed

7 files changed

+312
-1
lines changed

packages/eslint-plugin/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ In these cases, we create what we call an extension rule; a rule within our plug
183183
| [`@typescript-eslint/default-param-last`](./docs/rules/default-param-last.md) | Enforce default parameters to be last | | | |
184184
| [`@typescript-eslint/func-call-spacing`](./docs/rules/func-call-spacing.md) | Require or disallow spacing between function identifiers and their invocations | | :wrench: | |
185185
| [`@typescript-eslint/indent`](./docs/rules/indent.md) | Enforce consistent indentation | | :wrench: | |
186+
| [`@typescript-eslint/keyword-spacing`](./docs/rules/keyword-spacing.md) | Enforce consistent spacing before and after keywords | | :wrench: | |
186187
| [`@typescript-eslint/no-array-constructor`](./docs/rules/no-array-constructor.md) | Disallow generic `Array` constructors | :heavy_check_mark: | :wrench: | |
187188
| [`@typescript-eslint/no-dupe-class-members`](./docs/rules/no-dupe-class-members.md) | Disallow duplicate class members | | | |
188189
| [`@typescript-eslint/no-empty-function`](./docs/rules/no-empty-function.md) | Disallow empty functions | :heavy_check_mark: | | |
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Enforce consistent spacing before and after keywords (`keyword-spacing`)
2+
3+
## Rule Details
4+
5+
This rule extends the base [`eslint/keyword-spacing`](https://eslint.org/docs/rules/keyword-spacing) rule.
6+
This version adds support for generic type parameters on function calls.
7+
8+
## How to use
9+
10+
```cjson
11+
{
12+
// note you must disable the base rule as it can report incorrect errors
13+
"keyword-spacing": "off",
14+
"@typescript-eslint/keyword-spacing": ["error"]
15+
}
16+
```
17+
18+
## Options
19+
20+
See [`eslint/keyword-spacing` options](https://eslint.org/docs/rules/keyword-spacing#options).
21+
22+
<sup>Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/keyword-spacing.md)</sup>

packages/eslint-plugin/src/configs/all.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
"@typescript-eslint/func-call-spacing": "error",
2323
"indent": "off",
2424
"@typescript-eslint/indent": "error",
25+
"keyword-spacing": "off",
26+
"@typescript-eslint/keyword-spacing": "error",
2527
"@typescript-eslint/member-delimiter-style": "error",
2628
"@typescript-eslint/member-ordering": "error",
2729
"@typescript-eslint/method-signature-style": "error",

packages/eslint-plugin/src/rules/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import funcCallSpacing from './func-call-spacing';
1919
import genericTypeNaming from './generic-type-naming';
2020
import indent from './indent';
2121
import interfaceNamePrefix from './interface-name-prefix';
22+
import keywordSpacing from './keyword-spacing';
2223
import memberDelimiterStyle from './member-delimiter-style';
2324
import memberNaming from './member-naming';
2425
import memberOrdering from './member-ordering';
@@ -31,10 +32,10 @@ import noDynamicDelete from './no-dynamic-delete';
3132
import noEmptyFunction from './no-empty-function';
3233
import noEmptyInterface from './no-empty-interface';
3334
import noExplicitAny from './no-explicit-any';
35+
import noExtraneousClass from './no-extraneous-class';
3436
import noExtraNonNullAssertion from './no-extra-non-null-assertion';
3537
import noExtraParens from './no-extra-parens';
3638
import noExtraSemi from './no-extra-semi';
37-
import noExtraneousClass from './no-extraneous-class';
3839
import noFloatingPromises from './no-floating-promises';
3940
import noForInArray from './no-for-in-array';
4041
import noImpliedEval from './no-implied-eval';
@@ -118,6 +119,7 @@ export default {
118119
'generic-type-naming': genericTypeNaming,
119120
indent: indent,
120121
'interface-name-prefix': interfaceNamePrefix,
122+
'keyword-spacing': keywordSpacing,
121123
'member-delimiter-style': memberDelimiterStyle,
122124
'member-naming': memberNaming,
123125
'member-ordering': memberOrdering,
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { AST_TOKEN_TYPES } from '@typescript-eslint/experimental-utils';
2+
import baseRule from 'eslint/lib/rules/keyword-spacing';
3+
import * as util from '../util';
4+
5+
export type Options = util.InferOptionsTypeFromRule<typeof baseRule>;
6+
export type MessageIds = util.InferMessageIdsTypeFromRule<typeof baseRule>;
7+
8+
export default util.createRule<Options, MessageIds>({
9+
name: 'keyword-spacing',
10+
meta: {
11+
type: 'layout',
12+
docs: {
13+
description: 'Enforce consistent spacing before and after keywords',
14+
category: 'Stylistic Issues',
15+
recommended: false,
16+
extendsBaseRule: true,
17+
},
18+
fixable: 'whitespace',
19+
schema: baseRule.meta.schema,
20+
messages: baseRule.meta.messages,
21+
},
22+
defaultOptions: [{}],
23+
24+
create(context) {
25+
const sourceCode = context.getSourceCode();
26+
const baseRules = baseRule.create(context);
27+
return {
28+
...baseRules,
29+
TSAsExpression(node): void {
30+
const asToken = util.nullThrows(
31+
sourceCode.getTokenAfter(
32+
node.expression,
33+
token => token.value === 'as',
34+
),
35+
util.NullThrowsReasons.MissingToken('as', node.type),
36+
);
37+
const oldTokenType = asToken.type;
38+
// as is a contextual keyword, so it's always reported as an Identifier
39+
// the rule looks for keyword tokens, so we temporarily override it
40+
// we mutate it at the token level because the rule calls sourceCode.getFirstToken,
41+
// so mutating a copy would not change the underlying copy returned by that method
42+
asToken.type = AST_TOKEN_TYPES.Keyword;
43+
44+
// use this selector just because it is just a call to `checkSpacingAroundFirstToken`
45+
baseRules.DebuggerStatement(asToken as never);
46+
47+
// make sure to reset the type afterward so we don't permanently mutate the AST
48+
asToken.type = oldTokenType;
49+
},
50+
};
51+
},
52+
});
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/* eslint-disable eslint-comments/no-use */
2+
// this rule tests the spacing, which prettier will want to fix and break the tests
3+
/* eslint "@typescript-eslint/internal/plugin-test-formatting": ["error", { formatWithPrettier: false }] */
4+
/* eslint-enable eslint-comments/no-use */
5+
import { TSESLint } from '@typescript-eslint/experimental-utils';
6+
import rule, { MessageIds, Options } from '../../src/rules/keyword-spacing';
7+
import { RuleTester } from '../RuleTester';
8+
9+
//------------------------------------------------------------------------------
10+
// Helpers
11+
//------------------------------------------------------------------------------
12+
13+
const BOTH = { before: true, after: true };
14+
const NEITHER = { before: false, after: false };
15+
16+
/**
17+
* Creates an option object to test an 'overrides' option.
18+
*
19+
* e.g.
20+
*
21+
* override('as', BOTH)
22+
*
23+
* returns
24+
*
25+
* {
26+
* before: false,
27+
* after: false,
28+
* overrides: {as: {before: true, after: true}}
29+
* }
30+
* @param keyword A keyword to be overridden.
31+
* @param value A value to override.
32+
* @returns An option object to test an 'overrides' option.
33+
*/
34+
function overrides(keyword: string, value: Options[0]): Options[0] {
35+
return {
36+
before: value.before === false,
37+
after: value.after === false,
38+
overrides: { [keyword]: value },
39+
};
40+
}
41+
42+
/**
43+
* Gets an error message that expected space(s) before a specified keyword.
44+
* @param keyword A keyword.
45+
* @returns An error message.
46+
*/
47+
function expectedBefore(keyword: string): TSESLint.TestCaseError<MessageIds>[] {
48+
return [{ messageId: 'expectedBefore', data: { value: keyword } }];
49+
}
50+
51+
/**
52+
* Gets an error message that expected space(s) after a specified keyword.
53+
* @param keyword A keyword.
54+
* @returns An error message.
55+
*/
56+
function expectedAfter(keyword: string): TSESLint.TestCaseError<MessageIds>[] {
57+
return [{ messageId: 'expectedAfter', data: { value: keyword } }];
58+
}
59+
60+
/**
61+
* Gets an error message that unexpected space(s) before a specified keyword.
62+
* @param keyword A keyword.
63+
* @returns An error message.
64+
*/
65+
function unexpectedBefore(
66+
keyword: string,
67+
): TSESLint.TestCaseError<MessageIds>[] {
68+
return [{ messageId: 'unexpectedBefore', data: { value: keyword } }];
69+
}
70+
71+
/**
72+
* Gets an error message that unexpected space(s) after a specified keyword.
73+
* @param keyword A keyword.
74+
* @returns An error message.
75+
*/
76+
function unexpectedAfter(
77+
keyword: string,
78+
): TSESLint.TestCaseError<MessageIds>[] {
79+
return [{ messageId: 'unexpectedAfter', data: { value: keyword } }];
80+
}
81+
82+
const ruleTester = new RuleTester({
83+
parser: '@typescript-eslint/parser',
84+
});
85+
86+
ruleTester.run('keyword-spacing', rule, {
87+
valid: [
88+
//----------------------------------------------------------------------
89+
// as (typing)
90+
//----------------------------------------------------------------------
91+
{
92+
code: 'const foo = {} as {};',
93+
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
94+
},
95+
{
96+
code: 'const foo = {}as{};',
97+
options: [NEITHER],
98+
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
99+
},
100+
{
101+
code: 'const foo = {} as {};',
102+
options: [overrides('as', BOTH)],
103+
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
104+
},
105+
{
106+
code: 'const foo = {}as{};',
107+
options: [overrides('as', NEITHER)],
108+
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
109+
},
110+
{
111+
code: 'const foo = {} as {};',
112+
options: [{ overrides: { as: {} } }],
113+
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
114+
},
115+
],
116+
invalid: [
117+
//----------------------------------------------------------------------
118+
// as (typing)
119+
//----------------------------------------------------------------------
120+
{
121+
code: 'const foo = {}as {};',
122+
output: 'const foo = {} as {};',
123+
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
124+
errors: expectedBefore('as'),
125+
},
126+
{
127+
code: 'const foo = {} as{};',
128+
output: 'const foo = {}as{};',
129+
options: [NEITHER],
130+
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
131+
errors: unexpectedBefore('as'),
132+
},
133+
{
134+
code: 'const foo = {} as{};',
135+
output: 'const foo = {} as {};',
136+
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
137+
errors: expectedAfter('as'),
138+
},
139+
{
140+
code: 'const foo = {}as {};',
141+
output: 'const foo = {}as{};',
142+
options: [NEITHER],
143+
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
144+
errors: unexpectedAfter('as'),
145+
},
146+
{
147+
code: 'const foo = {} as{};',
148+
options: [{ overrides: { as: {} } }],
149+
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
150+
errors: expectedAfter('as'),
151+
},
152+
],
153+
});

packages/eslint-plugin/typings/eslint-rules.d.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,85 @@ declare module 'eslint/lib/rules/indent' {
142142
export = rule;
143143
}
144144

145+
declare module 'eslint/lib/rules/keyword-spacing' {
146+
import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils';
147+
import { RuleFunction } from '@typescript-eslint/experimental-utils/dist/ts-eslint';
148+
149+
type Options = [
150+
{
151+
before?: boolean;
152+
after?: boolean;
153+
overrides?: Record<
154+
string,
155+
{
156+
before?: boolean;
157+
after?: boolean;
158+
}
159+
>;
160+
},
161+
];
162+
type MessageIds =
163+
| 'expectedBefore'
164+
| 'expectedAfter'
165+
| 'unexpectedBefore'
166+
| 'unexpectedAfter';
167+
168+
const rule: TSESLint.RuleModule<
169+
MessageIds,
170+
Options,
171+
{
172+
// Statements
173+
DebuggerStatement: RuleFunction<TSESTree.DebuggerStatement>;
174+
WithStatement: RuleFunction<TSESTree.WithStatement>;
175+
176+
// Statements - Control flow
177+
BreakStatement: RuleFunction<TSESTree.BreakStatement>;
178+
ContinueStatement: RuleFunction<TSESTree.ContinueStatement>;
179+
ReturnStatement: RuleFunction<TSESTree.ReturnStatement>;
180+
ThrowStatement: RuleFunction<TSESTree.ThrowStatement>;
181+
TryStatement: RuleFunction<TSESTree.TryStatement>;
182+
183+
// Statements - Choice
184+
IfStatement: RuleFunction<TSESTree.IfStatement>;
185+
SwitchStatement: RuleFunction<TSESTree.Node>;
186+
SwitchCase: RuleFunction<TSESTree.Node>;
187+
188+
// Statements - Loops
189+
DoWhileStatement: RuleFunction<TSESTree.DoWhileStatement>;
190+
ForInStatement: RuleFunction<TSESTree.ForInStatement>;
191+
ForOfStatement: RuleFunction<TSESTree.ForOfStatement>;
192+
ForStatement: RuleFunction<TSESTree.ForStatement>;
193+
WhileStatement: RuleFunction<TSESTree.WhileStatement>;
194+
195+
// Statements - Declarations
196+
ClassDeclaration: RuleFunction<TSESTree.ClassDeclaration>;
197+
ExportNamedDeclaration: RuleFunction<TSESTree.ExportNamedDeclaration>;
198+
ExportDefaultDeclaration: RuleFunction<TSESTree.ExportDefaultDeclaration>;
199+
ExportAllDeclaration: RuleFunction<TSESTree.ExportAllDeclaration>;
200+
FunctionDeclaration: RuleFunction<TSESTree.FunctionDeclaration>;
201+
ImportDeclaration: RuleFunction<TSESTree.ImportDeclaration>;
202+
VariableDeclaration: RuleFunction<TSESTree.VariableDeclaration>;
203+
204+
// Expressions
205+
ArrowFunctionExpression: RuleFunction<TSESTree.ArrowFunctionExpression>;
206+
AwaitExpression: RuleFunction<TSESTree.AwaitExpression>;
207+
ClassExpression: RuleFunction<TSESTree.ClassExpression>;
208+
FunctionExpression: RuleFunction<TSESTree.FunctionExpression>;
209+
NewExpression: RuleFunction<TSESTree.NewExpression>;
210+
Super: RuleFunction<TSESTree.Super>;
211+
ThisExpression: RuleFunction<TSESTree.ThisExpression>;
212+
UnaryExpression: RuleFunction<TSESTree.UnaryExpression>;
213+
YieldExpression: RuleFunction<TSESTree.YieldExpression>;
214+
215+
// Others
216+
ImportNamespaceSpecifier: RuleFunction<TSESTree.ImportNamespaceSpecifier>;
217+
MethodDefinition: RuleFunction<TSESTree.MethodDefinition>;
218+
Property: RuleFunction<TSESTree.Property>;
219+
}
220+
>;
221+
export = rule;
222+
}
223+
145224
declare module 'eslint/lib/rules/no-dupe-class-members' {
146225
import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils';
147226

0 commit comments

Comments
 (0)
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