Skip to content

Commit 1c1c009

Browse files
author
Geraint White
authored
feat: add new rule no-duplicate-type-union-intersection-members (#503)
1 parent 0b5b3a8 commit 1c1c009

File tree

6 files changed

+205
-0
lines changed

6 files changed

+205
-0
lines changed

.README/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ When `true`, only checks files with a [`@flow` annotation](http://flowtype.org/d
169169
{"gitdown": "include", "file": "./rules/interface-id-match.md"}
170170
{"gitdown": "include", "file": "./rules/newline-after-flow-annotation.md"}
171171
{"gitdown": "include", "file": "./rules/no-dupe-keys.md"}
172+
{"gitdown": "include", "file": "./rules/no-duplicate-type-union-intersection-members.md"}
172173
{"gitdown": "include", "file": "./rules/no-existential-type.md"}
173174
{"gitdown": "include", "file": "./rules/no-flow-fix-me-comments.md"}
174175
{"gitdown": "include", "file": "./rules/no-internal-flow-type.md"}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
### `no-duplicate-type-union-intersection-members`
2+
3+
_The `--fix` option on the command line automatically fixes problems reported by this rule._
4+
5+
Checks for duplicate members of a type union/intersection.
6+
7+
#### Options
8+
9+
You can disable checking intersection types using `checkIntersections`.
10+
11+
* `true` (default) - check for duplicate members of intersection members.
12+
* `false` - do not check for duplicate members of intersection members.
13+
14+
```js
15+
{
16+
"rules": {
17+
"flowtype/no-duplicate-type-union-intersection-members": [
18+
2,
19+
{
20+
"checkIntersections": true
21+
}
22+
]
23+
}
24+
}
25+
```
26+
27+
You can disable checking union types using `checkUnions`.
28+
29+
* `true` (default) - check for duplicate members of union members.
30+
* `false` - do not check for duplicate members of union members.
31+
32+
```js
33+
{
34+
"rules": {
35+
"flowtype/no-duplicate-type-union-intersection-members": [
36+
2,
37+
{
38+
"checkUnions": true
39+
}
40+
]
41+
}
42+
}
43+
```

src/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import genericSpacing from './rules/genericSpacing';
1111
import interfaceIdMatch from './rules/interfaceIdMatch';
1212
import newlineAfterFlowAnnotation from './rules/newlineAfterFlowAnnotation';
1313
import noDupeKeys from './rules/noDupeKeys';
14+
import noDuplicateTypeUnionIntersectionMembers from './rules/noDuplicateTypeUnionIntersectionMembers';
1415
import noExistentialType from './rules/noExistentialType';
1516
import noFlowFixMeComments from './rules/noFlowFixMeComments';
1617
import noInternalFlowType from './rules/noInternalFlowType';
@@ -62,6 +63,7 @@ const rules = {
6263
'interface-id-match': interfaceIdMatch,
6364
'newline-after-flow-annotation': newlineAfterFlowAnnotation,
6465
'no-dupe-keys': noDupeKeys,
66+
'no-duplicate-type-union-intersection-members': noDuplicateTypeUnionIntersectionMembers,
6567
'no-existential-type': noExistentialType,
6668
'no-flow-fix-me-comments': noFlowFixMeComments,
6769
'no-internal-flow-type': noInternalFlowType,
@@ -121,6 +123,7 @@ export default {
121123
'interface-id-match': 0,
122124
'newline-after-flow-annotation': 0,
123125
'no-dupe-keys': 0,
126+
'no-duplicate-type-union-intersection-members': 0,
124127
'no-flow-fix-me-comments': 0,
125128
'no-mixed': 0,
126129
'no-mutable-array': 0,
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
const create = (context) => {
2+
const sourceCode = context.getSourceCode();
3+
4+
const {
5+
checkIntersections = true,
6+
checkUnions = true,
7+
} = context.options[1] || {};
8+
9+
const checkForDuplicates = (node) => {
10+
const uniqueMembers = [];
11+
const duplicates = [];
12+
13+
const source = node.types.map((type) => {
14+
return {
15+
node: type,
16+
text: sourceCode.getText(type),
17+
};
18+
});
19+
20+
const hasComments = node.types.some((type) => {
21+
const count =
22+
sourceCode.getCommentsBefore(type).length +
23+
sourceCode.getCommentsAfter(type).length;
24+
25+
return count > 0;
26+
});
27+
28+
const fix = (fixer) => {
29+
const result = uniqueMembers
30+
.map((t) => {
31+
return t.text;
32+
})
33+
.join(
34+
node.type === 'UnionTypeAnnotation' ? ' | ' : ' & ',
35+
);
36+
37+
return fixer.replaceText(node, result);
38+
};
39+
40+
for (const member of source) {
41+
const match = uniqueMembers.find((uniqueMember) => {
42+
return uniqueMember.text === member.text;
43+
});
44+
45+
if (match) {
46+
duplicates.push(member);
47+
} else {
48+
uniqueMembers.push(member);
49+
}
50+
}
51+
52+
for (const duplicate of duplicates) {
53+
context.report({
54+
data: {
55+
name: duplicate.text,
56+
type: node.type === 'UnionTypeAnnotation' ? 'union' : 'intersection',
57+
},
58+
messageId: 'duplicate',
59+
node,
60+
61+
// don't autofix if any of the types have leading/trailing comments
62+
// the logic for preserving them correctly is a pain - we may implement this later
63+
...hasComments ?
64+
{
65+
suggest: [
66+
{
67+
fix,
68+
messageId: 'suggestFix',
69+
},
70+
],
71+
} :
72+
{fix},
73+
});
74+
}
75+
};
76+
77+
return {
78+
IntersectionTypeAnnotation (node) {
79+
if (checkIntersections === true) {
80+
checkForDuplicates(node);
81+
}
82+
},
83+
UnionTypeAnnotation (node) {
84+
if (checkUnions === true) {
85+
checkForDuplicates(node);
86+
}
87+
},
88+
};
89+
};
90+
91+
export default {
92+
create,
93+
meta: {
94+
fixable: 'code',
95+
messages: {
96+
duplicate: 'Duplicate {{type}} member found "{{name}}".',
97+
suggestFix: 'Remove duplicate members of type (removes all comments).',
98+
},
99+
schema: [
100+
{
101+
properties: {
102+
checkIntersections: {
103+
type: 'boolean',
104+
},
105+
checkUnions: {
106+
type: 'boolean',
107+
},
108+
},
109+
type: 'object',
110+
},
111+
],
112+
},
113+
};
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
export default {
2+
invalid: [
3+
{
4+
code: 'type A = 1 | 2 | 3 | 1;',
5+
errors: [{message: 'Duplicate union member found "1".'}],
6+
output: 'type A = 1 | 2 | 3;',
7+
},
8+
{
9+
code: 'type B = \'foo\' | \'bar\' | \'foo\';',
10+
errors: [{message: 'Duplicate union member found "\'foo\'".'}],
11+
output: 'type B = \'foo\' | \'bar\';',
12+
},
13+
{
14+
code: 'type C = A | B | A | B;',
15+
errors: [
16+
{message: 'Duplicate union member found "A".'},
17+
{message: 'Duplicate union member found "B".'},
18+
],
19+
output: 'type C = A | B;',
20+
},
21+
{
22+
code: 'type C = A & B & A & B;',
23+
errors: [
24+
{message: 'Duplicate intersection member found "A".'},
25+
{message: 'Duplicate intersection member found "B".'},
26+
],
27+
output: 'type C = A & B;',
28+
},
29+
],
30+
valid: [
31+
{
32+
code: 'type A = 1 | 2 | 3;',
33+
},
34+
{
35+
code: 'type B = \'foo\' | \'bar\';',
36+
},
37+
{
38+
code: 'type C = A | B;',
39+
},
40+
{
41+
code: 'type C = A & B;',
42+
},
43+
],
44+
};

tests/rules/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const reportingRules = [
2222
'interface-id-match',
2323
'newline-after-flow-annotation',
2424
'no-dupe-keys',
25+
'no-duplicate-type-union-intersection-members',
2526
'no-existential-type',
2627
'no-flow-fix-me-comments',
2728
'no-mutable-array',

0 commit comments

Comments
 (0)