Skip to content

Commit

Permalink
feat: add new rule no-duplicate-type-union-intersection-members (#503)
Browse files Browse the repository at this point in the history
  • Loading branch information
Geraint White committed Sep 20, 2021
1 parent 0b5b3a8 commit 1c1c009
Show file tree
Hide file tree
Showing 6 changed files with 205 additions and 0 deletions.
1 change: 1 addition & 0 deletions .README/README.md
Expand Up @@ -169,6 +169,7 @@ When `true`, only checks files with a [`@flow` annotation](http://flowtype.org/d
{"gitdown": "include", "file": "./rules/interface-id-match.md"}
{"gitdown": "include", "file": "./rules/newline-after-flow-annotation.md"}
{"gitdown": "include", "file": "./rules/no-dupe-keys.md"}
{"gitdown": "include", "file": "./rules/no-duplicate-type-union-intersection-members.md"}
{"gitdown": "include", "file": "./rules/no-existential-type.md"}
{"gitdown": "include", "file": "./rules/no-flow-fix-me-comments.md"}
{"gitdown": "include", "file": "./rules/no-internal-flow-type.md"}
Expand Down
43 changes: 43 additions & 0 deletions .README/rules/no-duplicate-type-union-intersection-members.md
@@ -0,0 +1,43 @@
### `no-duplicate-type-union-intersection-members`

_The `--fix` option on the command line automatically fixes problems reported by this rule._

Checks for duplicate members of a type union/intersection.

#### Options

You can disable checking intersection types using `checkIntersections`.

* `true` (default) - check for duplicate members of intersection members.
* `false` - do not check for duplicate members of intersection members.

```js
{
"rules": {
"flowtype/no-duplicate-type-union-intersection-members": [
2,
{
"checkIntersections": true
}
]
}
}
```

You can disable checking union types using `checkUnions`.

* `true` (default) - check for duplicate members of union members.
* `false` - do not check for duplicate members of union members.

```js
{
"rules": {
"flowtype/no-duplicate-type-union-intersection-members": [
2,
{
"checkUnions": true
}
]
}
}
```
3 changes: 3 additions & 0 deletions src/index.js
Expand Up @@ -11,6 +11,7 @@ import genericSpacing from './rules/genericSpacing';
import interfaceIdMatch from './rules/interfaceIdMatch';
import newlineAfterFlowAnnotation from './rules/newlineAfterFlowAnnotation';
import noDupeKeys from './rules/noDupeKeys';
import noDuplicateTypeUnionIntersectionMembers from './rules/noDuplicateTypeUnionIntersectionMembers';
import noExistentialType from './rules/noExistentialType';
import noFlowFixMeComments from './rules/noFlowFixMeComments';
import noInternalFlowType from './rules/noInternalFlowType';
Expand Down Expand Up @@ -62,6 +63,7 @@ const rules = {
'interface-id-match': interfaceIdMatch,
'newline-after-flow-annotation': newlineAfterFlowAnnotation,
'no-dupe-keys': noDupeKeys,
'no-duplicate-type-union-intersection-members': noDuplicateTypeUnionIntersectionMembers,
'no-existential-type': noExistentialType,
'no-flow-fix-me-comments': noFlowFixMeComments,
'no-internal-flow-type': noInternalFlowType,
Expand Down Expand Up @@ -121,6 +123,7 @@ export default {
'interface-id-match': 0,
'newline-after-flow-annotation': 0,
'no-dupe-keys': 0,
'no-duplicate-type-union-intersection-members': 0,
'no-flow-fix-me-comments': 0,
'no-mixed': 0,
'no-mutable-array': 0,
Expand Down
113 changes: 113 additions & 0 deletions src/rules/noDuplicateTypeUnionIntersectionMembers.js
@@ -0,0 +1,113 @@
const create = (context) => {
const sourceCode = context.getSourceCode();

const {
checkIntersections = true,
checkUnions = true,
} = context.options[1] || {};

const checkForDuplicates = (node) => {
const uniqueMembers = [];
const duplicates = [];

const source = node.types.map((type) => {
return {
node: type,
text: sourceCode.getText(type),
};
});

const hasComments = node.types.some((type) => {
const count =
sourceCode.getCommentsBefore(type).length +
sourceCode.getCommentsAfter(type).length;

return count > 0;
});

const fix = (fixer) => {
const result = uniqueMembers
.map((t) => {
return t.text;
})
.join(
node.type === 'UnionTypeAnnotation' ? ' | ' : ' & ',
);

return fixer.replaceText(node, result);
};

for (const member of source) {
const match = uniqueMembers.find((uniqueMember) => {
return uniqueMember.text === member.text;
});

if (match) {
duplicates.push(member);
} else {
uniqueMembers.push(member);
}
}

for (const duplicate of duplicates) {
context.report({
data: {
name: duplicate.text,
type: node.type === 'UnionTypeAnnotation' ? 'union' : 'intersection',
},
messageId: 'duplicate',
node,

// don't autofix if any of the types have leading/trailing comments
// the logic for preserving them correctly is a pain - we may implement this later
...hasComments ?
{
suggest: [
{
fix,
messageId: 'suggestFix',
},
],
} :
{fix},
});
}
};

return {
IntersectionTypeAnnotation (node) {
if (checkIntersections === true) {
checkForDuplicates(node);
}
},
UnionTypeAnnotation (node) {
if (checkUnions === true) {
checkForDuplicates(node);
}
},
};
};

export default {
create,
meta: {
fixable: 'code',
messages: {
duplicate: 'Duplicate {{type}} member found "{{name}}".',
suggestFix: 'Remove duplicate members of type (removes all comments).',
},
schema: [
{
properties: {
checkIntersections: {
type: 'boolean',
},
checkUnions: {
type: 'boolean',
},
},
type: 'object',
},
],
},
};
44 changes: 44 additions & 0 deletions tests/rules/assertions/noDuplicateTypeUnionIntersectionMembers.js
@@ -0,0 +1,44 @@
export default {
invalid: [
{
code: 'type A = 1 | 2 | 3 | 1;',
errors: [{message: 'Duplicate union member found "1".'}],
output: 'type A = 1 | 2 | 3;',
},
{
code: 'type B = \'foo\' | \'bar\' | \'foo\';',
errors: [{message: 'Duplicate union member found "\'foo\'".'}],
output: 'type B = \'foo\' | \'bar\';',
},
{
code: 'type C = A | B | A | B;',
errors: [
{message: 'Duplicate union member found "A".'},
{message: 'Duplicate union member found "B".'},
],
output: 'type C = A | B;',
},
{
code: 'type C = A & B & A & B;',
errors: [
{message: 'Duplicate intersection member found "A".'},
{message: 'Duplicate intersection member found "B".'},
],
output: 'type C = A & B;',
},
],
valid: [
{
code: 'type A = 1 | 2 | 3;',
},
{
code: 'type B = \'foo\' | \'bar\';',
},
{
code: 'type C = A | B;',
},
{
code: 'type C = A & B;',
},
],
};
1 change: 1 addition & 0 deletions tests/rules/index.js
Expand Up @@ -22,6 +22,7 @@ const reportingRules = [
'interface-id-match',
'newline-after-flow-annotation',
'no-dupe-keys',
'no-duplicate-type-union-intersection-members',
'no-existential-type',
'no-flow-fix-me-comments',
'no-mutable-array',
Expand Down

0 comments on commit 1c1c009

Please sign in to comment.