Skip to content

Commit 9e8828b

Browse files
authored
fix(eslint-plugin): [no-duplicate-type-constituents] handle nested types (#10638)
* fix(eslint-plugin): [no-duplicate-type-constituents] handle nested types * fix * Update no-duplicate-type-constituents.ts * fix naming
1 parent 4f3f8cf commit 9e8828b

File tree

2 files changed

+264
-104
lines changed

2 files changed

+264
-104
lines changed

‎packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts

Lines changed: 143 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export type Options = [
2222

2323
export type MessageIds = 'duplicate' | 'unnecessary';
2424

25+
type UnionOrIntersection = 'Intersection' | 'Union';
26+
2527
const astIgnoreKeys = new Set(['loc', 'parent', 'range']);
2628

2729
const isSameAstNode = (actualNode: unknown, expectedNode: unknown): boolean => {
@@ -117,115 +119,158 @@ export default createRule<Options, MessageIds>({
117119
const parserServices = getParserServices(context);
118120
const { sourceCode } = context;
119121

122+
function report(
123+
messageId: MessageIds,
124+
constituentNode: TSESTree.TypeNode,
125+
data?: Record<string, unknown>,
126+
): void {
127+
const getUnionOrIntersectionToken = (
128+
where: 'After' | 'Before',
129+
at: number,
130+
): TSESTree.Token | undefined =>
131+
sourceCode[`getTokens${where}`](constituentNode, {
132+
filter: token =>
133+
['&', '|'].includes(token.value) &&
134+
constituentNode.parent.range[0] <= token.range[0] &&
135+
token.range[1] <= constituentNode.parent.range[1],
136+
}).at(at);
137+
138+
const beforeUnionOrIntersectionToken = getUnionOrIntersectionToken(
139+
'Before',
140+
-1,
141+
);
142+
let afterUnionOrIntersectionToken: TSESTree.Token | undefined;
143+
let bracketBeforeTokens;
144+
let bracketAfterTokens;
145+
if (beforeUnionOrIntersectionToken) {
146+
bracketBeforeTokens = sourceCode.getTokensBetween(
147+
beforeUnionOrIntersectionToken,
148+
constituentNode,
149+
);
150+
bracketAfterTokens = sourceCode.getTokensAfter(constituentNode, {
151+
count: bracketBeforeTokens.length,
152+
});
153+
} else {
154+
afterUnionOrIntersectionToken = nullThrows(
155+
getUnionOrIntersectionToken('After', 0),
156+
NullThrowsReasons.MissingToken(
157+
'union or intersection token',
158+
'duplicate type constituent',
159+
),
160+
);
161+
bracketAfterTokens = sourceCode.getTokensBetween(
162+
constituentNode,
163+
afterUnionOrIntersectionToken,
164+
);
165+
bracketBeforeTokens = sourceCode.getTokensBefore(constituentNode, {
166+
count: bracketAfterTokens.length,
167+
});
168+
}
169+
context.report({
170+
loc: {
171+
start: constituentNode.loc.start,
172+
end: (bracketAfterTokens.at(-1) ?? constituentNode).loc.end,
173+
},
174+
node: constituentNode,
175+
messageId,
176+
data,
177+
fix: fixer =>
178+
[
179+
beforeUnionOrIntersectionToken,
180+
...bracketBeforeTokens,
181+
constituentNode,
182+
...bracketAfterTokens,
183+
afterUnionOrIntersectionToken,
184+
].flatMap(token => (token ? fixer.remove(token) : [])),
185+
});
186+
}
187+
188+
function checkDuplicateRecursively(
189+
unionOrIntersection: UnionOrIntersection,
190+
constituentNode: TSESTree.TypeNode,
191+
uniqueConstituents: TSESTree.TypeNode[],
192+
cachedTypeMap: Map<Type, TSESTree.TypeNode>,
193+
forEachNodeType?: (type: Type, node: TSESTree.TypeNode) => void,
194+
): void {
195+
const type = parserServices.getTypeAtLocation(constituentNode);
196+
if (tsutils.isIntrinsicErrorType(type)) {
197+
return;
198+
}
199+
const duplicatedPrevious =
200+
uniqueConstituents.find(ele => isSameAstNode(ele, constituentNode)) ??
201+
cachedTypeMap.get(type);
202+
203+
if (duplicatedPrevious) {
204+
report('duplicate', constituentNode, {
205+
type: unionOrIntersection,
206+
previous: sourceCode.getText(duplicatedPrevious),
207+
});
208+
return;
209+
}
210+
211+
forEachNodeType?.(type, constituentNode);
212+
cachedTypeMap.set(type, constituentNode);
213+
uniqueConstituents.push(constituentNode);
214+
215+
if (
216+
(unionOrIntersection === 'Union' &&
217+
constituentNode.type === AST_NODE_TYPES.TSUnionType) ||
218+
(unionOrIntersection === 'Intersection' &&
219+
constituentNode.type === AST_NODE_TYPES.TSIntersectionType)
220+
) {
221+
for (const constituent of constituentNode.types) {
222+
checkDuplicateRecursively(
223+
unionOrIntersection,
224+
constituent,
225+
uniqueConstituents,
226+
cachedTypeMap,
227+
forEachNodeType,
228+
);
229+
}
230+
}
231+
}
232+
120233
function checkDuplicate(
121234
node: TSESTree.TSIntersectionType | TSESTree.TSUnionType,
122235
forEachNodeType?: (
123236
constituentNodeType: Type,
124-
report: (messageId: MessageIds) => void,
237+
constituentNode: TSESTree.TypeNode,
125238
) => void,
126239
): void {
127240
const cachedTypeMap = new Map<Type, TSESTree.TypeNode>();
128-
node.types.reduce<TSESTree.TypeNode[]>(
129-
(uniqueConstituents, constituentNode) => {
130-
const constituentNodeType =
131-
parserServices.getTypeAtLocation(constituentNode);
132-
if (tsutils.isIntrinsicErrorType(constituentNodeType)) {
133-
return uniqueConstituents;
134-
}
241+
const uniqueConstituents: TSESTree.TypeNode[] = [];
135242

136-
const report = (
137-
messageId: MessageIds,
138-
data?: Record<string, unknown>,
139-
): void => {
140-
const getUnionOrIntersectionToken = (
141-
where: 'After' | 'Before',
142-
at: number,
143-
): TSESTree.Token | undefined =>
144-
sourceCode[`getTokens${where}`](constituentNode, {
145-
filter: token => ['&', '|'].includes(token.value),
146-
}).at(at);
147-
148-
const beforeUnionOrIntersectionToken = getUnionOrIntersectionToken(
149-
'Before',
150-
-1,
151-
);
152-
let afterUnionOrIntersectionToken: TSESTree.Token | undefined;
153-
let bracketBeforeTokens;
154-
let bracketAfterTokens;
155-
if (beforeUnionOrIntersectionToken) {
156-
bracketBeforeTokens = sourceCode.getTokensBetween(
157-
beforeUnionOrIntersectionToken,
158-
constituentNode,
159-
);
160-
bracketAfterTokens = sourceCode.getTokensAfter(constituentNode, {
161-
count: bracketBeforeTokens.length,
162-
});
163-
} else {
164-
afterUnionOrIntersectionToken = nullThrows(
165-
getUnionOrIntersectionToken('After', 0),
166-
NullThrowsReasons.MissingToken(
167-
'union or intersection token',
168-
'duplicate type constituent',
169-
),
170-
);
171-
bracketAfterTokens = sourceCode.getTokensBetween(
172-
constituentNode,
173-
afterUnionOrIntersectionToken,
174-
);
175-
bracketBeforeTokens = sourceCode.getTokensBefore(
176-
constituentNode,
177-
{
178-
count: bracketAfterTokens.length,
179-
},
180-
);
181-
}
182-
context.report({
183-
loc: {
184-
start: constituentNode.loc.start,
185-
end: (bracketAfterTokens.at(-1) ?? constituentNode).loc.end,
186-
},
187-
node: constituentNode,
188-
messageId,
189-
data,
190-
fix: fixer =>
191-
[
192-
beforeUnionOrIntersectionToken,
193-
...bracketBeforeTokens,
194-
constituentNode,
195-
...bracketAfterTokens,
196-
afterUnionOrIntersectionToken,
197-
].flatMap(token => (token ? fixer.remove(token) : [])),
198-
});
199-
};
200-
const duplicatePrevious =
201-
uniqueConstituents.find(ele =>
202-
isSameAstNode(ele, constituentNode),
203-
) ?? cachedTypeMap.get(constituentNodeType);
204-
if (duplicatePrevious) {
205-
report('duplicate', {
206-
type:
207-
node.type === AST_NODE_TYPES.TSIntersectionType
208-
? 'Intersection'
209-
: 'Union',
210-
previous: sourceCode.getText(duplicatePrevious),
211-
});
212-
return uniqueConstituents;
213-
}
214-
forEachNodeType?.(constituentNodeType, report);
215-
cachedTypeMap.set(constituentNodeType, constituentNode);
216-
return [...uniqueConstituents, constituentNode];
217-
},
218-
[],
219-
);
243+
const unionOrIntersection =
244+
node.type === AST_NODE_TYPES.TSIntersectionType
245+
? 'Intersection'
246+
: 'Union';
247+
248+
for (const type of node.types) {
249+
checkDuplicateRecursively(
250+
unionOrIntersection,
251+
type,
252+
uniqueConstituents,
253+
cachedTypeMap,
254+
forEachNodeType,
255+
);
256+
}
220257
}
221258

222259
return {
223260
...(!ignoreIntersections && {
224-
TSIntersectionType: checkDuplicate,
261+
TSIntersectionType(node) {
262+
if (node.parent.type === AST_NODE_TYPES.TSIntersectionType) {
263+
return;
264+
}
265+
checkDuplicate(node);
266+
},
225267
}),
226268
...(!ignoreUnions && {
227-
TSUnionType: (node): void =>
228-
checkDuplicate(node, (constituentNodeType, report) => {
269+
TSUnionType: (node): void => {
270+
if (node.parent.type === AST_NODE_TYPES.TSUnionType) {
271+
return;
272+
}
273+
checkDuplicate(node, (constituentNodeType, constituentNode) => {
229274
const maybeTypeAnnotation = node.parent;
230275
if (maybeTypeAnnotation.type === AST_NODE_TYPES.TSTypeAnnotation) {
231276
const maybeIdentifier = maybeTypeAnnotation.parent;
@@ -242,11 +287,12 @@ export default createRule<Options, MessageIds>({
242287
ts.TypeFlags.Undefined,
243288
)
244289
) {
245-
report('unnecessary');
290+
report('unnecessary', constituentNode);
246291
}
247292
}
248293
}
249-
}),
294+
});
295+
},
250296
}),
251297
};
252298
},

0 commit comments

Comments
 (0)