Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit bc31078

Browse files
authored
feat(eslint-plugin): add no-duplicate-type-constituents rule (typescript-eslint#5728)
* feat: add rule code * test: add test for rule * docs: add docs of new rule * refactor: make method definitions more concise * fix: change check option to ignore option * refactor: rename to type-constituents * refactor: use recursive type-node checker * fix: rename doc filename and test title * refactor: use removeRage instead of replaceText * refactor: narrows node comparison function argument type * fix: doc description * refactor: update hasComments logic * fix: remove cases that never occur * refactor: use type checker * fix: do not change fixer behavior with comments * fix: delete bracket with fixer * fix: fix test cases and meta data * refactor : also use ast node checker * refactor : organize test cases * fix: fix rule description * fix: modify Rule Details to match implementation * refactor: add uniq set in each case * refactor: delete type guard * refactor: add test case * refactor: delete unnecessary comparison logic * refactor: add test-case * feat: show which the previous type is duplicating * fix: use word constituents * fix: sample case * fix: lint message * fix: rule docs * fix: use === & !== * fix: No `noFormat` in test. * fix: correct examples * refactor: use `flatMap` * refactor: Do not use temporary `fixes` variable. * refactor: make type comparison lazy and use cache * refactor: no unnecessary loop in `fix` function. * refactor: get logic of tokens to be deleted * refactor: separate report function and solve fixer range problem * refactor: improved documentation. * fix: make additionalProperties false * fix: delete printing message {{duplicated}} * fix: do not abbreviate "unique" * refactor: reverse the key and value in cachedTypeMap to reduce the amount of calculation. * fix: reportLocation start * refactor: stop test generation and write tests naively. * refactor: Narrowing the type of options * Revert "refactor: Narrowing the type of options" This reverts commit a6b2382. * refactor: use Set instead of array
1 parent 9f2122d commit bc31078

File tree

5 files changed

+937
-0
lines changed

5 files changed

+937
-0
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
description: 'Disallow duplicate constituents of union or intersection types.'
3+
---
4+
5+
> 🛑 This file is source code, not the primary documentation location! 🛑
6+
>
7+
> See **https://typescript-eslint.io/rules/no-duplicate-type-constituents** for documentation.
8+
9+
TypeScript supports types ("constituents") within union and intersection types being duplicates of each other.
10+
However, developers typically expect each constituent to be unique within its intersection or union.
11+
Duplicate values make the code overly verbose and generally reduce readability.
12+
13+
## Rule Details
14+
15+
This rule disallows duplicate union or intersection constituents.
16+
We consider types to be duplicate if they evaluate to the same result in the type system.
17+
For example, given `type A = string` and `type T = string | A`, this rule would flag that `A` is the same type as `string`.
18+
19+
<!--tabs-->
20+
21+
### ❌ Incorrect
22+
23+
```ts
24+
type T1 = 'A' | 'A';
25+
26+
type T2 = A | A | B;
27+
28+
type T3 = { a: string } & { a: string };
29+
30+
type T4 = [1, 2, 3] | [1, 2, 3];
31+
32+
type StringA = string;
33+
type StringB = string;
34+
type T5 = StringA | StringB;
35+
```
36+
37+
### ✅ Correct
38+
39+
```ts
40+
type T1 = 'A' | 'B';
41+
42+
type T2 = A | B | C;
43+
44+
type T3 = { a: string } & { b: string };
45+
46+
type T4 = [1, 2, 3] | [1, 2, 3, 4];
47+
48+
type StringA = string;
49+
type NumberB = number;
50+
type T5 = StringA | NumberB;
51+
```
52+
53+
## Options
54+
55+
### `ignoreIntersections`
56+
57+
When set to true, duplicate checks on intersection type constituents are ignored.
58+
59+
### `ignoreUnions`
60+
61+
When set to true, duplicate checks on union type constituents are ignored.

packages/eslint-plugin/src/configs/all.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export = {
5959
'no-dupe-class-members': 'off',
6060
'@typescript-eslint/no-dupe-class-members': 'error',
6161
'@typescript-eslint/no-duplicate-enum-values': 'error',
62+
'@typescript-eslint/no-duplicate-type-constituents': 'error',
6263
'@typescript-eslint/no-dynamic-delete': 'error',
6364
'no-empty-function': 'off',
6465
'@typescript-eslint/no-empty-function': 'error',

packages/eslint-plugin/src/rules/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import noConfusingVoidExpression from './no-confusing-void-expression';
3838
import noDupeClassMembers from './no-dupe-class-members';
3939
import noDuplicateEnumValues from './no-duplicate-enum-values';
4040
import noDuplicateImports from './no-duplicate-imports';
41+
import noDuplicateTypeConstituents from './no-duplicate-type-constituents';
4142
import noDynamicDelete from './no-dynamic-delete';
4243
import noEmptyFunction from './no-empty-function';
4344
import noEmptyInterface from './no-empty-interface';
@@ -174,6 +175,7 @@ export default {
174175
'no-dupe-class-members': noDupeClassMembers,
175176
'no-duplicate-enum-values': noDuplicateEnumValues,
176177
'no-duplicate-imports': noDuplicateImports,
178+
'no-duplicate-type-constituents': noDuplicateTypeConstituents,
177179
'no-dynamic-delete': noDynamicDelete,
178180
'no-empty-function': noEmptyFunction,
179181
'no-empty-interface': noEmptyInterface,
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import type { TSESTree } from '@typescript-eslint/utils';
2+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
3+
import type { Type } from 'typescript';
4+
5+
import * as util from '../util';
6+
7+
export type Options = [
8+
{
9+
ignoreIntersections?: boolean;
10+
ignoreUnions?: boolean;
11+
},
12+
];
13+
14+
export type MessageIds = 'duplicate';
15+
16+
const astIgnoreKeys = new Set(['range', 'loc', 'parent']);
17+
18+
const isSameAstNode = (actualNode: unknown, expectedNode: unknown): boolean => {
19+
if (actualNode === expectedNode) {
20+
return true;
21+
}
22+
if (
23+
actualNode &&
24+
expectedNode &&
25+
typeof actualNode === 'object' &&
26+
typeof expectedNode === 'object'
27+
) {
28+
if (Array.isArray(actualNode) && Array.isArray(expectedNode)) {
29+
if (actualNode.length !== expectedNode.length) {
30+
return false;
31+
}
32+
return !actualNode.some(
33+
(nodeEle, index) => !isSameAstNode(nodeEle, expectedNode[index]),
34+
);
35+
}
36+
const actualNodeKeys = Object.keys(actualNode).filter(
37+
key => !astIgnoreKeys.has(key),
38+
);
39+
const expectedNodeKeys = Object.keys(expectedNode).filter(
40+
key => !astIgnoreKeys.has(key),
41+
);
42+
if (actualNodeKeys.length !== expectedNodeKeys.length) {
43+
return false;
44+
}
45+
if (
46+
actualNodeKeys.some(
47+
actualNodeKey =>
48+
!Object.prototype.hasOwnProperty.call(expectedNode, actualNodeKey),
49+
)
50+
) {
51+
return false;
52+
}
53+
if (
54+
actualNodeKeys.some(
55+
actualNodeKey =>
56+
!isSameAstNode(
57+
actualNode[actualNodeKey as keyof typeof actualNode],
58+
expectedNode[actualNodeKey as keyof typeof expectedNode],
59+
),
60+
)
61+
) {
62+
return false;
63+
}
64+
return true;
65+
}
66+
return false;
67+
};
68+
69+
export default util.createRule<Options, MessageIds>({
70+
name: 'no-duplicate-type-constituents',
71+
meta: {
72+
type: 'suggestion',
73+
docs: {
74+
description:
75+
'Disallow duplicate constituents of union or intersection types',
76+
recommended: false,
77+
requiresTypeChecking: true,
78+
},
79+
fixable: 'code',
80+
messages: {
81+
duplicate: '{{type}} type constituent is duplicated with {{previous}}.',
82+
},
83+
schema: [
84+
{
85+
additionalProperties: false,
86+
type: 'object',
87+
properties: {
88+
ignoreIntersections: {
89+
type: 'boolean',
90+
},
91+
ignoreUnions: {
92+
type: 'boolean',
93+
},
94+
},
95+
},
96+
],
97+
},
98+
defaultOptions: [
99+
{
100+
ignoreIntersections: false,
101+
ignoreUnions: false,
102+
},
103+
],
104+
create(context, [{ ignoreIntersections, ignoreUnions }]) {
105+
const parserServices = util.getParserServices(context);
106+
const checker = parserServices.program.getTypeChecker();
107+
108+
function checkDuplicate(
109+
node: TSESTree.TSIntersectionType | TSESTree.TSUnionType,
110+
): void {
111+
const cachedTypeMap: Map<Type, TSESTree.TypeNode> = new Map();
112+
node.types.reduce<TSESTree.TypeNode[]>(
113+
(uniqueConstituents, constituentNode) => {
114+
const duplicatedPreviousConstituentInAst = uniqueConstituents.find(
115+
ele => isSameAstNode(ele, constituentNode),
116+
);
117+
if (duplicatedPreviousConstituentInAst) {
118+
reportDuplicate(
119+
{
120+
duplicated: constituentNode,
121+
duplicatePrevious: duplicatedPreviousConstituentInAst,
122+
},
123+
node,
124+
);
125+
return uniqueConstituents;
126+
}
127+
const constituentNodeType = checker.getTypeAtLocation(
128+
parserServices.esTreeNodeToTSNodeMap.get(constituentNode),
129+
);
130+
const duplicatedPreviousConstituentInType =
131+
cachedTypeMap.get(constituentNodeType);
132+
if (duplicatedPreviousConstituentInType) {
133+
reportDuplicate(
134+
{
135+
duplicated: constituentNode,
136+
duplicatePrevious: duplicatedPreviousConstituentInType,
137+
},
138+
node,
139+
);
140+
return uniqueConstituents;
141+
}
142+
cachedTypeMap.set(constituentNodeType, constituentNode);
143+
return [...uniqueConstituents, constituentNode];
144+
},
145+
[],
146+
);
147+
}
148+
function reportDuplicate(
149+
duplicateConstituent: {
150+
duplicated: TSESTree.TypeNode;
151+
duplicatePrevious: TSESTree.TypeNode;
152+
},
153+
parentNode: TSESTree.TSIntersectionType | TSESTree.TSUnionType,
154+
): void {
155+
const sourceCode = context.getSourceCode();
156+
const beforeTokens = sourceCode.getTokensBefore(
157+
duplicateConstituent.duplicated,
158+
{ filter: token => token.value === '|' || token.value === '&' },
159+
);
160+
const beforeUnionOrIntersectionToken =
161+
beforeTokens[beforeTokens.length - 1];
162+
const bracketBeforeTokens = sourceCode.getTokensBetween(
163+
beforeUnionOrIntersectionToken,
164+
duplicateConstituent.duplicated,
165+
);
166+
const bracketAfterTokens = sourceCode.getTokensAfter(
167+
duplicateConstituent.duplicated,
168+
{ count: bracketBeforeTokens.length },
169+
);
170+
const reportLocation: TSESTree.SourceLocation = {
171+
start: duplicateConstituent.duplicated.loc.start,
172+
end:
173+
bracketAfterTokens.length > 0
174+
? bracketAfterTokens[bracketAfterTokens.length - 1].loc.end
175+
: duplicateConstituent.duplicated.loc.end,
176+
};
177+
context.report({
178+
data: {
179+
type:
180+
parentNode.type === AST_NODE_TYPES.TSIntersectionType
181+
? 'Intersection'
182+
: 'Union',
183+
previous: sourceCode.getText(duplicateConstituent.duplicatePrevious),
184+
},
185+
messageId: 'duplicate',
186+
node: duplicateConstituent.duplicated,
187+
loc: reportLocation,
188+
fix: fixer => {
189+
return [
190+
beforeUnionOrIntersectionToken,
191+
...bracketBeforeTokens,
192+
duplicateConstituent.duplicated,
193+
...bracketAfterTokens,
194+
].map(token => fixer.remove(token));
195+
},
196+
});
197+
}
198+
return {
199+
...(!ignoreIntersections && {
200+
TSIntersectionType: checkDuplicate,
201+
}),
202+
...(!ignoreUnions && {
203+
TSUnionType: checkDuplicate,
204+
}),
205+
};
206+
},
207+
});

0 commit comments

Comments
 (0)