Skip to content

Commit 74bcb8c

Browse files
pnevykgajus
authored andcommitted
feat: add require-types-at-top rule (closes #319, #168, #285)
1 parent c10b0d9 commit 74bcb8c

File tree

6 files changed

+177
-0
lines changed

6 files changed

+177
-0
lines changed

.README/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ When `true`, only checks files with a [`@flow` annotation](http://flowtype.org/d
159159
{"gitdown": "include", "file": "./rules/require-exact-type.md"}
160160
{"gitdown": "include", "file": "./rules/require-parameter-type.md"}
161161
{"gitdown": "include", "file": "./rules/require-return-type.md"}
162+
{"gitdown": "include", "file": "./rules/require-types-at-top.md"}
162163
{"gitdown": "include", "file": "./rules/require-valid-file-annotation.md"}
163164
{"gitdown": "include", "file": "./rules/require-variable-type.md"}
164165
{"gitdown": "include", "file": "./rules/semi.md"}

.README/rules/require-types-at-top.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
### `require-types-at-top`
2+
3+
Requires all type declarations to be at the top of the file, after any import declarations.
4+
5+
#### Options
6+
7+
The rule has a string option:
8+
9+
* `"never"`
10+
* `"always"`
11+
12+
The default value is `"always"`.
13+
14+
<!-- assertions require-types-at-top -->

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import objectTypeDelimiter from './rules/objectTypeDelimiter';
1717
import requireExactType from './rules/requireExactType';
1818
import requireParameterType from './rules/requireParameterType';
1919
import requireReturnType from './rules/requireReturnType';
20+
import requireTypesAtTop from './rules/requireTypesAtTop';
2021
import requireValidFileAnnotation from './rules/requireValidFileAnnotation';
2122
import requireVariableType from './rules/requireVariableType';
2223
import semi from './rules/semi';
@@ -48,6 +49,7 @@ const rules = {
4849
'require-exact-type': requireExactType,
4950
'require-parameter-type': requireParameterType,
5051
'require-return-type': requireReturnType,
52+
'require-types-at-top': requireTypesAtTop,
5153
'require-valid-file-annotation': requireValidFileAnnotation,
5254
'require-variable-type': requireVariableType,
5355
semi,

src/rules/requireTypesAtTop.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import _ from 'lodash';
2+
3+
const schema = [
4+
{
5+
enum: ['always', 'never'],
6+
type: 'string'
7+
}
8+
];
9+
10+
const create = (context) => {
11+
const always = (context.options[0] || 'always') === 'always';
12+
13+
if (always) {
14+
const sourceCode = context.getSourceCode();
15+
16+
// nodes representing type and import declarations
17+
const ignoredNodes = [
18+
// import ...
19+
(node) => { return node.type === 'ImportDeclaration'; },
20+
// export type Foo = ...
21+
// export opaque type Foo = ...
22+
// export type Foo from ...
23+
// export opaque type Foo from ...
24+
(node) => { return node.type === 'ExportNamedDeclaration' && node.exportKind === 'type'; },
25+
// type Foo = ...
26+
(node) => { return node.type === 'TypeAlias'; },
27+
// opaque type Foo = ...
28+
(node) => { return node.type === 'OpaqueType'; }
29+
];
30+
31+
const isIgnoredNode = (node) => {
32+
for (const predicate of ignoredNodes) {
33+
if (predicate(node)) {
34+
return true;
35+
}
36+
}
37+
38+
return false;
39+
};
40+
41+
let regularCodeStartRange;
42+
43+
for (const node of sourceCode.ast.body) {
44+
if (!isIgnoredNode(node)) {
45+
regularCodeStartRange = node.range;
46+
break;
47+
}
48+
}
49+
50+
if (!_.isArray(regularCodeStartRange)) {
51+
// a source with only ignored nodes
52+
return {};
53+
}
54+
55+
return {
56+
'TypeAlias, OpaqueType' (node) {
57+
if (node.range[0] > regularCodeStartRange[0]) {
58+
context.report({
59+
message: 'All type declaration should be at the top of the file, after any import declarations.',
60+
node
61+
});
62+
}
63+
}
64+
};
65+
} else {
66+
return {};
67+
}
68+
};
69+
70+
export default {
71+
create,
72+
schema
73+
};
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
export default {
2+
invalid: [
3+
{
4+
code: 'const foo = 3;\ntype Foo = number;',
5+
errors: [{message: 'All type declaration should be at the top of the file, after any import declarations.'}]
6+
},
7+
{
8+
code: 'const foo = 3;\nopaque type Foo = number;',
9+
errors: [{message: 'All type declaration should be at the top of the file, after any import declarations.'}]
10+
},
11+
{
12+
code: 'const foo = 3;\nexport type Foo = number;',
13+
errors: [{message: 'All type declaration should be at the top of the file, after any import declarations.'}]
14+
},
15+
{
16+
code: 'const foo = 3;\nexport opaque type Foo = number;',
17+
errors: [{message: 'All type declaration should be at the top of the file, after any import declarations.'}]
18+
},
19+
{
20+
code: 'const foo = 3;\ntype Foo = number | string;',
21+
errors: [{message: 'All type declaration should be at the top of the file, after any import declarations.'}]
22+
},
23+
{
24+
code: 'import bar from "./bar";\nconst foo = 3;\ntype Foo = number;',
25+
errors: [{message: 'All type declaration should be at the top of the file, after any import declarations.'}]
26+
}
27+
],
28+
misconfigured: [
29+
{
30+
errors: [
31+
{
32+
data: 'sometimes',
33+
dataPath: '[0]',
34+
keyword: 'enum',
35+
message: 'should be equal to one of the allowed values',
36+
params: {
37+
allowedValues: [
38+
'always',
39+
'never'
40+
]
41+
},
42+
parentSchema: {
43+
enum: [
44+
'always',
45+
'never'
46+
],
47+
type: 'string'
48+
},
49+
schema: [
50+
'always',
51+
'never'
52+
],
53+
schemaPath: '#/items/0/enum'
54+
}
55+
],
56+
options: ['sometimes']
57+
}
58+
],
59+
valid: [
60+
{
61+
code: 'type Foo = number;\nconst foo = 3;'
62+
},
63+
{
64+
code: 'opaque type Foo = number;\nconst foo = 3;'
65+
},
66+
{
67+
code: 'export type Foo = number;\nconst foo = 3;'
68+
},
69+
{
70+
code: 'export opaque type Foo = number;\nconst foo = 3;'
71+
},
72+
{
73+
code: 'type Foo = number;\nconst foo = 3;'
74+
},
75+
{
76+
code: 'import bar from "./bar";\ntype Foo = number;'
77+
},
78+
{
79+
code: 'type Foo = number;\nimport bar from "./bar";'
80+
},
81+
{
82+
code: 'const foo = 3;\ntype Foo = number;',
83+
options: ['never']
84+
}
85+
]
86+
};

tests/rules/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const reportingRules = [
2727
'require-exact-type',
2828
'require-parameter-type',
2929
'require-return-type',
30+
'require-types-at-top',
3031
'require-valid-file-annotation',
3132
'require-variable-type',
3233
'semi',

0 commit comments

Comments
 (0)