-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
feat(eslint-plugin): add no-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 our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
JoshuaKGoldberg
merged 61 commits into
typescript-eslint:main
from
sajikix:add-rule-no-duplicate-type-union-intersection-members
Mar 24, 2023
Merged
Changes from all commits
Commits
Show all changes
61 commits
Select commit
Hold shift + click to select a range
6ee8df7
feat: add rule code
sajikix 90b5e96
test: add test for rule
sajikix 1a3a620
docs: add docs of new rule
sajikix 29874a8
refactor: make method definitions more concise
sajikix e03a7e8
fix: change check option to ignore option
sajikix 95afec8
refactor: rename to type-constituents
sajikix 1e7cbb1
refactor: use recursive type-node checker
sajikix 3c67cef
Merge branch 'main' into add-rule-no-duplicate-type-union-intersectio…
sajikix 39f18bc
fix: rename doc filename and test title
sajikix f430871
refactor: use removeRage instead of replaceText
sajikix 49e27be
refactor: narrows node comparison function argument type
sajikix b368abb
fix: doc description
sajikix 4455b49
Merge branch 'main' into add-rule-no-duplicate-type-union-intersectio…
sajikix 98a008c
Merge branch 'main' into add-rule-no-duplicate-type-union-intersectio…
sajikix f1f507d
refactor: update hasComments logic
sajikix 9e75e07
fix: remove cases that never occur
sajikix bec198c
refactor: use type checker
sajikix 71afc2e
fix: do not change fixer behavior with comments
sajikix 7c79e8a
fix: delete bracket with fixer
sajikix af57de9
fix: fix test cases and meta data
sajikix f15047d
refactor : also use ast node checker
sajikix 4571a23
refactor : organize test cases
sajikix 5c609c1
Merge branch 'main' into add-rule-no-duplicate-type-union-intersectio…
sajikix aa31c67
fix: fix rule description
sajikix fc9536e
fix: modify Rule Details to match implementation
sajikix 56da0d4
refactor: add uniq set in each case
sajikix 2f2bbad
refactor: delete type guard
sajikix f5e6ce3
refactor: add test case
sajikix 2d70212
refactor: delete unnecessary comparison logic
sajikix fea730e
refactor: add test-case
sajikix f6a9e32
feat: show which the previous type is duplicating
sajikix cc5d3be
Merge branch 'main' into add-rule-no-duplicate-type-union-intersectio…
sajikix a1b36e4
fix: use word constituents
sajikix 22c60f8
fix: sample case
sajikix ee6f9b2
Merge branch 'main' into add-rule-no-duplicate-type-union-intersectio…
sajikix 44c52e1
Merge branch 'main' into add-rule-no-duplicate-type-union-intersectio…
sajikix f265116
fix: lint message
sajikix 05cc529
fix: rule docs
sajikix b924fdd
fix: use === & !==
sajikix 3180851
Merge branch 'main' into add-rule-no-duplicate-type-union-intersectio…
sajikix d9489ad
fix: No `noFormat` in test.
sajikix 17fcdf9
fix: correct examples
sajikix 9f6f2aa
refactor: use `flatMap`
sajikix 0489f17
refactor: Do not use temporary `fixes` variable.
sajikix 7b1712b
refactor: make type comparison lazy and use cache
sajikix 2e00a2c
refactor: no unnecessary loop in `fix` function.
sajikix 2bbd34d
refactor: get logic of tokens to be deleted
sajikix 6e7094a
Merge branch 'main' into add-rule-no-duplicate-type-union-intersectio…
sajikix 2bae913
Merge branch 'main' into add-rule-no-duplicate-type-union-intersectio…
sajikix 9e46016
Merge branch 'main' into add-rule-no-duplicate-type-union-intersectio…
sajikix 73b66a7
refactor: separate report function and solve fixer range problem
sajikix f161c61
refactor: improved documentation.
sajikix 255cc8c
fix: make additionalProperties false
sajikix e8d1aa1
fix: delete printing message {{duplicated}}
sajikix 47c2b4c
fix: do not abbreviate "unique"
sajikix 9113663
refactor: reverse the key and value in cachedTypeMap to reduce the am…
sajikix a55e36f
fix: reportLocation start
sajikix 7a85718
refactor: stop test generation and write tests naively.
sajikix a6b2382
refactor: Narrowing the type of options
sajikix 9de38e4
Revert "refactor: Narrowing the type of options"
sajikix abb92d8
refactor: use Set instead of array
sajikix File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
61 changes: 61 additions & 0 deletions
61
packages/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. |
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
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 deletions
207
packages/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; | ||
}; | ||
|
||
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 resolved
Hide resolved
|
||
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.
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.
Uh oh!
There was an error while loading. Please reload this page.