@@ -14,9 +14,8 @@ import {
14
14
15
15
interface SwitchMetadata {
16
16
readonly symbolName : string | undefined ;
17
- readonly missingBranchTypes : ts . Type [ ] ;
18
17
readonly defaultCase : TSESTree . SwitchCase | undefined ;
19
- readonly isUnion : boolean ;
18
+ readonly missingLiteralBranchTypes : ts . Type [ ] ;
20
19
readonly containsNonLiteralType : boolean ;
21
20
}
22
21
@@ -109,16 +108,6 @@ export default createRule<Options, MessageIds>({
109
108
const containsNonLiteralType =
110
109
doesTypeContainNonLiteralType ( discriminantType ) ;
111
110
112
- if ( ! discriminantType . isUnion ( ) ) {
113
- return {
114
- symbolName,
115
- missingBranchTypes : [ ] ,
116
- defaultCase,
117
- isUnion : false ,
118
- containsNonLiteralType,
119
- } ;
120
- }
121
-
122
111
const caseTypes = new Set < ts . Type > ( ) ;
123
112
for ( const switchCase of node . cases ) {
124
113
// If the `test` property of the switch case is `null`, then we are on a
@@ -134,54 +123,47 @@ export default createRule<Options, MessageIds>({
134
123
caseTypes . add ( caseType ) ;
135
124
}
136
125
137
- const unionTypes = tsutils . unionTypeParts ( discriminantType ) ;
138
- const missingBranchTypes = unionTypes . filter (
139
- unionType => ! caseTypes . has ( unionType ) ,
140
- ) ;
126
+ const missingLiteralBranchTypes : ts . Type [ ] = [ ] ;
127
+
128
+ for ( const unionPart of tsutils . unionTypeParts ( discriminantType ) ) {
129
+ for ( const intersectionPart of tsutils . intersectionTypeParts (
130
+ unionPart ,
131
+ ) ) {
132
+ if (
133
+ caseTypes . has ( intersectionPart ) ||
134
+ ! isTypeLiteralLikeType ( intersectionPart )
135
+ ) {
136
+ continue ;
137
+ }
138
+
139
+ missingLiteralBranchTypes . push ( intersectionPart ) ;
140
+ }
141
+ }
141
142
142
143
return {
143
144
symbolName,
144
- missingBranchTypes ,
145
+ missingLiteralBranchTypes ,
145
146
defaultCase,
146
- isUnion : true ,
147
147
containsNonLiteralType,
148
148
} ;
149
149
}
150
150
151
- /**
152
- * For example:
153
- *
154
- * - `"foo" | "bar"` is a type with all literal types.
155
- * - `"foo" | number` is a type that contains non-literal types.
156
- *
157
- * Default cases are never superfluous in switches with non-literal types.
158
- */
159
- function doesTypeContainNonLiteralType ( type : ts . Type ) : boolean {
160
- const types = tsutils . unionTypeParts ( type ) ;
161
- return types . some (
162
- type =>
163
- ! isFlagSet (
164
- type . getFlags ( ) ,
165
- ts . TypeFlags . Literal | ts . TypeFlags . Undefined | ts . TypeFlags . Null ,
166
- ) ,
167
- ) ;
168
- }
169
-
170
151
function checkSwitchExhaustive (
171
152
node : TSESTree . SwitchStatement ,
172
153
switchMetadata : SwitchMetadata ,
173
154
) : void {
174
- const { missingBranchTypes, symbolName, defaultCase } = switchMetadata ;
155
+ const { missingLiteralBranchTypes, symbolName, defaultCase } =
156
+ switchMetadata ;
175
157
176
158
// We only trigger the rule if a `default` case does not exist, since that
177
159
// would disqualify the switch statement from having cases that exactly
178
160
// match the members of a union.
179
- if ( missingBranchTypes . length > 0 && defaultCase === undefined ) {
161
+ if ( missingLiteralBranchTypes . length > 0 && defaultCase === undefined ) {
180
162
context . report ( {
181
163
node : node . discriminant ,
182
164
messageId : 'switchIsNotExhaustive' ,
183
165
data : {
184
- missingBranches : missingBranchTypes
166
+ missingBranches : missingLiteralBranchTypes
185
167
. map ( missingType =>
186
168
tsutils . isTypeFlagSet ( missingType , ts . TypeFlags . ESSymbolLike )
187
169
? `typeof ${ missingType . getSymbol ( ) ?. escapedName as string } `
@@ -196,7 +178,7 @@ export default createRule<Options, MessageIds>({
196
178
return fixSwitch (
197
179
fixer ,
198
180
node ,
199
- missingBranchTypes ,
181
+ missingLiteralBranchTypes ,
200
182
symbolName ?. toString ( ) ,
201
183
) ;
202
184
} ,
@@ -227,24 +209,13 @@ export default createRule<Options, MessageIds>({
227
209
continue ;
228
210
}
229
211
230
- // While running this rule on the "checker.ts" file of TypeScript, the
231
- // the fix introduced a compiler error due to:
232
- //
233
- // ```ts
234
- // type __String = (string & {
235
- // __escapedIdentifier: void;
236
- // }) | (void & {
237
- // __escapedIdentifier: void;
238
- // }) | InternalSymbolName;
239
- // ```
240
- //
241
- // The following check fixes it.
242
- if ( missingBranchType . isIntersection ( ) ) {
243
- continue ;
244
- }
245
-
246
212
const missingBranchName = missingBranchType . getSymbol ( ) ?. escapedName ;
247
- let caseTest = checker . typeToString ( missingBranchType ) ;
213
+ let caseTest = tsutils . isTypeFlagSet (
214
+ missingBranchType ,
215
+ ts . TypeFlags . ESSymbolLike ,
216
+ )
217
+ ? missingBranchName !
218
+ : checker . typeToString ( missingBranchType ) ;
248
219
249
220
if (
250
221
symbolName &&
@@ -298,11 +269,11 @@ export default createRule<Options, MessageIds>({
298
269
return ;
299
270
}
300
271
301
- const { missingBranchTypes , defaultCase, containsNonLiteralType } =
272
+ const { missingLiteralBranchTypes , defaultCase, containsNonLiteralType } =
302
273
switchMetadata ;
303
274
304
275
if (
305
- missingBranchTypes . length === 0 &&
276
+ missingLiteralBranchTypes . length === 0 &&
306
277
defaultCase !== undefined &&
307
278
! containsNonLiteralType
308
279
) {
@@ -321,9 +292,9 @@ export default createRule<Options, MessageIds>({
321
292
return ;
322
293
}
323
294
324
- const { isUnion , defaultCase } = switchMetadata ;
295
+ const { defaultCase , containsNonLiteralType } = switchMetadata ;
325
296
326
- if ( ! isUnion && defaultCase === undefined ) {
297
+ if ( containsNonLiteralType && defaultCase === undefined ) {
327
298
context . report ( {
328
299
node : node . discriminant ,
329
300
messageId : 'switchIsNotExhaustive' ,
@@ -354,6 +325,31 @@ export default createRule<Options, MessageIds>({
354
325
} ,
355
326
} ) ;
356
327
357
- function isFlagSet ( flags : number , flag : number ) : boolean {
358
- return ( flags & flag ) !== 0 ;
328
+ function isTypeLiteralLikeType ( type : ts . Type ) : boolean {
329
+ return tsutils . isTypeFlagSet (
330
+ type ,
331
+ ts . TypeFlags . Literal |
332
+ ts . TypeFlags . Undefined |
333
+ ts . TypeFlags . Null |
334
+ ts . TypeFlags . UniqueESSymbol ,
335
+ ) ;
336
+ }
337
+
338
+ /**
339
+ * For example:
340
+ *
341
+ * - `"foo" | "bar"` is a type with all literal types.
342
+ * - `"foo" | number` is a type that contains non-literal types.
343
+ * - `"foo" & { bar: 1 }` is a type that contains non-literal types.
344
+ *
345
+ * Default cases are never superfluous in switches with non-literal types.
346
+ */
347
+ function doesTypeContainNonLiteralType ( type : ts . Type ) : boolean {
348
+ return tsutils
349
+ . unionTypeParts ( type )
350
+ . some ( type =>
351
+ tsutils
352
+ . intersectionTypeParts ( type )
353
+ . every ( subType => ! isTypeLiteralLikeType ( subType ) ) ,
354
+ ) ;
359
355
}
0 commit comments