From 1c1c009ba2a58b9660d43c43750396bef8d73904 Mon Sep 17 00:00:00 2001 From: Geraint White Date: Mon, 20 Sep 2021 14:17:05 +0100 Subject: [PATCH] feat: add new rule no-duplicate-type-union-intersection-members (#503) --- .README/README.md | 1 + ...plicate-type-union-intersection-members.md | 43 +++++++ src/index.js | 3 + ...noDuplicateTypeUnionIntersectionMembers.js | 113 ++++++++++++++++++ ...noDuplicateTypeUnionIntersectionMembers.js | 44 +++++++ tests/rules/index.js | 1 + 6 files changed, 205 insertions(+) create mode 100644 .README/rules/no-duplicate-type-union-intersection-members.md create mode 100644 src/rules/noDuplicateTypeUnionIntersectionMembers.js create mode 100644 tests/rules/assertions/noDuplicateTypeUnionIntersectionMembers.js diff --git a/.README/README.md b/.README/README.md index ffe9a24..24da4b5 100644 --- a/.README/README.md +++ b/.README/README.md @@ -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"} diff --git a/.README/rules/no-duplicate-type-union-intersection-members.md b/.README/rules/no-duplicate-type-union-intersection-members.md new file mode 100644 index 0000000..fa78b76 --- /dev/null +++ b/.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 + } + ] + } +} +``` diff --git a/src/index.js b/src/index.js index b8a9369..a6e5746 100644 --- a/src/index.js +++ b/src/index.js @@ -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'; @@ -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, @@ -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, diff --git a/src/rules/noDuplicateTypeUnionIntersectionMembers.js b/src/rules/noDuplicateTypeUnionIntersectionMembers.js new file mode 100644 index 0000000..87d4885 --- /dev/null +++ b/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', + }, + ], + }, +}; diff --git a/tests/rules/assertions/noDuplicateTypeUnionIntersectionMembers.js b/tests/rules/assertions/noDuplicateTypeUnionIntersectionMembers.js new file mode 100644 index 0000000..83ae84f --- /dev/null +++ b/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;', + }, + ], +}; diff --git a/tests/rules/index.js b/tests/rules/index.js index 3694375..87d0a7a 100644 --- a/tests/rules/index.js +++ b/tests/rules/index.js @@ -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',