@@ -22,6 +22,8 @@ export type Options = [
22
22
23
23
export type MessageIds = 'duplicate' | 'unnecessary' ;
24
24
25
+ type UnionOrIntersection = 'Intersection' | 'Union' ;
26
+
25
27
const astIgnoreKeys = new Set ( [ 'loc' , 'parent' , 'range' ] ) ;
26
28
27
29
const isSameAstNode = ( actualNode : unknown , expectedNode : unknown ) : boolean => {
@@ -117,115 +119,158 @@ export default createRule<Options, MessageIds>({
117
119
const parserServices = getParserServices ( context ) ;
118
120
const { sourceCode } = context ;
119
121
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
+
120
233
function checkDuplicate (
121
234
node : TSESTree . TSIntersectionType | TSESTree . TSUnionType ,
122
235
forEachNodeType ?: (
123
236
constituentNodeType : Type ,
124
- report : ( messageId : MessageIds ) => void ,
237
+ constituentNode : TSESTree . TypeNode ,
125
238
) => void ,
126
239
) : void {
127
240
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 [ ] = [ ] ;
135
242
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
+ }
220
257
}
221
258
222
259
return {
223
260
...( ! ignoreIntersections && {
224
- TSIntersectionType : checkDuplicate ,
261
+ TSIntersectionType ( node ) {
262
+ if ( node . parent . type === AST_NODE_TYPES . TSIntersectionType ) {
263
+ return ;
264
+ }
265
+ checkDuplicate ( node ) ;
266
+ } ,
225
267
} ) ,
226
268
...( ! 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 ) => {
229
274
const maybeTypeAnnotation = node . parent ;
230
275
if ( maybeTypeAnnotation . type === AST_NODE_TYPES . TSTypeAnnotation ) {
231
276
const maybeIdentifier = maybeTypeAnnotation . parent ;
@@ -242,11 +287,12 @@ export default createRule<Options, MessageIds>({
242
287
ts . TypeFlags . Undefined ,
243
288
)
244
289
) {
245
- report ( 'unnecessary' ) ;
290
+ report ( 'unnecessary' , constituentNode ) ;
246
291
}
247
292
}
248
293
}
249
- } ) ,
294
+ } ) ;
295
+ } ,
250
296
} ) ,
251
297
} ;
252
298
} ,
0 commit comments