Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit b097245

Browse files
authoredMar 2, 2020
feat(eslint-plugin): additional annotation spacing rules for va… (#1496)
1 parent 33e3e6f commit b097245

File tree

4 files changed

+332
-33
lines changed

4 files changed

+332
-33
lines changed
 

‎packages/eslint-plugin/docs/rules/type-annotation-spacing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ This rule has an object option:
4141
- `"before": true`, (default for arrow) requires a space before the colon/arrow.
4242
- `"after": true`, (default) requires a space after the colon/arrow.
4343
- `"after": false`, disallows spaces after the colon/arrow.
44-
- `"overrides"`, overrides the default options for type annotations with `colon` (e.g. `const foo: string`) and function types with `arrow` (e.g. `type Foo = () => {}`).
44+
- `"overrides"`, overrides the default options for type annotations with `colon` (e.g. `const foo: string`) and function types with `arrow` (e.g. `type Foo = () => {}`). Additionally allows granular overrides for `variable` (`const foo: string`),`parameter` (`function foo(bar: string) {...}`),`property` (`interface Foo { bar: string }`) and `returnType` (`function foo(): string {...}`) annotations.
4545

4646
### defaults
4747

‎packages/eslint-plugin/src/rules/type-annotation-spacing.ts

Lines changed: 96 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,35 @@
11
import { TSESTree } from '@typescript-eslint/experimental-utils';
22
import * as util from '../util';
3+
import {
4+
isClassOrTypeElement,
5+
isFunction,
6+
isFunctionOrFunctionType,
7+
isIdentifier,
8+
isTSFunctionType,
9+
isVariableDeclarator,
10+
} from '../util';
311

4-
type Options = [
5-
{
6-
before?: boolean;
7-
after?: boolean;
8-
overrides?: {
9-
colon?: {
10-
before?: boolean;
11-
after?: boolean;
12-
};
13-
arrow?: {
14-
before?: boolean;
15-
after?: boolean;
16-
};
17-
};
18-
}?,
19-
];
12+
interface WhitespaceRule {
13+
readonly before?: boolean;
14+
readonly after?: boolean;
15+
}
16+
17+
interface WhitespaceOverride {
18+
readonly colon?: WhitespaceRule;
19+
readonly arrow?: WhitespaceRule;
20+
readonly variable?: WhitespaceRule;
21+
readonly property?: WhitespaceRule;
22+
readonly parameter?: WhitespaceRule;
23+
readonly returnType?: WhitespaceRule;
24+
}
25+
26+
interface Config extends WhitespaceRule {
27+
readonly overrides?: WhitespaceOverride;
28+
}
29+
30+
type WhitespaceRules = Required<WhitespaceOverride>;
31+
32+
type Options = [Config?];
2033
type MessageIds =
2134
| 'expectedSpaceAfter'
2235
| 'expectedSpaceBefore'
@@ -32,6 +45,67 @@ const definition = {
3245
additionalProperties: false,
3346
};
3447

48+
function createRules(options?: Config): WhitespaceRules {
49+
const globals = {
50+
...(options?.before !== undefined ? { before: options.before } : {}),
51+
...(options?.after !== undefined ? { after: options.after } : {}),
52+
};
53+
const override = options?.overrides ?? {};
54+
const colon = {
55+
...{ before: false, after: true },
56+
...globals,
57+
...override?.colon,
58+
};
59+
const arrow = {
60+
...{ before: true, after: true },
61+
...globals,
62+
...override?.arrow,
63+
};
64+
65+
return {
66+
colon: colon,
67+
arrow: arrow,
68+
variable: { ...colon, ...override?.variable },
69+
property: { ...colon, ...override?.property },
70+
parameter: { ...colon, ...override?.parameter },
71+
returnType: { ...colon, ...override?.returnType },
72+
};
73+
}
74+
75+
function getIdentifierRules(
76+
rules: WhitespaceRules,
77+
node: TSESTree.Node | undefined,
78+
): WhitespaceRule {
79+
const scope = node?.parent;
80+
81+
if (isVariableDeclarator(scope)) {
82+
return rules.variable;
83+
} else if (isFunctionOrFunctionType(scope)) {
84+
return rules.parameter;
85+
} else {
86+
return rules.colon;
87+
}
88+
}
89+
90+
function getRules(
91+
rules: WhitespaceRules,
92+
node: TSESTree.TypeNode,
93+
): WhitespaceRule {
94+
const scope = node?.parent?.parent;
95+
96+
if (isTSFunctionType(scope)) {
97+
return rules.arrow;
98+
} else if (isIdentifier(scope)) {
99+
return getIdentifierRules(rules, scope);
100+
} else if (isClassOrTypeElement(scope)) {
101+
return rules.property;
102+
} else if (isFunction(scope)) {
103+
return rules.returnType;
104+
} else {
105+
return rules.colon;
106+
}
107+
}
108+
35109
export default util.createRule<Options, MessageIds>({
36110
name: 'type-annotation-spacing',
37111
meta: {
@@ -59,6 +133,10 @@ export default util.createRule<Options, MessageIds>({
59133
properties: {
60134
colon: definition,
61135
arrow: definition,
136+
variable: definition,
137+
parameter: definition,
138+
property: definition,
139+
returnType: definition,
62140
},
63141
additionalProperties: false,
64142
},
@@ -76,20 +154,7 @@ export default util.createRule<Options, MessageIds>({
76154
const punctuators = [':', '=>'];
77155
const sourceCode = context.getSourceCode();
78156

79-
const overrides = options?.overrides ?? { colon: {}, arrow: {} };
80-
81-
const colonOptions = Object.assign(
82-
{},
83-
{ before: false, after: true },
84-
options,
85-
overrides.colon,
86-
);
87-
const arrowOptions = Object.assign(
88-
{},
89-
{ before: true, after: true },
90-
options,
91-
overrides.arrow,
92-
);
157+
const ruleSet = createRules(options);
93158

94159
/**
95160
* Checks if there's proper spacing around type annotations (no space
@@ -108,8 +173,7 @@ export default util.createRule<Options, MessageIds>({
108173
return;
109174
}
110175

111-
const before = type === ':' ? colonOptions.before : arrowOptions.before;
112-
const after = type === ':' ? colonOptions.after : arrowOptions.after;
176+
const { before, after } = getRules(ruleSet, typeAnnotation);
113177

114178
if (type === ':' && previousToken.value === '?') {
115179
// shift the start to the ?

‎packages/eslint-plugin/src/util/astUtils.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,95 @@ function isTypeAssertion(
8282
);
8383
}
8484

85+
function isVariableDeclarator(
86+
node: TSESTree.Node | undefined,
87+
): node is TSESTree.VariableDeclarator {
88+
return node?.type === AST_NODE_TYPES.VariableDeclarator;
89+
}
90+
91+
function isFunction(
92+
node: TSESTree.Node | undefined,
93+
): node is
94+
| TSESTree.ArrowFunctionExpression
95+
| TSESTree.FunctionDeclaration
96+
| TSESTree.FunctionExpression {
97+
if (!node) {
98+
return false;
99+
}
100+
101+
return [
102+
AST_NODE_TYPES.ArrowFunctionExpression,
103+
AST_NODE_TYPES.FunctionDeclaration,
104+
AST_NODE_TYPES.FunctionExpression,
105+
].includes(node.type);
106+
}
107+
108+
function isFunctionType(
109+
node: TSESTree.Node | undefined,
110+
): node is
111+
| TSESTree.TSCallSignatureDeclaration
112+
| TSESTree.TSConstructSignatureDeclaration
113+
| TSESTree.TSEmptyBodyFunctionExpression
114+
| TSESTree.TSFunctionType
115+
| TSESTree.TSMethodSignature {
116+
if (!node) {
117+
return false;
118+
}
119+
120+
return [
121+
AST_NODE_TYPES.TSCallSignatureDeclaration,
122+
AST_NODE_TYPES.TSConstructSignatureDeclaration,
123+
AST_NODE_TYPES.TSEmptyBodyFunctionExpression,
124+
AST_NODE_TYPES.TSFunctionType,
125+
AST_NODE_TYPES.TSMethodSignature,
126+
].includes(node.type);
127+
}
128+
129+
function isFunctionOrFunctionType(
130+
node: TSESTree.Node | undefined,
131+
): node is
132+
| TSESTree.ArrowFunctionExpression
133+
| TSESTree.FunctionDeclaration
134+
| TSESTree.FunctionExpression
135+
| TSESTree.TSCallSignatureDeclaration
136+
| TSESTree.TSConstructSignatureDeclaration
137+
| TSESTree.TSEmptyBodyFunctionExpression
138+
| TSESTree.TSFunctionType
139+
| TSESTree.TSMethodSignature {
140+
return isFunction(node) || isFunctionType(node);
141+
}
142+
143+
function isTSFunctionType(
144+
node: TSESTree.Node | undefined,
145+
): node is TSESTree.TSFunctionType {
146+
return node?.type === AST_NODE_TYPES.TSFunctionType;
147+
}
148+
149+
function isClassOrTypeElement(
150+
node: TSESTree.Node | undefined,
151+
): node is TSESTree.ClassElement | TSESTree.TypeElement {
152+
if (!node) {
153+
return false;
154+
}
155+
156+
return [
157+
// ClassElement
158+
AST_NODE_TYPES.ClassProperty,
159+
AST_NODE_TYPES.FunctionExpression,
160+
AST_NODE_TYPES.MethodDefinition,
161+
AST_NODE_TYPES.TSAbstractClassProperty,
162+
AST_NODE_TYPES.TSAbstractMethodDefinition,
163+
AST_NODE_TYPES.TSEmptyBodyFunctionExpression,
164+
AST_NODE_TYPES.TSIndexSignature,
165+
// TypeElement
166+
AST_NODE_TYPES.TSCallSignatureDeclaration,
167+
AST_NODE_TYPES.TSConstructSignatureDeclaration,
168+
// AST_NODE_TYPES.TSIndexSignature,
169+
AST_NODE_TYPES.TSMethodSignature,
170+
AST_NODE_TYPES.TSPropertySignature,
171+
].includes(node.type);
172+
}
173+
85174
/**
86175
* Checks if a node is a constructor method.
87176
*/
@@ -136,6 +225,10 @@ export {
136225
isAwaitExpression,
137226
isAwaitKeyword,
138227
isConstructor,
228+
isClassOrTypeElement,
229+
isFunction,
230+
isFunctionOrFunctionType,
231+
isFunctionType,
139232
isIdentifier,
140233
isLogicalOrOperator,
141234
isNonNullAssertionPunctuator,
@@ -145,6 +238,8 @@ export {
145238
isOptionalOptionalChain,
146239
isSetter,
147240
isTokenOnSameLine,
241+
isTSFunctionType,
148242
isTypeAssertion,
243+
isVariableDeclarator,
149244
LINEBREAK_MATCHER,
150245
};

‎packages/eslint-plugin/tests/rules/type-annotation-spacing.test.ts

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,6 +1034,146 @@ type Bar = Record<keyof Foo, string>
10341034
],
10351035
},
10361036
'let resolver: (() => PromiseLike<T>) | PromiseLike<T>;',
1037+
{
1038+
code: 'const foo:string;',
1039+
options: [
1040+
{
1041+
overrides: {
1042+
colon: {
1043+
after: false,
1044+
before: true,
1045+
},
1046+
variable: {
1047+
before: false,
1048+
},
1049+
},
1050+
},
1051+
],
1052+
},
1053+
{
1054+
code: 'const foo:string;',
1055+
options: [
1056+
{
1057+
before: true,
1058+
overrides: {
1059+
colon: {
1060+
after: true,
1061+
before: false,
1062+
},
1063+
variable: {
1064+
after: false,
1065+
},
1066+
},
1067+
},
1068+
],
1069+
},
1070+
{
1071+
code: `
1072+
interface Foo {
1073+
greet():string;
1074+
}
1075+
`,
1076+
options: [
1077+
{
1078+
overrides: {
1079+
colon: {
1080+
after: false,
1081+
before: true,
1082+
},
1083+
property: {
1084+
before: false,
1085+
},
1086+
},
1087+
},
1088+
],
1089+
},
1090+
{
1091+
code: `
1092+
interface Foo {
1093+
name:string;
1094+
}
1095+
`,
1096+
options: [
1097+
{
1098+
before: true,
1099+
overrides: {
1100+
colon: {
1101+
after: true,
1102+
before: false,
1103+
},
1104+
property: {
1105+
after: false,
1106+
},
1107+
},
1108+
},
1109+
],
1110+
},
1111+
{
1112+
code: 'function foo(name:string) {}',
1113+
options: [
1114+
{
1115+
overrides: {
1116+
colon: {
1117+
after: false,
1118+
before: true,
1119+
},
1120+
parameter: {
1121+
before: false,
1122+
},
1123+
},
1124+
},
1125+
],
1126+
},
1127+
{
1128+
code: 'function foo(name:string) {}',
1129+
options: [
1130+
{
1131+
before: true,
1132+
overrides: {
1133+
colon: {
1134+
after: true,
1135+
before: false,
1136+
},
1137+
parameter: {
1138+
after: false,
1139+
},
1140+
},
1141+
},
1142+
],
1143+
},
1144+
{
1145+
code: 'function foo():string {}',
1146+
options: [
1147+
{
1148+
overrides: {
1149+
colon: {
1150+
after: false,
1151+
before: true,
1152+
},
1153+
returnType: {
1154+
before: false,
1155+
},
1156+
},
1157+
},
1158+
],
1159+
},
1160+
{
1161+
code: 'function foo():string {}',
1162+
options: [
1163+
{
1164+
before: true,
1165+
overrides: {
1166+
colon: {
1167+
after: true,
1168+
before: false,
1169+
},
1170+
returnType: {
1171+
after: false,
1172+
},
1173+
},
1174+
},
1175+
],
1176+
},
10371177
],
10381178
invalid: [
10391179
{

0 commit comments

Comments
 (0)
Please sign in to comment.