Skip to content

Commit 52606f1

Browse files
committed
create type-annotation-spacing
1 parent 7786e3b commit 52606f1

File tree

5 files changed

+341
-1
lines changed

5 files changed

+341
-1
lines changed

src/configs/recommended.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@
4444
2,
4545
"never"
4646
],
47+
"ft-flow/type-annotation-spacing": [
48+
"warn",
49+
"never"
50+
],
4751
"ft-flow/type-id-match": 0,
4852
"ft-flow/union-intersection-spacing": [
4953
2,
@@ -56,4 +60,4 @@
5660
"onlyFilesWithFlowAnnotation": false
5761
}
5862
}
59-
}
63+
}

src/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import spaceAfterTypeColon from './rules/spaceAfterTypeColon';
4444
import spaceBeforeGenericBracket from './rules/spaceBeforeGenericBracket';
4545
import spaceBeforeTypeColon from './rules/spaceBeforeTypeColon';
4646
import spreadExactType from './rules/spreadExactType';
47+
import typeAnnotationSpacing from './rules/typeAnnotationSpacing';
4748
import typeIdMatch from './rules/typeIdMatch';
4849
import typeImportStyle from './rules/typeImportStyle';
4950
import unionIntersectionSpacing from './rules/unionIntersectionSpacing';
@@ -98,6 +99,7 @@ const rules = {
9899
'space-before-generic-bracket': spaceBeforeGenericBracket,
99100
'space-before-type-colon': spaceBeforeTypeColon,
100101
'spread-exact-type': spreadExactType,
102+
'type-annotation-spacing': typeAnnotationSpacing,
101103
'type-id-match': typeIdMatch,
102104
'type-import-style': typeImportStyle,
103105
'union-intersection-spacing': unionIntersectionSpacing,
@@ -149,6 +151,7 @@ export default {
149151
'space-before-generic-bracket': 0,
150152
'space-before-type-colon': 0,
151153
'spread-exact-type': 0,
154+
'type-annotation-spacing': 0,
152155
'type-id-match': 0,
153156
'type-import-style': 0,
154157
'union-intersection-spacing': 0,

src/rules/typeAnnotationSpacing.js

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import {
2+
spacingFixers,
3+
} from '../utilities';
4+
5+
const schema = [
6+
{
7+
enum: ['always', 'never'],
8+
type: 'string',
9+
},
10+
];
11+
12+
function isNeverOption(context) {
13+
return (context.options[0] || 'never') === 'never';
14+
}
15+
16+
function isWhitespaceCRLF(whitespace) {
17+
return whitespace !== '\n' && whitespace !== '\r';
18+
}
19+
20+
function spacesOutside(node, context) {
21+
const { callee, typeArguments } = node;
22+
if (typeArguments == null) {
23+
return;
24+
}
25+
26+
const sourceCode = context.getSourceCode();
27+
const { name } = callee;
28+
const never = isNeverOption(context);
29+
const parentheses = sourceCode.getTokenAfter(typeArguments);
30+
31+
const spacesBefore = typeArguments.range[0] - callee.range[1];
32+
const spacesAfter = parentheses.range[0] - typeArguments.range[1];
33+
34+
if (never) {
35+
if (spacesBefore) {
36+
const whiteSpaceBefore = sourceCode.text[typeArguments.range[0]];
37+
38+
if (isWhitespaceCRLF(whiteSpaceBefore)) {
39+
context.report({
40+
data: { name },
41+
fix: spacingFixers.stripSpacesBefore(typeArguments, spacesBefore),
42+
message: 'There must be no space before "{{name}}" type annotation',
43+
node,
44+
});
45+
}
46+
}
47+
48+
if (spacesAfter) {
49+
const whiteSpaceAfter = sourceCode.text[typeArguments.range[1] - 1];
50+
51+
if (isWhitespaceCRLF(whiteSpaceAfter)) {
52+
context.report({
53+
data: { name },
54+
fix: spacingFixers.stripSpacesAfter(typeArguments, spacesAfter),
55+
message: 'There must be no space after "{{name}}" type annotation',
56+
node,
57+
});
58+
}
59+
}
60+
61+
return;
62+
}
63+
64+
if (!never) {
65+
if (spacesBefore > 1) {
66+
context.report({
67+
data: { name },
68+
fix: spacingFixers.stripSpacesBefore(typeArguments, spacesBefore - 1),
69+
message: 'There must be one space before "{{name}}" generic type annotation bracket',
70+
node,
71+
});
72+
}
73+
74+
if (spacesBefore === 0) {
75+
context.report({
76+
data: { name },
77+
fix: spacingFixers.addSpaceBefore(typeArguments),
78+
message: 'There must be a space before "{{name}}" generic type annotation bracket',
79+
node,
80+
});
81+
}
82+
83+
if (spacesAfter > 1) {
84+
context.report({
85+
data: { name },
86+
fix: spacingFixers.stripSpacesAfter(typeArguments, spacesAfter),
87+
message: 'There must be one space before "{{name}}" generic type annotation bracket',
88+
node,
89+
});
90+
}
91+
92+
if (spacesAfter === 0) {
93+
context.report({
94+
data: { name },
95+
fix: spacingFixers.addSpaceAfter(typeArguments),
96+
message: 'There must be a space before "{{name}}" generic type annotation bracket',
97+
node,
98+
});
99+
}
100+
}
101+
}
102+
103+
function spacesInside(node, context) {
104+
const { callee, typeArguments } = node;
105+
if (typeArguments == null) {
106+
return;
107+
}
108+
109+
const sourceCode = context.getSourceCode();
110+
const { name } = callee;
111+
const never = isNeverOption(context);
112+
const isNullable = typeArguments.params[0].type === 'NullableTypeAnnotation';
113+
const [
114+
opener,
115+
firstInnerToken,
116+
secondInnerToken,
117+
] = sourceCode.getFirstTokens(typeArguments, 3);
118+
const [
119+
lastInnerToken,
120+
closer,
121+
] = sourceCode.getLastTokens(typeArguments, 2);
122+
123+
const spacesBefore = firstInnerToken.range[0] - opener.range[1];
124+
const spaceBetweenNullToken = secondInnerToken.range[0] - firstInnerToken.range[1];
125+
const spacesAfter = closer.range[0] - lastInnerToken.range[1];
126+
127+
if (never) {
128+
if (spacesBefore) {
129+
const whiteSpaceBefore = sourceCode.text[opener.range[1]];
130+
131+
if (whiteSpaceBefore !== '\n' && whiteSpaceBefore !== '\r') {
132+
context.report({
133+
data: { name },
134+
fix: spacingFixers.stripSpacesAfter(opener, spacesBefore),
135+
message: 'There must be no spaces inside at the start of "{{name}}" type annotation',
136+
node,
137+
});
138+
}
139+
}
140+
141+
if (isNullable && spaceBetweenNullToken) {
142+
context.report({
143+
data: { name },
144+
fix: spacingFixers.stripSpacesAfter(firstInnerToken, spaceBetweenNullToken),
145+
message: 'There must be no spaces inside "{{name}}" type annotation',
146+
node,
147+
});
148+
}
149+
150+
if (spacesAfter) {
151+
const whiteSpaceAfter = sourceCode.text[closer.range[0] - 1];
152+
153+
if (isWhitespaceCRLF(whiteSpaceAfter)) {
154+
context.report({
155+
data: { name },
156+
fix: spacingFixers.stripSpacesAfter(lastInnerToken, spacesAfter),
157+
message: 'There must be no spaces inside at the end of "{{name}}" type annotation',
158+
node,
159+
});
160+
}
161+
}
162+
163+
return;
164+
}
165+
166+
if (!never) {
167+
if (spacesBefore > 1) {
168+
context.report({
169+
data: { name },
170+
fix: spacingFixers.stripSpacesBefore(opener, spacesBefore - 1),
171+
message: 'There must be one space before "{{name}}" generic type annotation bracket',
172+
node,
173+
});
174+
}
175+
176+
if (spacesBefore === 0) {
177+
context.report({
178+
data: { name },
179+
fix: spacingFixers.addSpaceBefore(opener),
180+
message: 'There must be a space before "{{name}}" generic type annotation bracket',
181+
node,
182+
});
183+
}
184+
185+
if (spacesAfter > 1) {
186+
context.report({
187+
data: { name },
188+
fix: spacingFixers.stripSpacesAfter(closer, spacesAfter),
189+
message: 'There must be one space before "{{name}}" generic type annotation bracket',
190+
node,
191+
});
192+
}
193+
194+
if (spacesAfter === 0) {
195+
context.report({
196+
data: { name },
197+
fix: spacingFixers.addSpaceAfter(closer),
198+
message: 'There must be a space before "{{name}}" generic type annotation bracket',
199+
node,
200+
});
201+
}
202+
}
203+
}
204+
205+
const create = (context) => ({
206+
CallExpression(node) {
207+
spacesOutside(node, context);
208+
spacesInside(node, context);
209+
},
210+
});
211+
212+
export default {
213+
create,
214+
meta: {
215+
fixable: 'code',
216+
},
217+
schema,
218+
};
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
export default {
2+
invalid: [
3+
// {
4+
// code: 'const [state, setState] = useState<?string >(null)',
5+
// errors: [{ message: 'There must be no space at start of type annotations' }],
6+
// output: 'const [state, setState] = useState<?string>(null)',
7+
// },
8+
// {
9+
// code: 'const [state, setState] = useState<?string > (null)',
10+
// errors: [{ message: 'There must be no space at start of type annotations' }],
11+
// output: 'const [state, setState] = useState<?string>(null)',
12+
// },
13+
// {
14+
// code: 'const [state, setState] = useState< ?string>(null)',
15+
// errors: [{ message: 'There must be no space at start of type annotations' }],
16+
// output: 'const [state, setState] = useState<?string>(null)',
17+
// },
18+
// {
19+
// code: 'const [state, setState] = useState < ?string>(null)',
20+
// errors: [{ message: 'There must be no space at start of type annotations' }],
21+
// output: 'const [state, setState] = useState<?string>(null)',
22+
// },
23+
// {
24+
// code: 'const [state, setState] = useState<? string>(null)',
25+
// errors: [{ message: 'There must be no space at start of type annotations' }],
26+
// output: 'const [state, setState] = useState<?string>(null)',
27+
// },
28+
// {
29+
// code: 'const [state, setState] = useState< ? string>(null)',
30+
// errors: [{ message: 'There must be no space at start of type annotations' }],
31+
// output: 'const [state, setState] = useState<?string>(null)',
32+
// },
33+
// {
34+
// code: 'const [state, setState] = useState< ? string >(null)',
35+
// errors: [{ message: 'There must be no space at start of type annotations' }],
36+
// output: 'const [state, setState] = useState<?string>(null)',
37+
// },
38+
// {
39+
// code: 'const [state, setState] = useState < ? string > (null)',
40+
// errors: [{ message: 'There must be no space at start of type annotations' }],
41+
// output: 'const [state, setState] = useState<?string>(null)',
42+
// },
43+
// {
44+
// code: 'const [state, setState] = useState < ? string > ()',
45+
// errors: [{ message: 'There must be no space at start of type annotations' }],
46+
// output: 'const [state, setState] = useState<?string>(null)',
47+
// },
48+
{
49+
code: 'useState<string >(null)',
50+
errors: [{ message: 'There must be no space at start of type annotations' }],
51+
output: 'useState<string>(null)',
52+
},
53+
{
54+
code: 'useState< string>(null)',
55+
errors: [{ message: 'There must be no space at start of type annotations' }],
56+
output: 'useState<string>(null)',
57+
},
58+
{
59+
code: 'useState< string >(null)',
60+
errors: [{ message: 'There must be no space at start of type annotations' }],
61+
output: 'useState<string>(null)',
62+
},
63+
],
64+
misconfigured: [
65+
{
66+
errors: [
67+
{
68+
data: 'frequently',
69+
instancePath: '/0',
70+
keyword: 'enum',
71+
message: 'must be equal to one of the allowed values',
72+
params: {
73+
allowedValues: [
74+
'always',
75+
'never',
76+
],
77+
},
78+
parentSchema: {
79+
enum: [
80+
'always',
81+
'never',
82+
],
83+
type: 'string',
84+
},
85+
schema: [
86+
'always',
87+
'never',
88+
],
89+
schemaPath: '#/items/0/enum',
90+
},
91+
],
92+
options: [
93+
'frequently',
94+
],
95+
},
96+
],
97+
valid: [
98+
{
99+
code: 'const [state, setState] = useState(null)',
100+
},
101+
{
102+
code: 'const [state, setState] = useState<string>("")',
103+
},
104+
// {
105+
// code: 'const [state, setState] = useState<?string>(null)',
106+
// },
107+
{
108+
code: 'const [state, setState] = useState<string | null>(null)',
109+
},
110+
{
111+
code: 'const [state, setState] = useState<string | number>(2)',
112+
},
113+
],
114+
};

tests/rules/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ const reportingRules = [
6666
'space-before-generic-bracket',
6767
'space-before-type-colon',
6868
'spread-exact-type',
69+
'type-annotation-spacing',
6970
'type-id-match',
7071
'type-import-style',
7172
'union-intersection-spacing',

0 commit comments

Comments
 (0)