Skip to content

Commit cda2815

Browse files
stereotype441Commit Queue
authored and
Commit Queue
committed
Add UNREACHABLE_SWITCH_DEFAULT warning to the analyzer.
This warning is similar to the existing `UNREACHABLE_SWITCH_CASE` warning, except that it warns if the `default` clause of a switch statement is unreachable due to all the `case` clasuses fully exhausting the switched type. To make the implementation easier, I changed the API for the `reportExhaustiveness` method in `_fe_analyzer_shared` (which is the primary entry point to the shared exhaustiveness checker). Previously, this method returned a list of `ExhaustivenessError`, where each list element was either an `UnreachableCaseError` (indicating that a certain case was unreachable) or a `NonExhaustiveError` (indicating that the entire switch statement was not exhaustive). If the caller passed in `false` for `computeUnreachable`, `UnreachableCaseError`s would not be returned, so the returned list would either be empty or contain a single `NonExhaustiveError`. The new API renames the types for clarity: - `NonExhaustiveError` becomes `NonExhaustiveness`, to highlight the fact that it's not necessarily an error for the switch's cases to be non-exhaustive; it's only an error if the scrutinee's static type is an "always exhaustive" type and there is no `default` clause. - `UnreachableCaseError` becomes `CaseUnreachability`, to highlight the fact that it's not an error for a case to be unreachable; it's a warning. Also, the new API adds instances of `CaseUnreachability` to an optional user-provided list instead of returning a newly created list; this allows callers to communicate that they don't need to see `CaseUnreachability` information by passing `null`. This frees up the return type to simply be an instance of `NonExhaustiveness` (if the cases are not exhaustive) or `null` (if they are exhaustive). This makes it easier for the analyzer to decide whether to issue the new warning, because it doesn't have to dig around the list looking for an instance of `NonExhaustiveness`. The new warning has an associated quick fix (remove the unreachable `default` clause). This quick fix uses the same `RemoveDeadCode` logic in the analysis server that the existing `UNREACHABLE_SWITCH_CASE` warning uses. Fixes #54575. Bug: #54575 Change-Id: I18b6b7c5249d77d28ead7488b4aae4ea65c4b664 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/378960 Reviewed-by: Johnni Winther <[email protected]> Commit-Queue: Paul Berry <[email protected]> Reviewed-by: Samuel Rawlins <[email protected]> Reviewed-by: Erik Ernst <[email protected]>
1 parent e06d0f3 commit cda2815

33 files changed

+660
-173
lines changed

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

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,37 +21,44 @@ bool isExhaustive(ObjectPropertyLookup fieldLookup, Space valueSpace,
2121
/// checks to see if any case can't be matched because it's covered by previous
2222
/// cases.
2323
///
24-
/// Returns a list of any unreachable case or non-exhaustive match errors.
25-
/// Returns an empty list if all cases are reachable and the cases are
26-
/// exhaustive.
27-
List<ExhaustivenessError> reportErrors(
24+
/// If any unreachable cases are found, information about them is appended to
25+
/// [caseUnreachabilities]. (If `null` is passed for [caseUnreachabilities],
26+
/// then no information about unreachable cases is generated).
27+
///
28+
/// If the switch cases are not fully exhaustive, details about how they fail to
29+
/// be exhaustive are returned using a data structure of type
30+
/// [NonExhaustiveness]; otherwise `null` is returned.
31+
///
32+
/// Note that if a non-null value is returned, that doesn't necessarily mean
33+
/// that an error should be reported; the caller still must check whether the
34+
/// switch has a `default` clause and whether the scrutinee type is an "always
35+
/// exhaustive" type.
36+
NonExhaustiveness? computeExhaustiveness(
2837
ObjectPropertyLookup fieldLookup, StaticType valueType, List<Space> cases,
29-
{required bool computeUnreachable}) {
38+
{List<CaseUnreachability>? caseUnreachabilities}) {
3039
_Checker checker = new _Checker(fieldLookup);
3140

32-
List<ExhaustivenessError> errors = <ExhaustivenessError>[];
33-
3441
Space valuePattern = new Space(const Path.root(), valueType);
3542
List<List<Space>> caseRows = cases.map((space) => [space]).toList();
3643

37-
if (computeUnreachable) {
44+
if (caseUnreachabilities != null) {
3845
for (int i = 1; i < caseRows.length; i++) {
3946
// See if this case is covered by previous ones.
4047
if (checker._unmatched(caseRows.sublist(0, i), caseRows[i],
4148
returnMultipleWitnesses: false) ==
4249
null) {
43-
errors.add(new UnreachableCaseError(valueType, cases, i));
50+
caseUnreachabilities.add(new CaseUnreachability(valueType, cases, i));
4451
}
4552
}
4653
}
4754

4855
List<Witness>? witnesses = checker._unmatched(caseRows, [valuePattern],
4956
returnMultipleWitnesses: true);
5057
if (witnesses != null) {
51-
errors.add(new NonExhaustiveError(valueType, cases, witnesses));
58+
return new NonExhaustiveness(valueType, cases, witnesses);
59+
} else {
60+
return null;
5261
}
53-
54-
return errors;
5562
}
5663

5764
/// Determines if [cases] is exhaustive over all values contained by
@@ -357,28 +364,26 @@ List<StaticType> checkingOrder(StaticType type, Set<Key> keysOfInterest) {
357364
return result;
358365
}
359366

360-
class ExhaustivenessError {}
361-
362-
class NonExhaustiveError implements ExhaustivenessError {
367+
class NonExhaustiveness {
363368
final StaticType valueType;
364369

365370
final List<Space> cases;
366371

367372
final List<Witness> witnesses;
368373

369-
NonExhaustiveError(this.valueType, this.cases, this.witnesses);
374+
NonExhaustiveness(this.valueType, this.cases, this.witnesses);
370375

371376
@override
372377
String toString() =>
373378
'$valueType is not exhaustively matched by ${cases.join('|')}.';
374379
}
375380

376-
class UnreachableCaseError implements ExhaustivenessError {
381+
class CaseUnreachability {
377382
final StaticType valueType;
378383
final List<Space> cases;
379384
final int index;
380385

381-
UnreachableCaseError(this.valueType, this.cases, this.index);
386+
CaseUnreachability(this.valueType, this.cases, this.index);
382387

383388
@override
384389
String toString() => 'Case #${index + 1} ${cases[index]} is unreachable.';

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

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -80,25 +80,20 @@ String? typesToText(Iterable<StaticType> types) {
8080
return sb.toString();
8181
}
8282

83-
String errorToText(ExhaustivenessError error) {
84-
if (error is NonExhaustiveError) {
85-
StringBuffer sb = new StringBuffer();
86-
sb.write('non-exhaustive:');
87-
String delimiter = '';
88-
for (Witness witness in error.witnesses) {
89-
sb.write(delimiter);
90-
String witnessText = witness.asWitness;
91-
String correctionText = witness.asCorrection;
92-
if (witnessText != correctionText) {
93-
sb.write('$witnessText/$correctionText');
94-
} else {
95-
sb.write(witnessText);
96-
}
97-
delimiter = ';';
83+
String nonExhaustivenessToText(NonExhaustiveness nonExhaustiveness) {
84+
StringBuffer sb = new StringBuffer();
85+
sb.write('non-exhaustive:');
86+
String delimiter = '';
87+
for (Witness witness in nonExhaustiveness.witnesses) {
88+
sb.write(delimiter);
89+
String witnessText = witness.asWitness;
90+
String correctionText = witness.asCorrection;
91+
if (witnessText != correctionText) {
92+
sb.write('$witnessText/$correctionText');
93+
} else {
94+
sb.write(witnessText);
9895
}
99-
return sb.toString();
100-
} else {
101-
assert(error is UnreachableCaseError);
102-
return 'unreachable';
96+
delimiter = ';';
10397
}
98+
return sb.toString();
10499
}

pkg/_fe_analyzer_shared/test/exhaustiveness/report_errors_test.dart

Lines changed: 41 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -36,54 +36,54 @@ void main() {
3636

3737
test('exhaustiveness', () {
3838
// Case matching top type covers all subtypes.
39-
expectReportErrors(env, a, [a]);
40-
expectReportErrors(env, b, [a]);
41-
expectReportErrors(env, d, [a]);
39+
expectExhaustiveness(env, a, [a]);
40+
expectExhaustiveness(env, b, [a]);
41+
expectExhaustiveness(env, d, [a]);
4242

4343
// Case matching subtype doesn't cover supertype.
44-
expectReportErrors(env, a, [b], 'A is not exhaustively matched by B.');
45-
expectReportErrors(env, b, [b]);
46-
expectReportErrors(env, d, [b]);
47-
expectReportErrors(env, e, [b]);
44+
expectExhaustiveness(env, a, [b], 'A is not exhaustively matched by B.');
45+
expectExhaustiveness(env, b, [b]);
46+
expectExhaustiveness(env, d, [b]);
47+
expectExhaustiveness(env, e, [b]);
4848

4949
// Matching subtypes of sealed type is exhaustive.
50-
expectReportErrors(env, a, [b, c]);
51-
expectReportErrors(env, a, [d, e, f]);
52-
expectReportErrors(env, a, [b, f]);
53-
expectReportErrors(
50+
expectExhaustiveness(env, a, [b, c]);
51+
expectExhaustiveness(env, a, [d, e, f]);
52+
expectExhaustiveness(env, a, [b, f]);
53+
expectExhaustiveness(
5454
env, a, [c, d], 'A is not exhaustively matched by C|D.');
55-
expectReportErrors(
55+
expectExhaustiveness(
5656
env, f, [g, h], 'F is not exhaustively matched by G|H.');
5757
});
5858

5959
test('unreachable case', () {
6060
// Same type.
61-
expectReportErrors(env, b, [b, b], 'Case #2 B is unreachable.');
61+
expectExhaustiveness(env, b, [b, b], 'Case #2 B is unreachable.');
6262

6363
// Previous case is supertype.
64-
expectReportErrors(env, b, [a, b], 'Case #2 B is unreachable.');
64+
expectExhaustiveness(env, b, [a, b], 'Case #2 B is unreachable.');
6565

6666
// Previous subtype cases cover sealed supertype.
67-
expectReportErrors(env, a, [b, c, a], 'Case #3 A is unreachable.');
68-
expectReportErrors(env, a, [d, e, f, a], 'Case #4 A is unreachable.');
69-
expectReportErrors(env, a, [b, f, a], 'Case #3 A is unreachable.');
70-
expectReportErrors(env, a, [c, d, a]);
67+
expectExhaustiveness(env, a, [b, c, a], 'Case #3 A is unreachable.');
68+
expectExhaustiveness(env, a, [d, e, f, a], 'Case #4 A is unreachable.');
69+
expectExhaustiveness(env, a, [b, f, a], 'Case #3 A is unreachable.');
70+
expectExhaustiveness(env, a, [c, d, a]);
7171

7272
// Previous subtype cases do not cover unsealed supertype.
73-
expectReportErrors(env, f, [g, h, f]);
73+
expectExhaustiveness(env, f, [g, h, f]);
7474
});
7575

7676
test('covered record destructuring |', () {
7777
var r = env.createRecordType({x: a, y: a, z: a});
7878

7979
// Wider field is not covered.
80-
expectReportErrors(env, r, [
80+
expectExhaustiveness(env, r, [
8181
ty(r, {x: b}),
8282
ty(r, {x: a}),
8383
]);
8484

8585
// Narrower field is covered.
86-
expectReportErrors(
86+
expectExhaustiveness(
8787
env,
8888
r,
8989
[
@@ -107,40 +107,45 @@ void main() {
107107
var e = env.createClass('E', inherits: [c]);
108108

109109
// Must cover null.
110-
expectReportErrors(env, a.nullable, [b, d, e],
110+
expectExhaustiveness(env, a.nullable, [b, d, e],
111111
'A? is not exhaustively matched by B|D|E.');
112112

113113
// Can cover null with any nullable subtype.
114-
expectReportErrors(env, a.nullable, [b.nullable, c]);
115-
expectReportErrors(env, a.nullable, [b, c.nullable]);
116-
expectReportErrors(env, a.nullable, [b, d.nullable, e]);
117-
expectReportErrors(env, a.nullable, [b, d, e.nullable]);
114+
expectExhaustiveness(env, a.nullable, [b.nullable, c]);
115+
expectExhaustiveness(env, a.nullable, [b, c.nullable]);
116+
expectExhaustiveness(env, a.nullable, [b, d.nullable, e]);
117+
expectExhaustiveness(env, a.nullable, [b, d, e.nullable]);
118118

119119
// Can cover null with a null space.
120-
expectReportErrors(env, a.nullable, [b, c, StaticType.nullType]);
121-
expectReportErrors(env, a.nullable, [b, d, e, StaticType.nullType]);
120+
expectExhaustiveness(env, a.nullable, [b, c, StaticType.nullType]);
121+
expectExhaustiveness(env, a.nullable, [b, d, e, StaticType.nullType]);
122122

123123
// Nullable covers the non-null.
124-
expectReportErrors(
124+
expectExhaustiveness(
125125
env, a.nullable, [a.nullable, a], 'Case #2 A is unreachable.');
126-
expectReportErrors(
126+
expectExhaustiveness(
127127
env, b.nullable, [a.nullable, b], 'Case #2 B is unreachable.');
128128

129129
// Nullable covers null.
130-
expectReportErrors(env, a.nullable, [a.nullable, StaticType.nullType],
130+
expectExhaustiveness(env, a.nullable, [a.nullable, StaticType.nullType],
131131
'Case #2 Null is unreachable.');
132-
expectReportErrors(env, b.nullable, [a.nullable, StaticType.nullType],
132+
expectExhaustiveness(env, b.nullable, [a.nullable, StaticType.nullType],
133133
'Case #2 Null is unreachable.');
134134
});
135135
});
136136
}
137137

138-
void expectReportErrors(ObjectPropertyLookup objectFieldLookup,
138+
void expectExhaustiveness(ObjectPropertyLookup objectFieldLookup,
139139
StaticType valueType, List<Object> cases,
140140
[String errors = '']) {
141+
List<CaseUnreachability> caseUnreachabilities = [];
142+
var nonExhaustiveness = computeExhaustiveness(
143+
objectFieldLookup, valueType, parseSpaces(cases),
144+
caseUnreachabilities: caseUnreachabilities);
141145
expect(
142-
reportErrors(objectFieldLookup, valueType, parseSpaces(cases),
143-
computeUnreachable: true)
144-
.join('\n'),
146+
[
147+
...caseUnreachabilities,
148+
if (nonExhaustiveness != null) nonExhaustiveness
149+
].join('\n'),
145150
errors);
146151
}

pkg/analysis_server/lib/src/services/correction/error_fix_status.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3739,6 +3739,8 @@ WarningCode.UNNECESSARY_WILDCARD_PATTERN:
37393739
status: hasFix
37403740
WarningCode.UNREACHABLE_SWITCH_CASE:
37413741
status: hasFix
3742+
WarningCode.UNREACHABLE_SWITCH_DEFAULT:
3743+
status: hasFix
37423744
WarningCode.UNUSED_CATCH_CLAUSE:
37433745
status: hasFix
37443746
WarningCode.UNUSED_CATCH_STACK:

pkg/analysis_server/lib/src/services/correction/fix_internal.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1824,6 +1824,9 @@ final _builtInNonLintProducers = <ErrorCode, List<ProducerGenerator>>{
18241824
WarningCode.UNREACHABLE_SWITCH_CASE: [
18251825
RemoveDeadCode.new,
18261826
],
1827+
WarningCode.UNREACHABLE_SWITCH_DEFAULT: [
1828+
RemoveDeadCode.new,
1829+
],
18271830
WarningCode.UNUSED_CATCH_CLAUSE: [
18281831
RemoveUnusedCatchClause.new,
18291832
],

pkg/analysis_server/test/src/services/correction/fix/remove_dead_code_test.dart

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,52 @@ void f() {
539539
''');
540540
}
541541

542+
Future<void> test_switchDefault_sharedStatements() async {
543+
await resolveTestCode('''
544+
enum E { e1, e2 }
545+
void f(E e) {
546+
switch(e) {
547+
case E.e1:
548+
case E.e2:
549+
default:
550+
break;
551+
}
552+
}
553+
''');
554+
await assertHasFix('''
555+
enum E { e1, e2 }
556+
void f(E e) {
557+
switch(e) {
558+
case E.e1:
559+
case E.e2:
560+
break;
561+
}
562+
}
563+
''');
564+
}
565+
566+
Future<void> test_switchDefault_uniqueStatements() async {
567+
await resolveTestCode('''
568+
enum E { e1, e2 }
569+
void f(E e) {
570+
switch(e) {
571+
case E.e1: print('e1');
572+
case E.e2: print('e2');
573+
default: print('e3');
574+
}
575+
}
576+
''');
577+
await assertHasFix('''
578+
enum E { e1, e2 }
579+
void f(E e) {
580+
switch(e) {
581+
case E.e1: print('e1');
582+
case E.e2: print('e2');
583+
}
584+
}
585+
''');
586+
}
587+
542588
@FailingTest(issue: 'https://github.com/dart-lang/sdk/issues/50950')
543589
Future<void> test_switchExpression() async {
544590
await resolveTestCode('''

0 commit comments

Comments
 (0)