8000 feat(eslint-plugin): add extension rule `keyword-spacing` (#1739) · DudaGod/typescript-eslint@c5106dd · GitHub
[go: up one dir, main page]

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
134
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+
{
+
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)
0