Skip to content

Commit 7f46b33

Browse files
johnniwintherCommit Queue
authored andcommitted
[_fe_analyzer_shared] Return the shortest witness
This updates the exhaustiveness checking to return the shortest witness when no more cases match a value. This also fixes an exponential case that occurred when checking for reachability. Closes #59927 Change-Id: I82ede75113ca5d361875620287f7ce3c5fb2f5d2 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/405120 Reviewed-by: Konstantin Shcheglov <[email protected]> Commit-Queue: Johnni Winther <[email protected]>
1 parent 762a4c7 commit 7f46b33

File tree

14 files changed

+226
-48
lines changed

14 files changed

+226
-48
lines changed

pkg/_fe_analyzer_shared/lib/src/exhaustiveness/exhaustive.dart

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ List<Witness>? checkExhaustiveness(
7979
// TODO(johnniwinther): Perform reachability checking.
8080
List<List<Space>> caseRows = cases.map((space) => [space]).toList();
8181

82-
List<Witness>? witnesses =
83-
checker._unmatched(caseRows, [valueSpace], returnMultipleWitnesses: true);
82+
List<Witness>? witnesses = checker._unmatched(caseRows, [valueSpace],
83+
returnMultipleWitnesses: true);
8484

8585
// Uncomment this to have it print out the witness for non-exhaustive matches.
8686
// if (witnesses != null) witnesses.forEach(print);
@@ -117,6 +117,54 @@ class _Checker {
117117
// complete description of at least one value that slipped past all the
118118
// rows.
119119
return [new Witness(witnessPredicates)];
120+
} else if (caseRows.isEmpty && !returnMultipleWitnesses) {
121+
// We have no more cases that can match the witness, so unless we want
122+
// to multiple witnesses, we return directly.
123+
//
124+
// This will return the shortest possible witness and will for instance
125+
// return `(E.b, _)` instead of `(E.b, E.a)` for
126+
//
127+
// enum E { a, b }
128+
// m(E e) => switch (e) { (E.a, _) => 0, };
129+
//
130+
// and `C(f1: int())` instead of `C(f1: int(), f2: int())` for
131+
//
132+
// abstract class C { num f1, f2; }
133+
// m(C c) => switch (e) { C(f1: double(), f2: double()) => 0, };
134+
//
135+
// Additionally, it avoids a degenerate case occurring only when checking
136+
// for unreachable cases. In this mode, the value space is created from
137+
// cases that are not included in the case rows. This means that the
138+
// value space visited with an empty set of case rows left, can be
139+
// exponential in the size of the case. For instance if we have
140+
//
141+
// sealed class S {}
142+
// class S1 extends S {}
143+
// class S2 extends S {
144+
// String? f1, f2, f3, f4, f5, f6, f7, f8;
145+
// }
146+
// m(S s) => {
147+
// S1() => 0,
148+
// S2(:var f1, :var f2, :var f3, :var f4,
149+
// :var f5, :var f6, :var f7, :var f8) => 1,
150+
// };
151+
//
152+
// then in order to check that the second case is not unreachable after
153+
// the first case, we would otherwise check all combinations of `S2` with
154+
// fields values `String` or `null` for fields `f1` to `f8`, instead of
155+
// just stopping with the shortest witness `S2()`.
156+
//
157+
// If we want to compute multiple witness, which we only do for the
158+
// top-most value space, we continue the search for longer witnesses. This
159+
// means that if we have
160+
//
161+
// enum E { a, b }
162+
// m(E e) => switch (e) {};
163+
//
164+
// we do not simply return `_` as the witness, but instead the witnesses
165+
// `E.a` and `E.b`. We want this for the quick-fix that adds all enum
166+
// values or sealed class subclasses in case of an empty switch.
167+
return [new Witness(witnessPredicates)];
120168
}
121169

122170
// Look down the first column of tests.

pkg/_fe_analyzer_shared/test/exhaustiveness/data/and_pattern.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ intersectSameClass(A o1, A o2, A o3) {
4848
],
4949
};
5050
var c = /*
51-
error=non-exhaustive:A(field1: C(), field2: C()),
51+
error=non-exhaustive:A(field1: C()),
5252
fields={field1:B,field2:B},
5353
type=A
5454
*/
@@ -73,7 +73,7 @@ intersectSubClass(A o1, A o2, A o3) {
7373
[field1, hashCode],
7474
};
7575
var c = /*
76-
error=non-exhaustive:A(field1: C(), field2: C(), hashCode: int())/A(field1: C(), field2: C()),
76+
error=non-exhaustive:A(field1: C(), field2: C()),
7777
fields={field1:B,field2:B,hashCode:int},
7878
type=A
7979
*/

pkg/_fe_analyzer_shared/test/exhaustiveness/data/extension_members.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ exhaustiveIJ_Multiple(A a) {
268268
nonExhaustiveIJ_MultipleRestricted(A a) {
269269
/*
270270
checkingOrder={A,B,C,D},
271-
error=non-exhaustive:I(member: B(), member2: B(), member3: C()) && J(member: double(), member2: C(), member3: true)/I(member2: B()) && J(member: double(), member2: C(), member3: true),
271+
error=non-exhaustive:I(member: B(), member2: B(), member3: C()) && J(member: double(), member2: C())/I(member2: B()) && J(member: double(), member2: C()),
272272
fields={member:-,C.member:C,I.member:B,I.member2:A,I.member3:C,J.member:num,J.member2:A,J.member3:bool},
273273
subtypes={B,C,D},
274274
type=A

pkg/_fe_analyzer_shared/test/exhaustiveness/data/issue51973.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ exhaustiveSealedListAsList(
9696
SealedList<int>
9797
sl) => /*
9898
checkingOrder={SealedList<int>,ListA<int>,ListB<int>,<int>[],<int>[()],<int>[(), (), ...],<int>[],<int>[()],<int>[(), (), ...]},
99-
error=non-exhaustive:[Object()];[Object(), Object(), ...Object()];[Object()];[Object(), Object(), ...Object()],
99+
error=non-exhaustive:[Object()];[Object(), _, ...];[Object()];[Object(), _, ...],
100100
expandedSubtypes={<int>[],<int>[()],<int>[(), (), ...],<int>[],<int>[()],<int>[(), (), ...]},
101101
subtypes={ListA<int>,ListB<int>},
102102
type=SealedList<int>
@@ -112,7 +112,7 @@ nonExhaustiveSealedListAsList(
112112
SealedList<int>
113113
sl) => /*
114114
checkingOrder={SealedList<int>,ListA<int>,ListB<int>,<int>[],<int>[()],<int>[(), (), ...],<int>[],<int>[()],<int>[(), (), ...]},
115-
error=non-exhaustive:[_];[Object(), Object(), ...Object()];[_];[Object(), Object(), ...Object()],
115+
error=non-exhaustive:[_];[Object(), _, ...];[_];[Object(), _, ...],
116116
expandedSubtypes={<int>[],<int>[()],<int>[(), (), ...],<int>[],<int>[()],<int>[(), (), ...]},
117117
subtypes={ListA<int>,ListB<int>},
118118
type=SealedList<int>
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
sealed class State {
6+
const State._();
7+
}
8+
9+
final class StateEmpty extends State {
10+
const StateEmpty() : super._();
11+
}
12+
13+
final class StateWithBunchOfFields extends State {
14+
const StateWithBunchOfFields({
15+
this.value1,
16+
this.value2,
17+
this.value3,
18+
this.value4,
19+
this.value5,
20+
this.value6,
21+
this.value7,
22+
this.value8,
23+
this.value9,
24+
this.value10,
25+
this.value11,
26+
this.value12,
27+
this.value13,
28+
this.value14,
29+
this.value15,
30+
this.value16,
31+
this.value17,
32+
this.value18,
33+
this.value19,
34+
this.value20,
35+
this.value21,
36+
this.value22,
37+
this.value23,
38+
this.value24,
39+
this.value25,
40+
this.value26,
41+
this.value27,
42+
this.value28,
43+
this.value29,
44+
this.value30,
45+
this.value31,
46+
}) : super._();
47+
48+
final String? value1;
49+
final String? value2;
50+
final String? value3;
51+
final String? value4;
52+
final String? value5;
53+
final String? value6;
54+
final String? value7;
55+
final String? value8;
56+
final String? value9;
57+
final String? value10;
58+
final String? value11;
59+
final String? value12;
60+
final String? value13;
61+
final String? value14;
62+
final String? value15;
63+
final String? value16;
64+
final String? value17;
65+
final String? value18;
66+
final String? value19;
67+
final String? value20;
68+
final String? value21;
69+
final String? value22;
70+
final String? value23;
71+
final String? value24;
72+
final String? value25;
73+
final String? value26;
74+
final String? value27;
75+
final String? value28;
76+
final String? value29;
77+
final String? value30;
78+
final String? value31;
79+
}
80+
81+
void handleState(State state) {
82+
/*
83+
checkingOrder={State,StateEmpty,StateWithBunchOfFields},
84+
fields={value1:-,value10:-,value11:-,value12:-,value13:-,value14:-,value15:-,value16:-,value17:-,value18:-,value19:-,value2:-,value20:-,value21:-,value22:-,value23:-,value24:-,value25:-,value26:-,value27:-,value28:-,value29:-,value3:-,value30:-,value31:-,value4:-,value5:-,value6:-,value7:-,value8:-,value9:-},
85+
subtypes={StateEmpty,StateWithBunchOfFields},
86+
type=State
87+
*/
88+
switch (state) {
89+
/*space=StateEmpty*/
90+
case StateEmpty():
91+
print("empty");
92+
93+
/*space=StateWithBunchOfFields(value1: String?, value2: String?, value3: String?, value4: String?, value5: String?, value6: String?, value7: String?, value8: String?, value9: String?, value10: String?, value11: String?, value12: String?, value13: String?, value14: String?, value15: String?, value16: String?, value17: String?, value18: String?, value19: String?, value20: String?, value21: String?, value22: String?, value23: String?, value24: String?, value25: String?, value26: String?, value27: String?, value28: String?, value29: String?, value30: String?, value31: String?)*/
94+
case StateWithBunchOfFields(
95+
:var value1,
96+
:var value2,
97+
:var value3,
98+
:var value4,
99+
:var value5,
100+
:var value6,
101+
:var value7,
102+
:var value8,
103+
:var value9,
104+
:var value10,
105+
:var value11,
106+
:var value12,
107+
:var value13,
108+
:var value14,
109+
:var value15,
110+
:var value16,
111+
:var value17,
112+
:var value18,
113+
:var value19,
114+
:var value20,
115+
:var value21,
116+
:var value22,
117+
:var value23,
118+
:var value24,
119+
:var value25,
120+
:var value26,
121+
:var value27,
122+
:var value28,
123+
:var value29,
124+
:var value30,
125+
:var value31,
126+
):
127+
print("many fields");
128+
}
129+
}

pkg/_fe_analyzer_shared/test/exhaustiveness/data/list.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ nonExhaustiveRestrictedHeadElement(List list) {
327327
nonExhaustiveRestrictedTailElement(List list) {
328328
return /*
329329
checkingOrder={List<dynamic>,[],[()],[(), ()],[(), (), ()],[(), (), (), ()],[(), (), (), (), (), ...]},
330-
error=non-exhaustive:[Object(), _, Object(), Object()];[Object(), _, _, ...[...], Object(), Object()]/[Object(), _, _, ..., Object(), Object()],
330+
error=non-exhaustive:[Object(), _, Object(), _];[Object(), _, _, ...[...], Object(), _]/[Object(), _, _, ..., Object(), _],
331331
subtypes={[],[()],[(), ()],[(), (), ()],[(), (), (), ()],[(), (), (), (), (), ...]},
332332
type=List<dynamic>
333333
*/
@@ -359,7 +359,7 @@ nonExhaustiveRestrictedRest(List list) {
359359
nonExhaustiveRestrictedRestWithTail(List list) {
360360
return /*
361361
checkingOrder={List<dynamic>,[],[()],[(), ()],[(), (), ()],[(), (), (), ()],[(), (), (), (), (), ...]},
362-
error=non-exhaustive:[Object(), _, Object(), Object()];[Object(), _, _, ...[...], Object(), Object()]/[Object(), _, _, ..., Object(), Object()],
362+
error=non-exhaustive:[Object(), _, _, _];[Object(), _, _, _, _, ...[...]]/[Object(), _, _, _, _, ...],
363363
subtypes={[],[()],[(), ()],[(), (), ()],[(), (), (), ()],[(), (), (), (), (), ...]},
364364
type=List<dynamic>
365365
*/

pkg/_fe_analyzer_shared/test/exhaustiveness/data/multiple.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ nonExhaustiveEnumNested2(
4242
Enum,
4343
Enum
4444
) r) => /*
45-
error=non-exhaustive:(Enum.b, Enum.a),
45+
error=non-exhaustive:(Enum.b, _),
4646
fields={$1:Enum,$2:Enum},
4747
type=(Enum, Enum)
4848
*/
@@ -57,7 +57,7 @@ nonExhaustiveEnumNested3(
5757
Enum,
5858
Enum
5959
) r) => /*
60-
error=non-exhaustive:(Enum.b, Enum.a),
60+
error=non-exhaustive:(Enum.b, _),
6161
fields={$1:Enum,$2:Enum},
6262
type=(Enum, Enum)
6363
*/
@@ -153,7 +153,7 @@ nonExhaustiveSealedNested3(
153153
S,
154154
S
155155
) r) => /*
156-
error=non-exhaustive:(B(), A()),
156+
error=non-exhaustive:(B(), _),
157157
fields={$1:S,$2:S},
158158
type=(S, S)
159159
*/

pkg/front_end/test/spell_checking_list_code.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1640,6 +1640,7 @@ shifts
16401640
ship
16411641
shl
16421642
shortening
1643+
shortest
16431644
showing
16441645
shr
16451646
shrinking

pkg/front_end/testcases/patterns/exhaustiveness/list.dart.strong.expect

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ library;
3838
// nonExhaustiveRestrictedType(List<num> list) => switch (list) {
3939
// ^
4040
//
41-
// pkg/front_end/testcases/patterns/exhaustiveness/list.dart:64:59: Error: The type 'List<num>' is not exhaustively matched by the switch cases since it doesn't match '[double(), ...[...]]'.
41+
// pkg/front_end/testcases/patterns/exhaustiveness/list.dart:64:59: Error: The type 'List<num>' is not exhaustively matched by the switch cases since it doesn't match '[double(), ...]'.
4242
// - 'List' is from 'dart:core'.
4343
// Try adding a wildcard pattern or cases that match '[double(), ...]'.
4444
// nonExhaustive1aRestrictedValue(List<num> list) => switch (list) {
@@ -56,9 +56,9 @@ library;
5656
// nonExhaustive1bRestrictedValue(List<num> list) => switch (list) {
5757
// ^
5858
//
59-
// pkg/front_end/testcases/patterns/exhaustiveness/list.dart:79:58: Error: The type 'List<num>' is not exhaustively matched by the switch cases since it doesn't match '[...[...], double()]'.
59+
// pkg/front_end/testcases/patterns/exhaustiveness/list.dart:79:58: Error: The type 'List<num>' is not exhaustively matched by the switch cases since it doesn't match '[_, ...[...]]'.
6060
// - 'List' is from 'dart:core'.
61-
// Try adding a wildcard pattern or cases that match '[..., double()]'.
61+
// Try adding a wildcard pattern or cases that match '[_, ...]'.
6262
// nonExhaustive1bRestrictedType(List<num> list) => switch (list) {
6363
// ^
6464
//
@@ -68,13 +68,13 @@ library;
6868
// nonExhaustive2aRestrictedValue(List<num> list) => switch (list) {
6969
// ^
7070
//
71-
// pkg/front_end/testcases/patterns/exhaustiveness/list.dart:90:59: Error: The type 'List<num>' is not exhaustively matched by the switch cases since it doesn't match '[double(), double(), ...[...]]'.
71+
// pkg/front_end/testcases/patterns/exhaustiveness/list.dart:90:59: Error: The type 'List<num>' is not exhaustively matched by the switch cases since it doesn't match '[double(), _, ...]'.
7272
// - 'List' is from 'dart:core'.
73-
// Try adding a wildcard pattern or cases that match '[double(), double(), ...]'.
73+
// Try adding a wildcard pattern or cases that match '[double(), _, ...]'.
7474
// nonExhaustive2bRestrictedValue(List<num> list) => switch (list) {
7575
// ^
7676
//
77-
// pkg/front_end/testcases/patterns/exhaustiveness/list.dart:96:59: Error: The type 'List<num>' is not exhaustively matched by the switch cases since it doesn't match '[double(), double(), ...[...]]'.
77+
// pkg/front_end/testcases/patterns/exhaustiveness/list.dart:96:59: Error: The type 'List<num>' is not exhaustively matched by the switch cases since it doesn't match '[double(), double(), ...]'.
7878
// - 'List' is from 'dart:core'.
7979
// Try adding a wildcard pattern or cases that match '[double(), double(), ...]'.
8080
// nonExhaustive2cRestrictedValue(List<num> list) => switch (list) {
@@ -86,9 +86,9 @@ library;
8686
// nonExhaustive2dRestrictedValue(List<num> list) => switch (list) {
8787
// ^
8888
//
89-
// pkg/front_end/testcases/patterns/exhaustiveness/list.dart:108:59: Error: The type 'List<num>' is not exhaustively matched by the switch cases since it doesn't match '[_, double()]'.
89+
// pkg/front_end/testcases/patterns/exhaustiveness/list.dart:108:59: Error: The type 'List<num>' is not exhaustively matched by the switch cases since it doesn't match '[double(), _]'.
9090
// - 'List' is from 'dart:core'.
91-
// Try adding a wildcard pattern or cases that match '[_, double()]'.
91+
// Try adding a wildcard pattern or cases that match '[double(), _]'.
9292
// nonExhaustive2eRestrictedValue(List<num> list) => switch (list) {
9393
// ^
9494
//

pkg/front_end/testcases/patterns/exhaustiveness/list.dart.strong.modular.expect

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ library;
3838
// nonExhaustiveRestrictedType(List<num> list) => switch (list) {
3939
// ^
4040
//
41-
// pkg/front_end/testcases/patterns/exhaustiveness/list.dart:64:59: Error: The type 'List<num>' is not exhaustively matched by the switch cases since it doesn't match '[double(), ...[...]]'.
41+
// pkg/front_end/testcases/patterns/exhaustiveness/list.dart:64:59: Error: The type 'List<num>' is not exhaustively matched by the switch cases since it doesn't match '[double(), ...]'.
4242
// - 'List' is from 'dart:core'.
4343
// Try adding a wildcard pattern or cases that match '[double(), ...]'.
4444
// nonExhaustive1aRestrictedValue(List<num> list) => switch (list) {
@@ -56,9 +56,9 @@ library;
5656
// nonExhaustive1bRestrictedValue(List<num> list) => switch (list) {
5757
// ^
5858
//
59-
// pkg/front_end/testcases/patterns/exhaustiveness/list.dart:79:58: Error: The type 'List<num>' is not exhaustively matched by the switch cases since it doesn't match '[...[...], double()]'.
59+
// pkg/front_end/testcases/patterns/exhaustiveness/list.dart:79:58: Error: The type 'List<num>' is not exhaustively matched by the switch cases since it doesn't match '[_, ...[...]]'.
6060
// - 'List' is from 'dart:core'.
61-
// Try adding a wildcard pattern or cases that match '[..., double()]'.
61+
// Try adding a wildcard pattern or cases that match '[_, ...]'.
6262
// nonExhaustive1bRestrictedType(List<num> list) => switch (list) {
6363
// ^
6464
//
@@ -68,13 +68,13 @@ library;
6868
// nonExhaustive2aRestrictedValue(List<num> list) => switch (list) {
6969
// ^
7070
//
71-
// pkg/front_end/testcases/patterns/exhaustiveness/list.dart:90:59: Error: The type 'List<num>' is not exhaustively matched by the switch cases since it doesn't match '[double(), double(), ...[...]]'.
71+
// pkg/front_end/testcases/patterns/exhaustiveness/list.dart:90:59: Error: The type 'List<num>' is not exhaustively matched by the switch cases since it doesn't match '[double(), _, ...]'.
7272
// - 'List' is from 'dart:core'.
73-
// Try adding a wildcard pattern or cases that match '[double(), double(), ...]'.
73+
// Try adding a wildcard pattern or cases that match '[double(), _, ...]'.
7474
// nonExhaustive2bRestrictedValue(List<num> list) => switch (list) {
7575
// ^
7676
//
77-
// pkg/front_end/testcases/patterns/exhaustiveness/list.dart:96:59: Error: The type 'List<num>' is not exhaustively matched by the switch cases since it doesn't match '[double(), double(), ...[...]]'.
77+
// pkg/front_end/testcases/patterns/exhaustiveness/list.dart:96:59: Error: The type 'List<num>' is not exhaustively matched by the switch cases since it doesn't match '[double(), double(), ...]'.
7878
// - 'List' is from 'dart:core'.
7979
// Try adding a wildcard pattern or cases that match '[double(), double(), ...]'.
8080
// nonExhaustive2cRestrictedValue(List<num> list) => switch (list) {
@@ -86,9 +86,9 @@ library;
8686
// nonExhaustive2dRestrictedValue(List<num> list) => switch (list) {
8787
// ^
8888
//
89-
// pkg/front_end/testcases/patterns/exhaustiveness/list.dart:108:59: Error: The type 'List<num>' is not exhaustively matched by the switch cases since it doesn't match '[_, double()]'.
89+
// pkg/front_end/testcases/patterns/exhaustiveness/list.dart:108:59: Error: The type 'List<num>' is not exhaustively matched by the switch cases since it doesn't match '[double(), _]'.
9090
// - 'List' is from 'dart:core'.
91-
// Try adding a wildcard pattern or cases that match '[_, double()]'.
91+
// Try adding a wildcard pattern or cases that match '[double(), _]'.
9292
// nonExhaustive2eRestrictedValue(List<num> list) => switch (list) {
9393
// ^
9494
//

0 commit comments

Comments
 (0)