diff --git a/pkg/_fe_analyzer_shared/lib/src/exhaustiveness/exhaustive.dart b/pkg/_fe_analyzer_shared/lib/src/exhaustiveness/exhaustive.dart index eb8cae851dac..a0f4e260380d 100644 --- a/pkg/_fe_analyzer_shared/lib/src/exhaustiveness/exhaustive.dart +++ b/pkg/_fe_analyzer_shared/lib/src/exhaustiveness/exhaustive.dart @@ -21,26 +21,33 @@ bool isExhaustive(ObjectPropertyLookup fieldLookup, Space valueSpace, /// checks to see if any case can't be matched because it's covered by previous /// cases. /// -/// Returns a list of any unreachable case or non-exhaustive match errors. -/// Returns an empty list if all cases are reachable and the cases are -/// exhaustive. -List reportErrors( +/// If any unreachable cases are found, information about them is appended to +/// [caseUnreachabilities]. (If `null` is passed for [caseUnreachabilities], +/// then no information about unreachable cases is generated). +/// +/// If the switch cases are not fully exhaustive, details about how they fail to +/// be exhaustive are returned using a data structure of type +/// [NonExhaustiveness]; otherwise `null` is returned. +/// +/// Note that if a non-null value is returned, that doesn't necessarily mean +/// that an error should be reported; the caller still must check whether the +/// switch has a `default` clause and whether the scrutinee type is an "always +/// exhaustive" type. +NonExhaustiveness? computeExhaustiveness( ObjectPropertyLookup fieldLookup, StaticType valueType, List cases, - {required bool computeUnreachable}) { + {List? caseUnreachabilities}) { _Checker checker = new _Checker(fieldLookup); - List errors = []; - Space valuePattern = new Space(const Path.root(), valueType); List> caseRows = cases.map((space) => [space]).toList(); - if (computeUnreachable) { + if (caseUnreachabilities != null) { for (int i = 1; i < caseRows.length; i++) { // See if this case is covered by previous ones. if (checker._unmatched(caseRows.sublist(0, i), caseRows[i], returnMultipleWitnesses: false) == null) { - errors.add(new UnreachableCaseError(valueType, cases, i)); + caseUnreachabilities.add(new CaseUnreachability(valueType, cases, i)); } } } @@ -48,10 +55,10 @@ List reportErrors( List? witnesses = checker._unmatched(caseRows, [valuePattern], returnMultipleWitnesses: true); if (witnesses != null) { - errors.add(new NonExhaustiveError(valueType, cases, witnesses)); + return new NonExhaustiveness(valueType, cases, witnesses); + } else { + return null; } - - return errors; } /// Determines if [cases] is exhaustive over all values contained by @@ -357,28 +364,26 @@ List checkingOrder(StaticType type, Set keysOfInterest) { return result; } -class ExhaustivenessError {} - -class NonExhaustiveError implements ExhaustivenessError { +class NonExhaustiveness { final StaticType valueType; final List cases; final List witnesses; - NonExhaustiveError(this.valueType, this.cases, this.witnesses); + NonExhaustiveness(this.valueType, this.cases, this.witnesses); @override String toString() => '$valueType is not exhaustively matched by ${cases.join('|')}.'; } -class UnreachableCaseError implements ExhaustivenessError { +class CaseUnreachability { final StaticType valueType; final List cases; final int index; - UnreachableCaseError(this.valueType, this.cases, this.index); + CaseUnreachability(this.valueType, this.cases, this.index); @override String toString() => 'Case #${index + 1} ${cases[index]} is unreachable.'; diff --git a/pkg/_fe_analyzer_shared/lib/src/exhaustiveness/test_helper.dart b/pkg/_fe_analyzer_shared/lib/src/exhaustiveness/test_helper.dart index 4ed5094dc802..ef31075907b7 100644 --- a/pkg/_fe_analyzer_shared/lib/src/exhaustiveness/test_helper.dart +++ b/pkg/_fe_analyzer_shared/lib/src/exhaustiveness/test_helper.dart @@ -80,25 +80,20 @@ String? typesToText(Iterable types) { return sb.toString(); } -String errorToText(ExhaustivenessError error) { - if (error is NonExhaustiveError) { - StringBuffer sb = new StringBuffer(); - sb.write('non-exhaustive:'); - String delimiter = ''; - for (Witness witness in error.witnesses) { - sb.write(delimiter); - String witnessText = witness.asWitness; - String correctionText = witness.asCorrection; - if (witnessText != correctionText) { - sb.write('$witnessText/$correctionText'); - } else { - sb.write(witnessText); - } - delimiter = ';'; +String nonExhaustivenessToText(NonExhaustiveness nonExhaustiveness) { + StringBuffer sb = new StringBuffer(); + sb.write('non-exhaustive:'); + String delimiter = ''; + for (Witness witness in nonExhaustiveness.witnesses) { + sb.write(delimiter); + String witnessText = witness.asWitness; + String correctionText = witness.asCorrection; + if (witnessText != correctionText) { + sb.write('$witnessText/$correctionText'); + } else { + sb.write(witnessText); } - return sb.toString(); - } else { - assert(error is UnreachableCaseError); - return 'unreachable'; + delimiter = ';'; } + return sb.toString(); } diff --git a/pkg/_fe_analyzer_shared/test/exhaustiveness/report_errors_test.dart b/pkg/_fe_analyzer_shared/test/exhaustiveness/report_errors_test.dart index d7038ca0333c..40cfccda4120 100644 --- a/pkg/_fe_analyzer_shared/test/exhaustiveness/report_errors_test.dart +++ b/pkg/_fe_analyzer_shared/test/exhaustiveness/report_errors_test.dart @@ -36,54 +36,54 @@ void main() { test('exhaustiveness', () { // Case matching top type covers all subtypes. - expectReportErrors(env, a, [a]); - expectReportErrors(env, b, [a]); - expectReportErrors(env, d, [a]); + expectExhaustiveness(env, a, [a]); + expectExhaustiveness(env, b, [a]); + expectExhaustiveness(env, d, [a]); // Case matching subtype doesn't cover supertype. - expectReportErrors(env, a, [b], 'A is not exhaustively matched by B.'); - expectReportErrors(env, b, [b]); - expectReportErrors(env, d, [b]); - expectReportErrors(env, e, [b]); + expectExhaustiveness(env, a, [b], 'A is not exhaustively matched by B.'); + expectExhaustiveness(env, b, [b]); + expectExhaustiveness(env, d, [b]); + expectExhaustiveness(env, e, [b]); // Matching subtypes of sealed type is exhaustive. - expectReportErrors(env, a, [b, c]); - expectReportErrors(env, a, [d, e, f]); - expectReportErrors(env, a, [b, f]); - expectReportErrors( + expectExhaustiveness(env, a, [b, c]); + expectExhaustiveness(env, a, [d, e, f]); + expectExhaustiveness(env, a, [b, f]); + expectExhaustiveness( env, a, [c, d], 'A is not exhaustively matched by C|D.'); - expectReportErrors( + expectExhaustiveness( env, f, [g, h], 'F is not exhaustively matched by G|H.'); }); test('unreachable case', () { // Same type. - expectReportErrors(env, b, [b, b], 'Case #2 B is unreachable.'); + expectExhaustiveness(env, b, [b, b], 'Case #2 B is unreachable.'); // Previous case is supertype. - expectReportErrors(env, b, [a, b], 'Case #2 B is unreachable.'); + expectExhaustiveness(env, b, [a, b], 'Case #2 B is unreachable.'); // Previous subtype cases cover sealed supertype. - expectReportErrors(env, a, [b, c, a], 'Case #3 A is unreachable.'); - expectReportErrors(env, a, [d, e, f, a], 'Case #4 A is unreachable.'); - expectReportErrors(env, a, [b, f, a], 'Case #3 A is unreachable.'); - expectReportErrors(env, a, [c, d, a]); + expectExhaustiveness(env, a, [b, c, a], 'Case #3 A is unreachable.'); + expectExhaustiveness(env, a, [d, e, f, a], 'Case #4 A is unreachable.'); + expectExhaustiveness(env, a, [b, f, a], 'Case #3 A is unreachable.'); + expectExhaustiveness(env, a, [c, d, a]); // Previous subtype cases do not cover unsealed supertype. - expectReportErrors(env, f, [g, h, f]); + expectExhaustiveness(env, f, [g, h, f]); }); test('covered record destructuring |', () { var r = env.createRecordType({x: a, y: a, z: a}); // Wider field is not covered. - expectReportErrors(env, r, [ + expectExhaustiveness(env, r, [ ty(r, {x: b}), ty(r, {x: a}), ]); // Narrower field is covered. - expectReportErrors( + expectExhaustiveness( env, r, [ @@ -107,40 +107,45 @@ void main() { var e = env.createClass('E', inherits: [c]); // Must cover null. - expectReportErrors(env, a.nullable, [b, d, e], + expectExhaustiveness(env, a.nullable, [b, d, e], 'A? is not exhaustively matched by B|D|E.'); // Can cover null with any nullable subtype. - expectReportErrors(env, a.nullable, [b.nullable, c]); - expectReportErrors(env, a.nullable, [b, c.nullable]); - expectReportErrors(env, a.nullable, [b, d.nullable, e]); - expectReportErrors(env, a.nullable, [b, d, e.nullable]); + expectExhaustiveness(env, a.nullable, [b.nullable, c]); + expectExhaustiveness(env, a.nullable, [b, c.nullable]); + expectExhaustiveness(env, a.nullable, [b, d.nullable, e]); + expectExhaustiveness(env, a.nullable, [b, d, e.nullable]); // Can cover null with a null space. - expectReportErrors(env, a.nullable, [b, c, StaticType.nullType]); - expectReportErrors(env, a.nullable, [b, d, e, StaticType.nullType]); + expectExhaustiveness(env, a.nullable, [b, c, StaticType.nullType]); + expectExhaustiveness(env, a.nullable, [b, d, e, StaticType.nullType]); // Nullable covers the non-null. - expectReportErrors( + expectExhaustiveness( env, a.nullable, [a.nullable, a], 'Case #2 A is unreachable.'); - expectReportErrors( + expectExhaustiveness( env, b.nullable, [a.nullable, b], 'Case #2 B is unreachable.'); // Nullable covers null. - expectReportErrors(env, a.nullable, [a.nullable, StaticType.nullType], + expectExhaustiveness(env, a.nullable, [a.nullable, StaticType.nullType], 'Case #2 Null is unreachable.'); - expectReportErrors(env, b.nullable, [a.nullable, StaticType.nullType], + expectExhaustiveness(env, b.nullable, [a.nullable, StaticType.nullType], 'Case #2 Null is unreachable.'); }); }); } -void expectReportErrors(ObjectPropertyLookup objectFieldLookup, +void expectExhaustiveness(ObjectPropertyLookup objectFieldLookup, StaticType valueType, List cases, [String errors = '']) { + List caseUnreachabilities = []; + var nonExhaustiveness = computeExhaustiveness( + objectFieldLookup, valueType, parseSpaces(cases), + caseUnreachabilities: caseUnreachabilities); expect( - reportErrors(objectFieldLookup, valueType, parseSpaces(cases), - computeUnreachable: true) - .join('\n'), + [ + ...caseUnreachabilities, + if (nonExhaustiveness != null) nonExhaustiveness + ].join('\n'), errors); } diff --git a/pkg/analysis_server/lib/src/services/correction/error_fix_status.yaml b/pkg/analysis_server/lib/src/services/correction/error_fix_status.yaml index 620408804032..ba6b35abe2b8 100644 --- a/pkg/analysis_server/lib/src/services/correction/error_fix_status.yaml +++ b/pkg/analysis_server/lib/src/services/correction/error_fix_status.yaml @@ -3739,6 +3739,8 @@ WarningCode.UNNECESSARY_WILDCARD_PATTERN: status: hasFix WarningCode.UNREACHABLE_SWITCH_CASE: status: hasFix +WarningCode.UNREACHABLE_SWITCH_DEFAULT: + status: hasFix WarningCode.UNUSED_CATCH_CLAUSE: status: hasFix WarningCode.UNUSED_CATCH_STACK: diff --git a/pkg/analysis_server/lib/src/services/correction/fix_internal.dart b/pkg/analysis_server/lib/src/services/correction/fix_internal.dart index dc5d257f4636..595419eebe54 100644 --- a/pkg/analysis_server/lib/src/services/correction/fix_internal.dart +++ b/pkg/analysis_server/lib/src/services/correction/fix_internal.dart @@ -1824,6 +1824,9 @@ final _builtInNonLintProducers = >{ WarningCode.UNREACHABLE_SWITCH_CASE: [ RemoveDeadCode.new, ], + WarningCode.UNREACHABLE_SWITCH_DEFAULT: [ + RemoveDeadCode.new, + ], WarningCode.UNUSED_CATCH_CLAUSE: [ RemoveUnusedCatchClause.new, ], diff --git a/pkg/analysis_server/test/src/services/correction/fix/remove_dead_code_test.dart b/pkg/analysis_server/test/src/services/correction/fix/remove_dead_code_test.dart index 5baf4ace10d0..ac4f3afb8811 100644 --- a/pkg/analysis_server/test/src/services/correction/fix/remove_dead_code_test.dart +++ b/pkg/analysis_server/test/src/services/correction/fix/remove_dead_code_test.dart @@ -539,6 +539,52 @@ void f() { '''); } + Future test_switchDefault_sharedStatements() async { + await resolveTestCode(''' +enum E { e1, e2 } +void f(E e) { + switch(e) { + case E.e1: + case E.e2: + default: + break; + } +} +'''); + await assertHasFix(''' +enum E { e1, e2 } +void f(E e) { + switch(e) { + case E.e1: + case E.e2: + break; + } +} +'''); + } + + Future test_switchDefault_uniqueStatements() async { + await resolveTestCode(''' +enum E { e1, e2 } +void f(E e) { + switch(e) { + case E.e1: print('e1'); + case E.e2: print('e2'); + default: print('e3'); + } +} +'''); + await assertHasFix(''' +enum E { e1, e2 } +void f(E e) { + switch(e) { + case E.e1: print('e1'); + case E.e2: print('e2'); + } +} +'''); + } + @FailingTest(issue: 'https://github.com/dart-lang/sdk/issues/50950') Future test_switchExpression() async { await resolveTestCode(''' diff --git a/pkg/analyzer/lib/src/dart/constant/constant_verifier.dart b/pkg/analyzer/lib/src/dart/constant/constant_verifier.dart index 623b5177ac95..f35dedb8f782 100644 --- a/pkg/analyzer/lib/src/dart/constant/constant_verifier.dart +++ b/pkg/analyzer/lib/src/dart/constant/constant_verifier.dart @@ -896,7 +896,7 @@ class ConstantVerifier extends RecursiveAstVisitor { var caseNodesWithSpace = []; var caseSpaces = []; - var hasDefault = false; + SwitchDefault? defaultNode; var patternConverter = PatternConverter( languageVersion: _currentLibrary.languageVersion.effective, @@ -913,7 +913,7 @@ class ConstantVerifier extends RecursiveAstVisitor { if (caseNode is SwitchCase) { // Should not happen, ignore. } else if (caseNode is SwitchDefault) { - hasDefault = true; + defaultNode = caseNode; } else if (caseNode is SwitchExpressionCase) { guardedPattern = caseNode.guardedPattern; } else if (caseNode is SwitchPatternCase) { @@ -930,20 +930,20 @@ class ConstantVerifier extends RecursiveAstVisitor { caseSpaces.add(space); } } + var reportNonExhaustive = mustBeExhaustive && defaultNode == null; // Prepare for recording data for testing. var exhaustivenessDataForTesting = this.exhaustivenessDataForTesting; // Compute and report errors. - var errors = patternConverter.hasInvalidType - ? const [] - : reportErrors(_exhaustivenessCache, scrutineeTypeEx, caseSpaces, - computeUnreachable: true); - - var reportNonExhaustive = mustBeExhaustive && !hasDefault; - for (var error in errors) { - if (error is UnreachableCaseError) { - var caseNode = caseNodesWithSpace[error.index]; + List caseUnreachabilities = []; + NonExhaustiveness? nonExhaustiveness; + if (!patternConverter.hasInvalidType) { + nonExhaustiveness = computeExhaustiveness( + _exhaustivenessCache, scrutineeTypeEx, caseSpaces, + caseUnreachabilities: caseUnreachabilities); + for (var caseUnreachability in caseUnreachabilities) { + var caseNode = caseNodesWithSpace[caseUnreachability.index]; Token errorToken; if (caseNode is SwitchExpressionCase) { errorToken = caseNode.arrow; @@ -956,32 +956,45 @@ class ConstantVerifier extends RecursiveAstVisitor { errorToken, WarningCode.UNREACHABLE_SWITCH_CASE, ); - } else if (error is NonExhaustiveError && reportNonExhaustive) { - var errorBuffer = SimpleDartBuffer(); - error.witnesses.first.toDart(errorBuffer, forCorrection: false); - var correctionTextBuffer = SimpleDartBuffer(); - error.witnesses.first.toDart(correctionTextBuffer, forCorrection: true); - - var correctionData = >[]; - for (var witness in error.witnesses) { - var correctionDataBuffer = AnalyzerDartTemplateBuffer(); - witness.toDart(correctionDataBuffer, forCorrection: true); - if (correctionDataBuffer.isComplete) { - correctionData.add(correctionDataBuffer.parts); + } + if (nonExhaustiveness != null) { + if (reportNonExhaustive) { + var errorBuffer = SimpleDartBuffer(); + nonExhaustiveness.witnesses.first + .toDart(errorBuffer, forCorrection: false); + var correctionTextBuffer = SimpleDartBuffer(); + nonExhaustiveness.witnesses.first + .toDart(correctionTextBuffer, forCorrection: true); + + var correctionData = >[]; + for (var witness in nonExhaustiveness.witnesses) { + var correctionDataBuffer = AnalyzerDartTemplateBuffer(); + witness.toDart(correctionDataBuffer, forCorrection: true); + if (correctionDataBuffer.isComplete) { + correctionData.add(correctionDataBuffer.parts); + } } + _errorReporter.atToken( + switchKeyword, + isSwitchExpression + ? CompileTimeErrorCode.NON_EXHAUSTIVE_SWITCH_EXPRESSION + : CompileTimeErrorCode.NON_EXHAUSTIVE_SWITCH_STATEMENT, + arguments: [ + scrutineeType, + errorBuffer.toString(), + correctionTextBuffer.toString(), + ], + data: correctionData.isNotEmpty ? correctionData : null, + ); + } + } else { + if (defaultNode != null && mustBeExhaustive) { + // Default node is unreachable + _errorReporter.atToken( + defaultNode.keyword, + WarningCode.UNREACHABLE_SWITCH_DEFAULT, + ); } - _errorReporter.atToken( - switchKeyword, - isSwitchExpression - ? CompileTimeErrorCode.NON_EXHAUSTIVE_SWITCH_EXPRESSION - : CompileTimeErrorCode.NON_EXHAUSTIVE_SWITCH_STATEMENT, - arguments: [ - scrutineeType, - errorBuffer.toString(), - correctionTextBuffer.toString(), - ], - data: correctionData.isNotEmpty ? correctionData : null, - ); } } @@ -993,13 +1006,13 @@ class ConstantVerifier extends RecursiveAstVisitor { } exhaustivenessDataForTesting.switchScrutineeType[node] = scrutineeTypeEx; exhaustivenessDataForTesting.switchCases[node] = caseSpaces; - for (var error in errors) { - if (error is UnreachableCaseError) { - exhaustivenessDataForTesting.errors[caseNodesWithSpace[error.index]] = - error; - } else if (reportNonExhaustive) { - exhaustivenessDataForTesting.errors[node] = error; - } + for (var caseUnreachability in caseUnreachabilities) { + exhaustivenessDataForTesting.caseUnreachabilities[ + caseNodesWithSpace[caseUnreachability.index]] = caseUnreachability; + } + if (nonExhaustiveness != null && reportNonExhaustive) { + exhaustivenessDataForTesting.nonExhaustivenesses[node] = + nonExhaustiveness; } } } diff --git a/pkg/analyzer/lib/src/error/codes.g.dart b/pkg/analyzer/lib/src/error/codes.g.dart index 7e467d4ebb4b..14ca7953721b 100644 --- a/pkg/analyzer/lib/src/error/codes.g.dart +++ b/pkg/analyzer/lib/src/error/codes.g.dart @@ -7543,6 +7543,15 @@ class WarningCode extends AnalyzerErrorCode { hasPublishedDocs: true, ); + /// No parameters. + static const WarningCode UNREACHABLE_SWITCH_DEFAULT = WarningCode( + 'UNREACHABLE_SWITCH_DEFAULT', + "This default clause is covered by the previous cases.", + correctionMessage: + "Try removing the default clause, or restructuring the preceding " + "patterns.", + ); + /// Parameters: /// 0: the name of the exception variable static const WarningCode UNUSED_CATCH_CLAUSE = WarningCode( diff --git a/pkg/analyzer/lib/src/error/error_code_values.g.dart b/pkg/analyzer/lib/src/error/error_code_values.g.dart index 2d94e9f17738..c207246da76c 100644 --- a/pkg/analyzer/lib/src/error/error_code_values.g.dart +++ b/pkg/analyzer/lib/src/error/error_code_values.g.dart @@ -1097,6 +1097,7 @@ const List errorCodeValues = [ WarningCode.UNNECESSARY_TYPE_CHECK_TRUE, WarningCode.UNNECESSARY_WILDCARD_PATTERN, WarningCode.UNREACHABLE_SWITCH_CASE, + WarningCode.UNREACHABLE_SWITCH_DEFAULT, WarningCode.UNUSED_CATCH_CLAUSE, WarningCode.UNUSED_CATCH_STACK, WarningCode.UNUSED_ELEMENT, diff --git a/pkg/analyzer/lib/src/generated/exhaustiveness.dart b/pkg/analyzer/lib/src/generated/exhaustiveness.dart index a4923c1ac6a0..1ff1ef782683 100644 --- a/pkg/analyzer/lib/src/generated/exhaustiveness.dart +++ b/pkg/analyzer/lib/src/generated/exhaustiveness.dart @@ -440,9 +440,13 @@ class ExhaustivenessDataForTesting { /// Map from switch case nodes to the space for its pattern/expression. Map caseSpaces = {}; - /// Map from switch statement/expression/case nodes to the error reported - /// on the node. - Map errors = {}; + /// Map from unreachable switch case nodes to information about their + /// unreachability. + Map caseUnreachabilities = {}; + + /// Map from switch statement nodes that are erroneous due to being + /// non-exhaustive, to information about their non-exhaustiveness. + Map nonExhaustivenesses = {}; ExhaustivenessDataForTesting(this.objectFieldLookup); } diff --git a/pkg/analyzer/messages.yaml b/pkg/analyzer/messages.yaml index f5932cff8aed..e13026ef3008 100644 --- a/pkg/analyzer/messages.yaml +++ b/pkg/analyzer/messages.yaml @@ -27196,6 +27196,53 @@ WarningCode: } } ``` + UNREACHABLE_SWITCH_DEFAULT: + problemMessage: "This default clause is covered by the previous cases." + correctionMessage: Try removing the default clause, or restructuring the preceding patterns. + hasPublishedDocs: false + comment: No parameters. + documentation: |- + #### Description + + The analyzer produces this diagnostic when a `default` clause in a + `switch` statement doesn't match anything because all of the matchable + values are matched by an earlier `case` clause. + + #### Example + + The following code produces this diagnostic because the values `E.e1` and + `E.e2` were matched in the preceding cases: + + ```dart + enum E { e1, e2 } + + void f(E x) { + switch (x) { + case E.e1: + print('one'); + case E.e2: + print('two'); + [!default!]: + print('other'); + } + } + ``` + + #### Common fixes + + Remove the unnecessary `default` clause: + + ```dart + enum E { e1, e2 } + void f(E x) { + switch (x) { + case E.e1: + print('one'); + case E.e2: + print('two'); + } + } + ``` UNUSED_CATCH_CLAUSE: problemMessage: "The exception variable '{0}' isn't used, so the 'catch' clause can be removed." correctionMessage: Try removing the catch clause. diff --git a/pkg/analyzer/test/id_tests/exhaustiveness_test.dart b/pkg/analyzer/test/id_tests/exhaustiveness_test.dart index b9d0e5ef9cdb..10fadc0dbb79 100644 --- a/pkg/analyzer/test/id_tests/exhaustiveness_test.dart +++ b/pkg/analyzer/test/id_tests/exhaustiveness_test.dart @@ -102,18 +102,20 @@ class _ExhaustivenessDataExtractor extends AstDataExtractor { } } } - ExhaustivenessError? error = _exhaustivenessData.errors[node]; - if (error != null) { - features[Tags.error] = errorToText(error); + NonExhaustiveness? nonExhaustiveness = + _exhaustivenessData.nonExhaustivenesses[node]; + if (nonExhaustiveness != null) { + features[Tags.error] = nonExhaustivenessToText(nonExhaustiveness); } } else if (node is SwitchMember || node is SwitchExpressionCase) { Space? caseSpace = _exhaustivenessData.caseSpaces[node]; if (caseSpace != null) { features[Tags.space] = spacesToText(caseSpace); } - ExhaustivenessError? error = _exhaustivenessData.errors[node]; - if (error != null) { - features[Tags.error] = errorToText(error); + CaseUnreachability? caseUnreachability = + _exhaustivenessData.caseUnreachabilities[node]; + if (caseUnreachability != null) { + features[Tags.error] = 'unreachable'; } } return features.isNotEmpty ? features : null; diff --git a/pkg/analyzer/test/src/dart/resolution/record_pattern_test.dart b/pkg/analyzer/test/src/dart/resolution/record_pattern_test.dart index 80814a6bbcef..f98734936423 100644 --- a/pkg/analyzer/test/src/dart/resolution/record_pattern_test.dart +++ b/pkg/analyzer/test/src/dart/resolution/record_pattern_test.dart @@ -498,6 +498,7 @@ void f(() x) { } ''', [ error(WarningCode.DEAD_CODE, 60, 7), + error(WarningCode.UNREACHABLE_SWITCH_DEFAULT, 60, 7), ]); var node = findNode.singleGuardedPattern.pattern; assertResolvedNodeText(node, r''' diff --git a/pkg/analyzer/test/src/diagnostics/test_all.dart b/pkg/analyzer/test/src/diagnostics/test_all.dart index 48850f8e86e3..a13163f373e3 100644 --- a/pkg/analyzer/test/src/diagnostics/test_all.dart +++ b/pkg/analyzer/test/src/diagnostics/test_all.dart @@ -888,6 +888,7 @@ import 'unqualified_reference_to_non_local_static_member_test.dart' import 'unqualified_reference_to_static_member_of_extended_type_test.dart' as unqualified_reference_to_static_member_of_extended_type; import 'unreachable_switch_case_test.dart' as unreachable_switch_case; +import 'unreachable_switch_default_test.dart' as unreachable_switch_default; import 'unused_catch_clause_test.dart' as unused_catch_clause; import 'unused_catch_stack_test.dart' as unused_catch_stack; import 'unused_element_test.dart' as unused_element; @@ -1500,6 +1501,7 @@ main() { unqualified_reference_to_non_local_static_member.main(); unqualified_reference_to_static_member_of_extended_type.main(); unreachable_switch_case.main(); + unreachable_switch_default.main(); unused_catch_clause.main(); unused_catch_stack.main(); unused_element.main(); diff --git a/pkg/analyzer/test/src/diagnostics/unreachable_switch_default_test.dart b/pkg/analyzer/test/src/diagnostics/unreachable_switch_default_test.dart new file mode 100644 index 000000000000..844f78ad32ed --- /dev/null +++ b/pkg/analyzer/test/src/diagnostics/unreachable_switch_default_test.dart @@ -0,0 +1,91 @@ +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/src/error/codes.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../dart/resolution/context_collection_resolution.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UnreachableSwitchDefaultTest); + }); +} + +@reflectiveTest +class UnreachableSwitchDefaultTest extends PubPackageResolutionTest { + test_bool() async { + await assertErrorsInCode(r''' +void f(bool x) { + switch (x) { + case false: + case true: + default: + break; + } +} +''', [ + error(WarningCode.UNREACHABLE_SWITCH_DEFAULT, 67, 7), + ]); + } + + test_enum() async { + await assertErrorsInCode(r''' +enum E { e1, e2 } + +String f(E e) { + switch (e) { + case E.e1: + return 'e1'; + case E.e2: + return 'e2'; + default: + return 'Some other value of E (impossible)'; + } +} +''', [ + error(WarningCode.UNREACHABLE_SWITCH_DEFAULT, 122, 7), + ]); + } + + test_not_always_exhaustive() async { + // If the type being switched on isn't "always exhaustive", the diagnostic + // isn't reported, because flow analysis might not understand that the + // switch cases fully exhaust the switch, so removing the default clause + // might result in spurious errors. + await assertNoErrorsInCode(r''' +String f(List x) { + switch (x) { + case []: + return 'empty'; + case [var y, ...]: + return 'non-empty starting with $y'; + default: + return 'impossible'; + } +} +'''); + } + + test_sealed_class() async { + await assertErrorsInCode(r''' +sealed class A {} +class B extends A {} +class C extends A {} + +String f(A x) { + switch (x) { + case B(): + return 'B'; + case C(): + return 'C'; + default: + return 'Some other subclass of A (impossible)'; + } +} +''', [ + error(WarningCode.UNREACHABLE_SWITCH_DEFAULT, 160, 7), + ]); + } +} diff --git a/pkg/analyzer/tool/diagnostics/diagnostics.md b/pkg/analyzer/tool/diagnostics/diagnostics.md index 3eb37202778c..dd8754856dc8 100644 --- a/pkg/analyzer/tool/diagnostics/diagnostics.md +++ b/pkg/analyzer/tool/diagnostics/diagnostics.md @@ -23108,6 +23108,52 @@ void f(int x) { } ``` +### unreachable_switch_default + +_This default clause is covered by the previous cases._ + +#### Description + +The analyzer produces this diagnostic when a `default` clause in a +`switch` statement doesn't match anything because all of the matchable +values are matched by an earlier `case` clause. + +#### Example + +The following code produces this diagnostic because the values `E.e1` and +`E.e2` were matched in the preceding cases: + +```dart +enum E { e1, e2 } + +void f(E x) { + switch (x) { + case E.e1: + print('one'); + case E.e2: + print('two'); + [!default!]: + print('other'); + } +} +``` + +#### Common fixes + +Remove the unnecessary `default` clause: + +```dart +enum E { e1, e2 } +void f(E x) { + switch (x) { + case E.e1: + print('one'); + case E.e2: + print('two'); + } +} +``` + ### unused_catch_clause _The exception variable '{0}' isn't used, so the 'catch' clause can be removed._ diff --git a/pkg/front_end/lib/src/kernel/constant_evaluator.dart b/pkg/front_end/lib/src/kernel/constant_evaluator.dart index bc625a338e7e..4d14d8777f38 100644 --- a/pkg/front_end/lib/src/kernel/constant_evaluator.dart +++ b/pkg/front_end/lib/src/kernel/constant_evaluator.dart @@ -1335,42 +1335,43 @@ class ConstantsTransformer extends RemovingTransformer { cases.add(patternConverter.createRootSpace(type, patternGuard.pattern, hasGuard: patternGuard.guard != null)); } - List errors = reportErrors( - _exhaustivenessCache!, type, cases, - computeUnreachable: retainDataForTesting); - List? reportedErrors; - if (_exhaustivenessDataForTesting != null) { - // Coverage-ignore-block(suite): Not run. - reportedErrors = []; - } - for (ExhaustivenessError error in errors) { - if (error is UnreachableCaseError) { - // Coverage-ignore-block(suite): Not run. - reportedErrors?.add(error); - } else if (error is NonExhaustiveError && - !hasDefault && - mustBeExhaustive) { + List? caseUnreachabilities = retainDataForTesting + ? // Coverage-ignore(suite): Not run. - reportedErrors?.add(error); - constantEvaluator.errorReporter.report( - constantEvaluator.createLocatedMessageWithOffset( - node, - fileOffset, - (isSwitchExpression - ? templateNonExhaustiveSwitchExpression - : templateNonExhaustiveSwitchStatement) - .withArguments( - expressionType, - error.witnesses.first.asWitness, - error.witnesses.first.asCorrection))); - } + [] + : null; + NonExhaustiveness? nonExhaustiveness = computeExhaustiveness( + _exhaustivenessCache!, type, cases, + caseUnreachabilities: caseUnreachabilities); + NonExhaustiveness? reportedNonExhaustiveness; + if (nonExhaustiveness != null && !hasDefault && mustBeExhaustive) { + reportedNonExhaustiveness = nonExhaustiveness; + constantEvaluator.errorReporter.report( + constantEvaluator.createLocatedMessageWithOffset( + node, + fileOffset, + (isSwitchExpression + ? templateNonExhaustiveSwitchExpression + : templateNonExhaustiveSwitchStatement) + .withArguments( + expressionType, + nonExhaustiveness.witnesses.first.asWitness, + nonExhaustiveness.witnesses.first.asCorrection))); } if (_exhaustivenessDataForTesting != null) { // Coverage-ignore-block(suite): Not run. _exhaustivenessDataForTesting.objectFieldLookup ??= _exhaustivenessCache; _exhaustivenessDataForTesting.switchResults[replacement] = - new ExhaustivenessResult(type, cases, - patternGuards.map((c) => c.fileOffset).toList(), reportedErrors!); + new ExhaustivenessResult( + type, + cases, + patternGuards.map((c) => c.fileOffset).toList(), + { + for (CaseUnreachability caseUnreachability + in caseUnreachabilities!) + caseUnreachability.index + }, + reportedNonExhaustiveness); } } diff --git a/pkg/front_end/lib/src/kernel/exhaustiveness.dart b/pkg/front_end/lib/src/kernel/exhaustiveness.dart index 3524704413d7..f2ebad167fe5 100644 --- a/pkg/front_end/lib/src/kernel/exhaustiveness.dart +++ b/pkg/front_end/lib/src/kernel/exhaustiveness.dart @@ -40,10 +40,11 @@ class ExhaustivenessResult { final StaticType scrutineeType; final List caseSpaces; final List caseOffsets; - final List errors; + final Set unreachableCases; + final NonExhaustiveness? nonExhaustiveness; - ExhaustivenessResult( - this.scrutineeType, this.caseSpaces, this.caseOffsets, this.errors); + ExhaustivenessResult(this.scrutineeType, this.caseSpaces, this.caseOffsets, + this.unreachableCases, this.nonExhaustiveness); } class CfeTypeOperations implements TypeOperations { diff --git a/pkg/front_end/test/id_tests/exhaustiveness_test.dart b/pkg/front_end/test/id_tests/exhaustiveness_test.dart index 1230ce6b224d..b573a0ec790a 100644 --- a/pkg/front_end/test/id_tests/exhaustiveness_test.dart +++ b/pkg/front_end/test/id_tests/exhaustiveness_test.dart @@ -101,20 +101,16 @@ class ExhaustivenessDataExtractor extends CfeDataExtractor { features[Tags.scrutineeFields] = fieldsToText(result.scrutineeType, _exhaustivenessData.objectFieldLookup!, fieldsOfInterest); } - for (ExhaustivenessError error in result.errors) { - if (error is NonExhaustiveError) { - features[Tags.error] = errorToText(error); - } + if (result.nonExhaustiveness case NonExhaustiveness nonExhaustiveness) { + features[Tags.error] = nonExhaustivenessToText(nonExhaustiveness); } Uri uri = node.location!.file; for (int i = 0; i < result.caseSpaces.length; i++) { int offset = result.caseOffsets[i]; Features caseFeatures = new Features(); caseFeatures[Tags.space] = spacesToText(result.caseSpaces[i]); - for (ExhaustivenessError error in result.errors) { - if (error is UnreachableCaseError && error.index == i) { - caseFeatures[Tags.error] = errorToText(error); - } + if (result.unreachableCases.contains(i)) { + caseFeatures[Tags.error] = 'unreachable'; } registerValue( uri, offset, new NodeId(offset, IdKind.node), caseFeatures, node); diff --git a/pkg/front_end/test/spell_checking_list_code.txt b/pkg/front_end/test/spell_checking_list_code.txt index 8cdd2df2e548..fbe570c7ad91 100644 --- a/pkg/front_end/test/spell_checking_list_code.txt +++ b/pkg/front_end/test/spell_checking_list_code.txt @@ -1972,6 +1972,8 @@ unparsed unpleasant unpromotable unqualified +unreachabilities +unreachability unreachable unregister unregistered diff --git a/pkg/linter/test/rules/no_duplicate_case_values_test.dart b/pkg/linter/test/rules/no_duplicate_case_values_test.dart index 1e8db7d2b9a2..980e8ffe5f22 100644 --- a/pkg/linter/test/rules/no_duplicate_case_values_test.dart +++ b/pkg/linter/test/rules/no_duplicate_case_values_test.dart @@ -65,6 +65,7 @@ void switchEnum() { ''', [ // No lint. error(WarningCode.UNREACHABLE_SWITCH_CASE, 139, 4), + error(WarningCode.UNREACHABLE_SWITCH_DEFAULT, 155, 7), ]); } diff --git a/pkg/test_runner/lib/src/command_output.dart b/pkg/test_runner/lib/src/command_output.dart index adc4a4ad1199..fac66a1b7a49 100644 --- a/pkg/test_runner/lib/src/command_output.dart +++ b/pkg/test_runner/lib/src/command_output.dart @@ -444,6 +444,7 @@ class AnalyzerError implements Comparable { 'unnecessary_null_assert_pattern', 'unnecessary_null_check_pattern', 'unreachable_switch_case', + 'unreachable_switch_default', }; /// The set of hints which must be expected in a test. Any hint not specified diff --git a/pkg/test_runner/lib/src/static_error.dart b/pkg/test_runner/lib/src/static_error.dart index dd30e53efd68..dfe3e245175b 100644 --- a/pkg/test_runner/lib/src/static_error.dart +++ b/pkg/test_runner/lib/src/static_error.dart @@ -84,6 +84,7 @@ class StaticError implements Comparable { "STATIC_WARNING.INVALID_SECTION_FORMAT", "STATIC_WARNING.SPEC_MODE_REMOVED", "STATIC_WARNING.UNREACHABLE_SWITCH_CASE", + "STATIC_WARNING.UNREACHABLE_SWITCH_DEFAULT", "STATIC_WARNING.UNRECOGNIZED_ERROR_CODE", "STATIC_WARNING.UNSUPPORTED_OPTION_WITH_LEGAL_VALUE", "STATIC_WARNING.UNSUPPORTED_OPTION_WITH_LEGAL_VALUES", diff --git a/tests/language/patterns/exhaustiveness/bool_switch_test.dart b/tests/language/patterns/exhaustiveness/bool_switch_test.dart index 6d0a89534230..fb918d1ff874 100644 --- a/tests/language/patterns/exhaustiveness/bool_switch_test.dart +++ b/tests/language/patterns/exhaustiveness/bool_switch_test.dart @@ -169,3 +169,19 @@ void unreachableCase3(bool? b) { break; } } + +void unreachableDefault(bool b) { + switch (b) /* Ok */ { + case true: + print('true'); + break; + case false: + print('false'); + break; + default: // Unreachable +// ^^^^^^^ +// [analyzer] STATIC_WARNING.UNREACHABLE_SWITCH_DEFAULT + print('default'); + break; + } +} diff --git a/tests/language/patterns/exhaustiveness/enum_switch_test.dart b/tests/language/patterns/exhaustiveness/enum_switch_test.dart index ffe949f1853e..291227da91fc 100644 --- a/tests/language/patterns/exhaustiveness/enum_switch_test.dart +++ b/tests/language/patterns/exhaustiveness/enum_switch_test.dart @@ -268,3 +268,22 @@ void unreachableCase5(Enum e) { break; } } + +void unreachableDefault(Enum e) { + switch (e) /* Ok */ { + case Enum.a: + print('a'); + break; + case Enum.b: + print('b'); + break; + case Enum.c: + print('c'); + break; + default: // Unreachable +// ^^^^^^^ +// [analyzer] STATIC_WARNING.UNREACHABLE_SWITCH_DEFAULT + print('default'); + break; + } +} diff --git a/tests/language/patterns/exhaustiveness/object_pattern_switch_test.dart b/tests/language/patterns/exhaustiveness/object_pattern_switch_test.dart index ce7c6a33ee10..f8a6378308d9 100644 --- a/tests/language/patterns/exhaustiveness/object_pattern_switch_test.dart +++ b/tests/language/patterns/exhaustiveness/object_pattern_switch_test.dart @@ -208,3 +208,25 @@ void unreachableCase3(A? r) { break; } } + +void unreachableDefault(A r) { + switch (r) /* Ok */ { + case A(a: Enum.a, b: false): + print('A(a, false)'); + break; + case A(a: Enum.b, b: false): + print('A(b, false)'); + break; + case A(a: Enum.a, b: true): + print('A(a, true)'); + break; + case A(a: Enum.b, b: true): + print('A(b, true)'); + break; + default: // Unreachable +// ^^^^^^^ +// [analyzer] STATIC_WARNING.UNREACHABLE_SWITCH_DEFAULT + print('default'); + break; + } +} diff --git a/tests/language/patterns/exhaustiveness/record_literal_named_switch_test.dart b/tests/language/patterns/exhaustiveness/record_literal_named_switch_test.dart index 9927525689eb..9825f0862d19 100644 --- a/tests/language/patterns/exhaustiveness/record_literal_named_switch_test.dart +++ b/tests/language/patterns/exhaustiveness/record_literal_named_switch_test.dart @@ -197,3 +197,49 @@ void unreachableCase3(({Enum a, bool b})? r) { break; } } + +void unreachableDefault(({Enum a, bool b}) r) { + switch (r) /* Ok */ { + case (a: Enum.a, b: false): + print('(a, false)'); + break; + case (a: Enum.b, b: false): + print('(b, false)'); + break; + case (a: Enum.a, b: true): + print('(a, true)'); + break; + case (a: Enum.b, b: true): + print('(b, true)'); + break; + default: // Unreachable +// ^^^^^^^ +// [analyzer] STATIC_WARNING.UNREACHABLE_SWITCH_DEFAULT + print('default'); + break; + } +} + +void unreachableDefaultNotAlwaysExhaustive(({Enum a, int i}) r) { + // If the type being switched on isn't "always exhaustive", no + // `UNREACHABLE_SWITCH_DEFAULT` warning is reported, because flow analysis + // might not understand that the switch cases fully exhaust the switch, so + // removing the default clause might result in spurious errors. + switch (r) /* Ok */ { + case (a: Enum.a, i: 0): + print('(a, 0)'); + break; + case (a: Enum.b, i: 0): + print('(b, 0)'); + break; + case (a: Enum.a, i: _): + print('(a, nonzero)'); + break; + case (a: Enum.b, i: _): + print('(b, nonzero)'); + break; + default: // Unreachable + print('default'); + break; + } +} diff --git a/tests/language/patterns/exhaustiveness/record_literal_switch_test.dart b/tests/language/patterns/exhaustiveness/record_literal_switch_test.dart index 15ee8edeca5e..a57ab700d0ba 100644 --- a/tests/language/patterns/exhaustiveness/record_literal_switch_test.dart +++ b/tests/language/patterns/exhaustiveness/record_literal_switch_test.dart @@ -197,3 +197,49 @@ void unreachableCase3((Enum, bool)? r) { break; } } + +void unreachableDefault((Enum, bool) r) { + switch (r) /* Ok */ { + case (Enum.a, false): + print('(a, false)'); + break; + case (Enum.b, false): + print('(b, false)'); + break; + case (Enum.a, true): + print('(a, true)'); + break; + case (Enum.b, true): + print('(b, true)'); + break; + default: // Unreachable +// ^^^^^^^ +// [analyzer] STATIC_WARNING.UNREACHABLE_SWITCH_DEFAULT + print('default'); + break; + } +} + +void unreachableDefaultNotAlwaysExhaustive((Enum, int) r) { + // If the type being switched on isn't "always exhaustive", no + // `UNREACHABLE_SWITCH_DEFAULT` warning is reported, because flow analysis + // might not understand that the switch cases fully exhaust the switch, so + // removing the default clause might result in spurious errors. + switch (r) /* Ok */ { + case (Enum.a, 0): + print('(a, 0)'); + break; + case (Enum.b, 0): + print('(b, 0)'); + break; + case (Enum.a, _): + print('(a, nonzero)'); + break; + case (Enum.b, _): + print('(b, nonzero)'); + break; + default: // Unreachable + print('default'); + break; + } +} diff --git a/tests/language/patterns/exhaustiveness/record_switch_test.dart b/tests/language/patterns/exhaustiveness/record_switch_test.dart index 18901308d660..84763d7af927 100644 --- a/tests/language/patterns/exhaustiveness/record_switch_test.dart +++ b/tests/language/patterns/exhaustiveness/record_switch_test.dart @@ -201,3 +201,25 @@ void unreachableCase3((Enum, bool)? r) { break; } } + +void unreachableDefault((Enum, bool) r) { + switch (r) /* Ok */ { + case r0: + print('(a, false)'); + break; + case r1: + print('(b, false)'); + break; + case r2: + print('(a, true)'); + break; + case r3: + print('(b, true)'); + break; + default: // Unreachable +// ^^^^^^^ +// [analyzer] STATIC_WARNING.UNREACHABLE_SWITCH_DEFAULT + print('default'); + break; + } +} diff --git a/tests/language/patterns/exhaustiveness/sealed_class_switch_test.dart b/tests/language/patterns/exhaustiveness/sealed_class_switch_test.dart index 753a5e2162d8..66fa1f1b2e2a 100644 --- a/tests/language/patterns/exhaustiveness/sealed_class_switch_test.dart +++ b/tests/language/patterns/exhaustiveness/sealed_class_switch_test.dart @@ -183,3 +183,22 @@ void unreachableCase3(A? a) { break; } } + +void unreachableDefault(A a) { + switch (a) /* Ok */ { + case B b: + print('B'); + break; + case C c: + print('C'); + break; + case D d: + print('D'); + break; + default: // Unreachable +// ^^^^^^^ +// [analyzer] STATIC_WARNING.UNREACHABLE_SWITCH_DEFAULT + print('default'); + break; + } +} diff --git a/tests/language/patterns/exhaustiveness/variable_pattern_switch_test.dart b/tests/language/patterns/exhaustiveness/variable_pattern_switch_test.dart index a3d48e479aa3..6a104bd786a0 100644 --- a/tests/language/patterns/exhaustiveness/variable_pattern_switch_test.dart +++ b/tests/language/patterns/exhaustiveness/variable_pattern_switch_test.dart @@ -175,3 +175,25 @@ void unreachableCase3((Enum, bool)? r) { break; } } + +void unreachableDefault((Enum, bool) r) { + switch (r) /* Ok */ { + case (Enum.a, false): + print('(a, false)'); + break; + case (Enum.b, false): + print('(b, false)'); + break; + case (Enum.a, true): + print('(a, true)'); + break; + case (Enum.b, true): + print('(b, true)'); + break; + default: // Unreachable +// ^^^^^^^ +// [analyzer] STATIC_WARNING.UNREACHABLE_SWITCH_DEFAULT + print('default'); + break; + } +} diff --git a/tests/language/patterns/invalid_inside_unary_pattern_parentheses_test.dart b/tests/language/patterns/invalid_inside_unary_pattern_parentheses_test.dart index e70820c66151..4d68771e03f9 100644 --- a/tests/language/patterns/invalid_inside_unary_pattern_parentheses_test.dart +++ b/tests/language/patterns/invalid_inside_unary_pattern_parentheses_test.dart @@ -11,7 +11,7 @@ test_cast_insideCast(x) { switch (x) { case (_ as int) as num: break; - default: + default: // ignore: unreachable_switch_default Expect.fail('failed to match'); } } @@ -20,7 +20,7 @@ test_cast_insideNullAssert(x) { switch (x) { case (_ as int)!: break; - default: + default: // ignore: unreachable_switch_default Expect.fail('failed to match'); } } @@ -38,7 +38,7 @@ test_nullAssert_insideCast(x) { switch (x) { case (_!) as num?: break; - default: + default: // ignore: unreachable_switch_default Expect.fail('failed to match'); } } @@ -47,7 +47,7 @@ test_nullAssert_insideNullAssert(x) { switch (x) { case (_!)!: break; - default: + default: // ignore: unreachable_switch_default Expect.fail('failed to match'); } } @@ -56,7 +56,7 @@ test_nullAssert_insideNullCheck(x) { switch (x) { case (_!)?: break; - default: + default: // ignore: unreachable_switch_default Expect.fail('failed to match'); } } @@ -74,7 +74,7 @@ test_nullCheck_insideNullAssert(x) { switch (x) { case (_?)!: break; - default: + default: // ignore: unreachable_switch_default Expect.fail('failed to match'); } } diff --git a/tests/language/patterns/switch_case_scope_test.dart b/tests/language/patterns/switch_case_scope_test.dart index c6cf014ca720..2bdf19f8f5c0 100644 --- a/tests/language/patterns/switch_case_scope_test.dart +++ b/tests/language/patterns/switch_case_scope_test.dart @@ -23,7 +23,7 @@ void switchStatementBodyScope() { // Not a collision: var x = 'local'; Expect.equals('local', x); - default: + default: // ignore: unreachable_switch_default Expect.fail('Should not reach this.'); } } @@ -44,7 +44,7 @@ void unsharedPatternVariableShadows() { // Assign to pattern variable. local = 'assigned'; - default: + default: // ignore: unreachable_switch_default Expect.fail('Should not reach this.'); }