Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork2.8k
feat(eslint-plugin): addno-duplicate-type-constituents rule#5728
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Merged
JoshuaKGoldberg merged 61 commits intotypescript-eslint:mainfromsajikix:add-rule-no-duplicate-type-union-intersection-membersMar 24, 2023
Uh oh!
There was an error while loading.Please reload this page.
Merged
Changes fromall commits
Commits
Show all changes
61 commits Select commitHold shift + click to select a range
6ee8df7 feat: add rule code
sajikix90b5e96 test: add test for rule
sajikix1a3a620 docs: add docs of new rule
sajikix29874a8 refactor: make method definitions more concise
sajikixe03a7e8 fix: change check option to ignore option
sajikix95afec8 refactor: rename to type-constituents
sajikix1e7cbb1 refactor: use recursive type-node checker
sajikix3c67cef Merge branch 'main' into add-rule-no-duplicate-type-union-intersectio…
sajikix39f18bc fix: rename doc filename and test title
sajikixf430871 refactor: use removeRage instead of replaceText
sajikix49e27be refactor: narrows node comparison function argument type
sajikixb368abb fix: doc description
sajikix4455b49 Merge branch 'main' into add-rule-no-duplicate-type-union-intersectio…
sajikix98a008c Merge branch 'main' into add-rule-no-duplicate-type-union-intersectio…
sajikixf1f507d refactor: update hasComments logic
sajikix9e75e07 fix: remove cases that never occur
sajikixbec198c refactor: use type checker
sajikix71afc2e fix: do not change fixer behavior with comments
sajikix7c79e8a fix: delete bracket with fixer
sajikixaf57de9 fix: fix test cases and meta data
sajikixf15047d refactor : also use ast node checker
sajikix4571a23 refactor : organize test cases
sajikix5c609c1 Merge branch 'main' into add-rule-no-duplicate-type-union-intersectio…
sajikixaa31c67 fix: fix rule description
sajikixfc9536e fix: modify Rule Details to match implementation
sajikix56da0d4 refactor: add uniq set in each case
sajikix2f2bbad refactor: delete type guard
sajikixf5e6ce3 refactor: add test case
sajikix2d70212 refactor: delete unnecessary comparison logic
sajikixfea730e refactor: add test-case
sajikixf6a9e32 feat: show which the previous type is duplicating
sajikixcc5d3be Merge branch 'main' into add-rule-no-duplicate-type-union-intersectio…
sajikixa1b36e4 fix: use word constituents
sajikix22c60f8 fix: sample case
sajikixee6f9b2 Merge branch 'main' into add-rule-no-duplicate-type-union-intersectio…
sajikix44c52e1 Merge branch 'main' into add-rule-no-duplicate-type-union-intersectio…
sajikixf265116 fix: lint message
sajikix05cc529 fix: rule docs
sajikixb924fdd fix: use === & !==
sajikix3180851 Merge branch 'main' into add-rule-no-duplicate-type-union-intersectio…
sajikixd9489ad fix: No `noFormat` in test.
sajikix17fcdf9 fix: correct examples
sajikix9f6f2aa refactor: use `flatMap`
sajikix0489f17 refactor: Do not use temporary `fixes` variable.
sajikix7b1712b refactor: make type comparison lazy and use cache
sajikix2e00a2c refactor: no unnecessary loop in `fix` function.
sajikix2bbd34d refactor: get logic of tokens to be deleted
sajikix6e7094a Merge branch 'main' into add-rule-no-duplicate-type-union-intersectio…
sajikix2bae913 Merge branch 'main' into add-rule-no-duplicate-type-union-intersectio…
sajikix9e46016 Merge branch 'main' into add-rule-no-duplicate-type-union-intersectio…
sajikix73b66a7 refactor: separate report function and solve fixer range problem
sajikixf161c61 refactor: improved documentation.
sajikix255cc8c fix: make additionalProperties false
sajikixe8d1aa1 fix: delete printing message {{duplicated}}
sajikix47c2b4c fix: do not abbreviate "unique"
sajikix9113663 refactor: reverse the key and value in cachedTypeMap to reduce the am…
sajikixa55e36f fix: reportLocation start
sajikix7a85718 refactor: stop test generation and write tests naively.
sajikixa6b2382 refactor: Narrowing the type of options
sajikix9de38e4 Revert "refactor: Narrowing the type of options"
sajikixabb92d8 refactor: use Set instead of array
sajikixFile filter
Filter by extension
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
61 changes: 61 additions & 0 deletionspackages/eslint-plugin/docs/rules/no-duplicate-type-constituents.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| --- | ||
| description: 'Disallow duplicate constituents of union or intersection types.' | ||
| --- | ||
| > 🛑 This file is source code, not the primary documentation location! 🛑 | ||
| > | ||
| > See **https://typescript-eslint.io/rules/no-duplicate-type-constituents** for documentation. | ||
| TypeScript supports types ("constituents") within union and intersection types being duplicates of each other. | ||
| However, developers typically expect each constituent to be unique within its intersection or union. | ||
| Duplicate values make the code overly verbose and generally reduce readability. | ||
| ## Rule Details | ||
| This rule disallows duplicate union or intersection constituents. | ||
| We consider types to be duplicate if they evaluate to the same result in the type system. | ||
| For example, given `type A = string` and `type T = string | A`, this rule would flag that `A` is the same type as `string`. | ||
| <!--tabs--> | ||
| ### ❌ Incorrect | ||
| ```ts | ||
| type T1 = 'A' | 'A'; | ||
| type T2 = A | A | B; | ||
| type T3 = { a: string } & { a: string }; | ||
| type T4 = [1, 2, 3] | [1, 2, 3]; | ||
| type StringA = string; | ||
| type StringB = string; | ||
| type T5 = StringA | StringB; | ||
| ``` | ||
| ### ✅ Correct | ||
| ```ts | ||
| type T1 = 'A' | 'B'; | ||
| type T2 = A | B | C; | ||
| type T3 = { a: string } & { b: string }; | ||
| type T4 = [1, 2, 3] | [1, 2, 3, 4]; | ||
| type StringA = string; | ||
| type NumberB = number; | ||
| type T5 = StringA | NumberB; | ||
| ``` | ||
| ## Options | ||
| ### `ignoreIntersections` | ||
| When set to true, duplicate checks on intersection type constituents are ignored. | ||
| ### `ignoreUnions` | ||
| When set to true, duplicate checks on union type constituents are ignored. |
1 change: 1 addition & 0 deletionspackages/eslint-plugin/src/configs/all.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
2 changes: 2 additions & 0 deletionspackages/eslint-plugin/src/rules/index.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
207 changes: 207 additions & 0 deletionspackages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,207 @@ | ||
| import type { TSESTree } from '@typescript-eslint/utils'; | ||
| import { AST_NODE_TYPES } from '@typescript-eslint/utils'; | ||
| import type { Type } from 'typescript'; | ||
| import * as util from '../util'; | ||
| export type Options = [ | ||
| { | ||
| ignoreIntersections?: boolean; | ||
| ignoreUnions?: boolean; | ||
| }, | ||
| ]; | ||
| export type MessageIds = 'duplicate'; | ||
| const astIgnoreKeys = new Set(['range', 'loc', 'parent']); | ||
| const isSameAstNode = (actualNode: unknown, expectedNode: unknown): boolean => { | ||
| if (actualNode === expectedNode) { | ||
| return true; | ||
| } | ||
| if ( | ||
| actualNode && | ||
| expectedNode && | ||
| typeof actualNode === 'object' && | ||
| typeof expectedNode === 'object' | ||
| ) { | ||
| if (Array.isArray(actualNode) && Array.isArray(expectedNode)) { | ||
| if (actualNode.length !== expectedNode.length) { | ||
| return false; | ||
| } | ||
| return !actualNode.some( | ||
| (nodeEle, index) => !isSameAstNode(nodeEle, expectedNode[index]), | ||
| ); | ||
| } | ||
| const actualNodeKeys = Object.keys(actualNode).filter( | ||
| key => !astIgnoreKeys.has(key), | ||
| ); | ||
| const expectedNodeKeys = Object.keys(expectedNode).filter( | ||
| key => !astIgnoreKeys.has(key), | ||
| ); | ||
| if (actualNodeKeys.length !== expectedNodeKeys.length) { | ||
| return false; | ||
| } | ||
| if ( | ||
| actualNodeKeys.some( | ||
| actualNodeKey => | ||
| !Object.prototype.hasOwnProperty.call(expectedNode, actualNodeKey), | ||
| ) | ||
| ) { | ||
| return false; | ||
| } | ||
| if ( | ||
| actualNodeKeys.some( | ||
| actualNodeKey => | ||
| !isSameAstNode( | ||
| actualNode[actualNodeKey as keyof typeof actualNode], | ||
| expectedNode[actualNodeKey as keyof typeof expectedNode], | ||
| ), | ||
| ) | ||
| ) { | ||
| return false; | ||
| } | ||
| return true; | ||
| } | ||
| return false; | ||
| }; | ||
JoshuaKGoldberg marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
| export default util.createRule<Options, MessageIds>({ | ||
| name: 'no-duplicate-type-constituents', | ||
| meta: { | ||
| type: 'suggestion', | ||
| docs: { | ||
| description: | ||
| 'Disallow duplicate constituents of union or intersection types', | ||
| recommended: false, | ||
| requiresTypeChecking: true, | ||
| }, | ||
| fixable: 'code', | ||
| messages: { | ||
| duplicate: '{{type}} type constituent is duplicated with {{previous}}.', | ||
| }, | ||
| schema: [ | ||
| { | ||
| additionalProperties: false, | ||
| type: 'object', | ||
JoshuaKGoldberg marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
| properties: { | ||
| ignoreIntersections: { | ||
| type: 'boolean', | ||
| }, | ||
| ignoreUnions: { | ||
| type: 'boolean', | ||
| }, | ||
| }, | ||
| }, | ||
| ], | ||
| }, | ||
| defaultOptions: [ | ||
| { | ||
| ignoreIntersections: false, | ||
| ignoreUnions: false, | ||
| }, | ||
| ], | ||
| create(context, [{ ignoreIntersections, ignoreUnions }]) { | ||
| const parserServices = util.getParserServices(context); | ||
| const checker = parserServices.program.getTypeChecker(); | ||
| function checkDuplicate( | ||
| node: TSESTree.TSIntersectionType | TSESTree.TSUnionType, | ||
| ): void { | ||
| const cachedTypeMap: Map<Type, TSESTree.TypeNode> = new Map(); | ||
| node.types.reduce<TSESTree.TypeNode[]>( | ||
| (uniqueConstituents, constituentNode) => { | ||
| const duplicatedPreviousConstituentInAst = uniqueConstituents.find( | ||
| ele => isSameAstNode(ele, constituentNode), | ||
| ); | ||
| if (duplicatedPreviousConstituentInAst) { | ||
| reportDuplicate( | ||
| { | ||
| duplicated: constituentNode, | ||
| duplicatePrevious: duplicatedPreviousConstituentInAst, | ||
| }, | ||
| node, | ||
| ); | ||
| return uniqueConstituents; | ||
| } | ||
| const constituentNodeType = checker.getTypeAtLocation( | ||
| parserServices.esTreeNodeToTSNodeMap.get(constituentNode), | ||
| ); | ||
| const duplicatedPreviousConstituentInType = | ||
| cachedTypeMap.get(constituentNodeType); | ||
| if (duplicatedPreviousConstituentInType) { | ||
| reportDuplicate( | ||
| { | ||
| duplicated: constituentNode, | ||
| duplicatePrevious: duplicatedPreviousConstituentInType, | ||
| }, | ||
| node, | ||
| ); | ||
| return uniqueConstituents; | ||
| } | ||
| cachedTypeMap.set(constituentNodeType, constituentNode); | ||
| return [...uniqueConstituents, constituentNode]; | ||
| }, | ||
| [], | ||
| ); | ||
| } | ||
| function reportDuplicate( | ||
| duplicateConstituent: { | ||
| duplicated: TSESTree.TypeNode; | ||
| duplicatePrevious: TSESTree.TypeNode; | ||
| }, | ||
| parentNode: TSESTree.TSIntersectionType | TSESTree.TSUnionType, | ||
| ): void { | ||
| const sourceCode = context.getSourceCode(); | ||
| const beforeTokens = sourceCode.getTokensBefore( | ||
| duplicateConstituent.duplicated, | ||
| { filter: token => token.value === '|' || token.value === '&' }, | ||
| ); | ||
| const beforeUnionOrIntersectionToken = | ||
| beforeTokens[beforeTokens.length - 1]; | ||
| const bracketBeforeTokens = sourceCode.getTokensBetween( | ||
| beforeUnionOrIntersectionToken, | ||
| duplicateConstituent.duplicated, | ||
| ); | ||
| const bracketAfterTokens = sourceCode.getTokensAfter( | ||
| duplicateConstituent.duplicated, | ||
| { count: bracketBeforeTokens.length }, | ||
| ); | ||
| const reportLocation: TSESTree.SourceLocation = { | ||
| start: duplicateConstituent.duplicated.loc.start, | ||
| end: | ||
| bracketAfterTokens.length > 0 | ||
| ? bracketAfterTokens[bracketAfterTokens.length - 1].loc.end | ||
| : duplicateConstituent.duplicated.loc.end, | ||
| }; | ||
| context.report({ | ||
| data: { | ||
| type: | ||
| parentNode.type === AST_NODE_TYPES.TSIntersectionType | ||
| ? 'Intersection' | ||
| : 'Union', | ||
| previous: sourceCode.getText(duplicateConstituent.duplicatePrevious), | ||
| }, | ||
| messageId: 'duplicate', | ||
| node: duplicateConstituent.duplicated, | ||
| loc: reportLocation, | ||
| fix: fixer => { | ||
| return [ | ||
| beforeUnionOrIntersectionToken, | ||
| ...bracketBeforeTokens, | ||
| duplicateConstituent.duplicated, | ||
| ...bracketAfterTokens, | ||
| ].map(token => fixer.remove(token)); | ||
| }, | ||
| }); | ||
| } | ||
| return { | ||
| ...(!ignoreIntersections && { | ||
| TSIntersectionType: checkDuplicate, | ||
| }), | ||
| ...(!ignoreUnions && { | ||
| TSUnionType: checkDuplicate, | ||
| }), | ||
| }; | ||
| }, | ||
| }); | ||
Oops, something went wrong.
Uh oh!
There was an error while loading.Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.