Skip to content

Commit a3f5cfd

Browse files
stereotype441Commit Queue
authored and
Commit Queue
committed
Patterns parsing: recover when switch statement syntax used for switch expressions.
This change ensure that if the user makes an erroneous switch expression by imitating the syntax of case statements (i.e. using `case`, `default`, `:` instead of `=>`, or `;` instead of `,`), error recovery will understand the user's intent, avoiding follow-on errors. Fixes #51886. Bug: #51886, #51943 Change-Id: Icd405d4fd4ddfb1aadcf0867e5a51ba898d4cdbc Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/293200 Reviewed-by: Konstantin Shcheglov <[email protected]> Commit-Queue: Paul Berry <[email protected]>
1 parent 832e8fa commit a3f5cfd

File tree

35 files changed

+1103
-14
lines changed

35 files changed

+1103
-14
lines changed

pkg/_fe_analyzer_shared/lib/src/messages/codes_generated.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2312,6 +2312,18 @@ const MessageCode messageDeclaredMemberConflictsWithOverriddenMembersCause =
23122312
severity: Severity.context,
23132313
problemMessage: r"""This is one of the overridden members.""");
23142314

2315+
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
2316+
const Code<Null> codeDefaultInSwitchExpression =
2317+
messageDefaultInSwitchExpression;
2318+
2319+
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
2320+
const MessageCode messageDefaultInSwitchExpression = const MessageCode(
2321+
"DefaultInSwitchExpression",
2322+
index: 153,
2323+
problemMessage:
2324+
r"""A switch expression may not use the `default` keyword.""",
2325+
correctionMessage: r"""Try replacing `default` with `_`.""");
2326+
23152327
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
23162328
const Template<Message Function(String name)>
23172329
templateDefaultValueInRedirectingFactoryConstructor =

pkg/_fe_analyzer_shared/lib/src/parser/parser_impl.dart

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10371,7 +10371,20 @@ class Parser {
1037110371
mayParseFunctionExpressions = false;
1037210372
while (true) {
1037310373
listener.beginSwitchExpressionCase();
10374-
token = parsePattern(token, PatternContext.matching);
10374+
next = token.next!;
10375+
if (optional('default', next)) {
10376+
reportRecoverableError(next, codes.messageDefaultInSwitchExpression);
10377+
listener.handleNoType(next);
10378+
listener.handleWildcardPattern(null, next);
10379+
token = next;
10380+
} else {
10381+
if (optional('case', next)) {
10382+
reportRecoverableError(
10383+
next, codes.templateUnexpectedToken.withArguments(next));
10384+
token = next;
10385+
}
10386+
token = parsePattern(token, PatternContext.matching);
10387+
}
1037510388
listener.handleSwitchExpressionCasePattern(token);
1037610389
Token? when;
1037710390
next = token.next!;
@@ -10400,6 +10413,12 @@ class Parser {
1040010413
if (optional(',', next)) {
1040110414
comma = token = next;
1040210415
next = token.next!;
10416+
} else if (optional(';', next)) {
10417+
// User accidentally used `;` instead of `,`
10418+
reportRecoverableError(
10419+
next, codes.templateExpectedButGot.withArguments(','));
10420+
comma = token = next;
10421+
next = token.next!;
1040310422
}
1040410423
if (optional('}', next)) {
1040510424
break;
@@ -10418,13 +10437,16 @@ class Parser {
1041810437
} else {
1041910438
// Scanner guarantees a closing curly bracket
1042010439
Token closingBracket = beginSwitch.endGroup!;
10421-
comma = findNextComma(next, closingBracket);
10440+
comma = findNextCommaOrSemicolon(next, closingBracket);
1042210441
if (comma == null) {
1042310442
reportRecoverableError(
1042410443
next, codes.templateExpectedButGot.withArguments('}'));
1042510444
next = closingBracket;
1042610445
break;
1042710446
} else {
10447+
// Note: `findNextCommaOrSemicolon` might have found a `;` instead
10448+
// of a `,`, but if it did, there's need to report an additional
10449+
// error.
1042810450
reportRecoverableError(
1042910451
next, codes.templateExpectedButGot.withArguments(','));
1043010452
token = comma;
@@ -10442,14 +10464,14 @@ class Parser {
1044210464
return token;
1044310465
}
1044410466

10445-
/// Finds and returns the next `,` token, starting at [token], but not
10467+
/// Finds and returns the next `,` or `;` token, starting at [token], but not
1044610468
/// searching beyond [limit]. If a begin token is encountered, the search
1044710469
/// proceeds after its matching end token, so the returned token (if any) will
1044810470
/// not be any more deeply nested than the starting point.
10449-
Token? findNextComma(Token token, Token limit) {
10471+
Token? findNextCommaOrSemicolon(Token token, Token limit) {
1045010472
while (true) {
1045110473
if (token.isEof || identical(token, limit)) return null;
10452-
if (optional(',', token)) return token;
10474+
if (optional(',', token) || optional(';', token)) return token;
1045310475
if (token is BeginToken) {
1045410476
token = token.endGroup!;
1045510477
} else {

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
@@ -2320,6 +2320,8 @@ ParserErrorCode.COVARIANT_MEMBER:
23202320
status: needsEvaluation
23212321
ParserErrorCode.COVARIANT_TOP_LEVEL_DECLARATION:
23222322
status: needsEvaluation
2323+
ParserErrorCode.DEFAULT_IN_SWITCH_EXPRESSION:
2324+
status: needsEvaluation
23232325
ParserErrorCode.DEFAULT_VALUE_IN_FUNCTION_TYPE:
23242326
status: needsEvaluation
23252327
ParserErrorCode.DEFERRED_AFTER_PREFIX:

pkg/analyzer/lib/src/dart/error/syntactic_errors.g.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ final fastaAnalyzerErrorCodes = <ErrorCode?>[
167167
ParserErrorCode.INVALID_INSIDE_UNARY_PATTERN,
168168
ParserErrorCode.LATE_PATTERN_VARIABLE_DECLARATION,
169169
ParserErrorCode.PATTERN_VARIABLE_DECLARATION_OUTSIDE_FUNCTION_OR_METHOD,
170+
ParserErrorCode.DEFAULT_IN_SWITCH_EXPRESSION,
170171
];
171172

172173
class ParserErrorCode extends ErrorCode {
@@ -415,6 +416,13 @@ class ParserErrorCode extends ErrorCode {
415416
correctionMessage: "Try removing the keyword 'covariant'.",
416417
);
417418

419+
/// No parameters.
420+
static const ParserErrorCode DEFAULT_IN_SWITCH_EXPRESSION = ParserErrorCode(
421+
'DEFAULT_IN_SWITCH_EXPRESSION',
422+
"A switch expression may not use the `default` keyword.",
423+
correctionMessage: "Try replacing `default` with `_`.",
424+
);
425+
418426
/// No parameters.
419427
static const ParserErrorCode DEFAULT_VALUE_IN_FUNCTION_TYPE = ParserErrorCode(
420428
'DEFAULT_VALUE_IN_FUNCTION_TYPE',

pkg/analyzer/lib/src/error/error_code_values.g.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,7 @@ const List<ErrorCode> errorCodeValues = [
652652
ParserErrorCode.COVARIANT_CONSTRUCTOR,
653653
ParserErrorCode.COVARIANT_MEMBER,
654654
ParserErrorCode.COVARIANT_TOP_LEVEL_DECLARATION,
655+
ParserErrorCode.DEFAULT_IN_SWITCH_EXPRESSION,
655656
ParserErrorCode.DEFAULT_VALUE_IN_FUNCTION_TYPE,
656657
ParserErrorCode.DEFERRED_AFTER_PREFIX,
657658
ParserErrorCode.DIRECTIVE_AFTER_DECLARATION,

pkg/analyzer/lib/src/fasta/ast_builder.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5505,7 +5505,9 @@ class AstBuilder extends StackListener {
55055505
void handleWildcardPattern(Token? keyword, Token wildcard) {
55065506
debugEvent('WildcardPattern');
55075507
assert(_featureSet.isEnabled(Feature.patterns));
5508-
assert(wildcard.lexeme == '_');
5508+
// Note: if `default` appears in a switch expression, parser error recovery
5509+
// treats it as a wildcard pattern.
5510+
assert(wildcard.lexeme == '_' || wildcard.lexeme == 'default');
55095511
var type = pop() as TypeAnnotationImpl?;
55105512
push(
55115513
WildcardPatternImpl(

pkg/analyzer/test/generated/patterns_parser_test.dart

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9393,6 +9393,46 @@ SwitchExpression
93939393
''');
93949394
}
93959395

9396+
test_switchExpression_recovery_caseKeyword() {
9397+
_parse('''
9398+
f(x) => switch (x) {
9399+
case 1 => 'one',
9400+
case 2 => 'two'
9401+
};
9402+
''', errors: [
9403+
error(ParserErrorCode.UNEXPECTED_TOKEN, 23, 4),
9404+
error(ParserErrorCode.UNEXPECTED_TOKEN, 42, 4),
9405+
]);
9406+
var node = findNode.switchExpression('switch');
9407+
assertParsedNodeText(node, r'''
9408+
SwitchExpression
9409+
switchKeyword: switch
9410+
leftParenthesis: (
9411+
expression: SimpleIdentifier
9412+
token: x
9413+
rightParenthesis: )
9414+
leftBracket: {
9415+
cases
9416+
SwitchExpressionCase
9417+
guardedPattern: GuardedPattern
9418+
pattern: ConstantPattern
9419+
expression: IntegerLiteral
9420+
literal: 1
9421+
arrow: =>
9422+
expression: SimpleStringLiteral
9423+
literal: 'one'
9424+
SwitchExpressionCase
9425+
guardedPattern: GuardedPattern
9426+
pattern: ConstantPattern
9427+
expression: IntegerLiteral
9428+
literal: 2
9429+
arrow: =>
9430+
expression: SimpleStringLiteral
9431+
literal: 'two'
9432+
rightBracket: }
9433+
''');
9434+
}
9435+
93969436
test_switchExpression_recovery_colonInsteadOfArrow() {
93979437
_parse('''
93989438
f(x) => switch (x) {
@@ -9433,6 +9473,44 @@ SwitchExpression
94339473
''');
94349474
}
94359475

9476+
test_switchExpression_recovery_defaultKeyword() {
9477+
_parse('''
9478+
f(x) => switch (x) {
9479+
1 => 'one',
9480+
default => 'other'
9481+
};
9482+
''', errors: [
9483+
error(ParserErrorCode.DEFAULT_IN_SWITCH_EXPRESSION, 37, 7),
9484+
]);
9485+
var node = findNode.switchExpression('switch');
9486+
assertParsedNodeText(node, r'''
9487+
SwitchExpression
9488+
switchKeyword: switch
9489+
leftParenthesis: (
9490+
expression: SimpleIdentifier
9491+
token: x
9492+
rightParenthesis: )
9493+
leftBracket: {
9494+
cases
9495+
SwitchExpressionCase
9496+
guardedPattern: GuardedPattern
9497+
pattern: ConstantPattern
9498+
expression: IntegerLiteral
9499+
literal: 1
9500+
arrow: =>
9501+
expression: SimpleStringLiteral
9502+
literal: 'one'
9503+
SwitchExpressionCase
9504+
guardedPattern: GuardedPattern
9505+
pattern: WildcardPattern
9506+
name: default
9507+
arrow: =>
9508+
expression: SimpleStringLiteral
9509+
literal: 'other'
9510+
rightBracket: }
9511+
''');
9512+
}
9513+
94369514
test_switchExpression_recovery_illegalFunctionExpressionInGuard() {
94379515
// If a function expression occurs in a guard, parsing skips to the case
94389516
// that follows.
@@ -9477,6 +9555,52 @@ SwitchExpression
94779555
''');
94789556
}
94799557

9558+
test_switchExpression_recovery_illegalFunctionExpressionInGuard_semicolon() {
9559+
// If a function expression occurs in a guard, parsing skips to the case
9560+
// that follows. The logic to skip to the next case understands that a
9561+
// naive user might have mistakenly used `;` instead of `,` to separate
9562+
// cases.
9563+
_parse('''
9564+
f(x) => switch (x) {
9565+
_ when () => true => 1;
9566+
_ => 2
9567+
};
9568+
''', errors: [
9569+
error(ParserErrorCode.EXPECTED_TOKEN, 41, 2),
9570+
]);
9571+
var node = findNode.switchExpression('switch');
9572+
assertParsedNodeText(node, r'''
9573+
SwitchExpression
9574+
switchKeyword: switch
9575+
leftParenthesis: (
9576+
expression: SimpleIdentifier
9577+
token: x
9578+
rightParenthesis: )
9579+
leftBracket: {
9580+
cases
9581+
SwitchExpressionCase
9582+
guardedPattern: GuardedPattern
9583+
pattern: WildcardPattern
9584+
name: _
9585+
whenClause: WhenClause
9586+
whenKeyword: when
9587+
expression: RecordLiteral
9588+
leftParenthesis: (
9589+
rightParenthesis: )
9590+
arrow: =>
9591+
expression: BooleanLiteral
9592+
literal: true
9593+
SwitchExpressionCase
9594+
guardedPattern: GuardedPattern
9595+
pattern: WildcardPattern
9596+
name: _
9597+
arrow: =>
9598+
expression: IntegerLiteral
9599+
literal: 2
9600+
rightBracket: }
9601+
''');
9602+
}
9603+
94809604
test_switchExpression_recovery_missingComma() {
94819605
// If the extra tokens after a switch case look like they could be a
94829606
// pattern, the parser assumes there's a missing comma.
@@ -9524,6 +9648,45 @@ SwitchExpression
95249648
''');
95259649
}
95269650

9651+
test_switchExpression_recovery_semicolonInsteadOfComma() {
9652+
_parse('''
9653+
f(x) => switch (x) {
9654+
1 => 'one';
9655+
2 => 'two'
9656+
};
9657+
''', errors: [
9658+
error(ParserErrorCode.EXPECTED_TOKEN, 33, 1),
9659+
]);
9660+
var node = findNode.switchExpression('switch');
9661+
assertParsedNodeText(node, r'''
9662+
SwitchExpression
9663+
switchKeyword: switch
9664+
leftParenthesis: (
9665+
expression: SimpleIdentifier
9666+
token: x
9667+
rightParenthesis: )
9668+
leftBracket: {
9669+
cases
9670+
SwitchExpressionCase
9671+
guardedPattern: GuardedPattern
9672+
pattern: ConstantPattern
9673+
expression: IntegerLiteral
9674+
literal: 1
9675+
arrow: =>
9676+
expression: SimpleStringLiteral
9677+
literal: 'one'
9678+
SwitchExpressionCase
9679+
guardedPattern: GuardedPattern
9680+
pattern: ConstantPattern
9681+
expression: IntegerLiteral
9682+
literal: 2
9683+
arrow: =>
9684+
expression: SimpleStringLiteral
9685+
literal: 'two'
9686+
rightBracket: }
9687+
''');
9688+
}
9689+
95279690
test_switchExpression_twoPatterns() {
95289691
_parse('''
95299692
f(x) => switch(x) {

pkg/front_end/lib/src/fasta/kernel/body_builder.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9365,7 +9365,9 @@ class BodyBuilder extends StackListenerImpl
93659365
libraryFeatures.patterns, wildcard.charOffset, wildcard.charCount);
93669366
TypeBuilder? type = pop(NullValues.TypeBuilder) as TypeBuilder?;
93679367
DartType? patternType = type?.build(libraryBuilder, TypeUse.variableType);
9368-
assert(wildcard.lexeme == '_');
9368+
// Note: if `default` appears in a switch expression, parser error recovery
9369+
// treats it as a wildcard pattern.
9370+
assert(wildcard.lexeme == '_' || wildcard.lexeme == 'default');
93699371
push(forest.createWildcardPattern(wildcard.charOffset, patternType));
93709372
}
93719373

pkg/front_end/messages.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6755,6 +6755,19 @@ PatternVariableDeclarationOutsideFunctionOrMethod:
67556755
var (a, b) = (0, 1);
67566756
}
67576757
6758+
DefaultInSwitchExpression:
6759+
problemMessage: A switch expression may not use the `default` keyword.
6760+
correctionMessage: Try replacing `default` with `_`.
6761+
analyzerCode: ParserErrorCode.DEFAULT_IN_SWITCH_EXPRESSION
6762+
index: 153
6763+
experiments: patterns
6764+
comment: No parameters.
6765+
script: |
6766+
void f(x) => switch (x) {
6767+
1 => 'one',
6768+
default => 'other'
6769+
};
6770+
67586771
WeakReferenceNotStatic:
67596772
problemMessage: "Weak reference pragma can be used on a static method only."
67606773

pkg/front_end/parser_testcases/patterns/switchExpression_recovery_bogusTokensAfterCase.dart.intertwined.expect

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ parseUnit(f)
8585
parseLiteralInt(=>)
8686
listener: handleLiteralInt(0)
8787
listener: endSwitchExpressionCase(null, =>, 0)
88-
findNextComma(:, })
88+
findNextCommaOrSemicolon(:, })
8989
reportRecoverableError(:, Message[ExpectedButGot, Expected '}' before this., null, {string: }}])
9090
listener: handleRecoverableError(Message[ExpectedButGot, Expected '}' before this., null, {string: }}], :, :)
9191
listener: endSwitchExpressionBlock(1, {, })

pkg/front_end/parser_testcases/patterns/switchExpression_recovery_bogusTokensAfterCase_laterComma.dart.intertwined.expect

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ parseUnit(f)
9393
parseLiteralInt(=>)
9494
listener: handleLiteralInt(0)
9595
listener: endSwitchExpressionCase(null, =>, 0)
96-
findNextComma(:, })
96+
findNextCommaOrSemicolon(:, })
9797
reportRecoverableError(:, Message[ExpectedButGot, Expected '}' before this., null, {string: }}])
9898
listener: handleRecoverableError(Message[ExpectedButGot, Expected '}' before this., null, {string: }}], :, :)
9999
listener: endSwitchExpressionBlock(1, {, })

pkg/front_end/parser_testcases/patterns/switchExpression_recovery_bogusTokensAfterCase_nestedComma.dart.intertwined.expect

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ parseUnit(f)
8585
parseLiteralInt(=>)
8686
listener: handleLiteralInt(0)
8787
listener: endSwitchExpressionCase(null, =>, 0)
88-
findNextComma(:, })
88+
findNextCommaOrSemicolon(:, })
8989
reportRecoverableError(:, Message[ExpectedButGot, Expected '}' before this., null, {string: }}])
9090
listener: handleRecoverableError(Message[ExpectedButGot, Expected '}' before this., null, {string: }}], :, :)
9191
listener: endSwitchExpressionBlock(1, {, })
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
f(x) => switch (x) {
2+
case 1 => 'one',
3+
case 2 => 'two'
4+
};

0 commit comments

Comments
 (0)