visitSwitchStatement method
Visit a SSwitchStatement.
Implementation
@override
Object? visitSwitchStatement(SSwitchStatement node) {
final switchValue = node.expression!.accept<Object?>(this);
final switchEnvironment = Environment(enclosing: environment);
final previousEnvironment = environment;
environment = switchEnvironment;
// Build a map of labels to member indices for 'continue <label>'
final Map<String, int> labelToIndex = {};
for (int i = 0; i < node.members.length; i++) {
final member = node.members[i];
List<dynamic> labels = [];
if (member is SSwitchCase) {
labels = member.labels;
} else if (member is SSwitchDefault) {
labels = member.labels;
} else if (member is SSwitchPatternCase) {
labels = member.labels;
}
for (final label in labels) {
final labelName = (label is SLabel)
? label.label?.name
: (label as SAstNode).toString();
if (labelName != null) labelToIndex[labelName] = i;
}
}
bool matched = false; // Has any case matched the switchValue?
bool execute =
false; // Should we execute statements in the current/next section?
int startIndex = 0; // Starting index for processing members
try {
while (true) {
// outer loop for continue with label
try {
for (int i = startIndex; i < node.members.length; i++) {
final member = node.members[i];
List<SAstNode> statementsToExecute = [];
// Cluster C2: Dart 3 pattern cases do not fall through. When a
// SwitchPatternCase with a constant pattern matches and runs,
// we must break out of the for loop after executing — otherwise
// subsequent cases would run with `execute` still true,
// re-evaluating their bodies (e.g. reassigning `late final`
// locals → "already been assigned").
bool patternCaseMatchedThisIteration = false;
if (member is SSwitchCase) {
if (!matched) {
final caseValue = member.expression!.accept<Object?>(this);
Logger.debug(
"[Switch] Checking legacy case value: $caseValue against $switchValue",
);
// Cluster-26: see _matchAndBind SConstantPattern — the
// native enum / BridgedEnumValue boundary is asymmetric.
if (switchValue == caseValue ||
(caseValue != null && caseValue == switchValue)) {
matched = true;
execute = true;
Logger.debug("[Switch] Matched legacy case: $caseValue");
}
}
statementsToExecute = member.statements;
} else if (member is SSwitchPatternCase) {
// Try explicit cast to potentially help the linter
final gp = member.guardedPattern;
final pattern = gp.pattern;
if (pattern is SConstantPattern) {
// This handles 'case <constant>:'
if (!matched) {
// Access expression from the SConstantPattern
final caseValue = pattern.expression.accept<Object?>(this);
Logger.debug(
"[Switch] Checking pattern case value: $caseValue against $switchValue",
);
// Cluster-26: see _matchAndBind SConstantPattern — the
// native enum / BridgedEnumValue boundary is asymmetric.
if (switchValue == caseValue ||
(caseValue != null && caseValue == switchValue)) {
matched = true;
execute = true; // Start executing
patternCaseMatchedThisIteration = true;
Logger.debug("[Switch] Matched pattern case: $caseValue");
}
}
statementsToExecute = member.statements;
} else {
// Handle other pattern types using our improved _matchAndBind function
if (!matched) {
// Create a temporary environment for pattern matching in switch
final tempEnvironment = Environment(enclosing: environment);
try {
_matchAndBind(pattern, switchValue, tempEnvironment);
// If we get here, the pattern matched
matched = true;
// G-DOV-8 FIX: Execute pattern case body in the pattern's own scope
// and do NOT fall through to subsequent cases (Dart 3 semantics).
Logger.debug(
"[Switch] Matched pattern case: ${pattern.runtimeType}",
);
// Check guard clause (when)
final gpWhenClause = gp.whenClause;
if (gpWhenClause != null) {
final prevEnv = environment;
environment = tempEnvironment;
try {
final guardResult = gpWhenClause.expression
.accept<Object?>(this);
if (guardResult != true) {
environment = prevEnv;
// Guard failed, pattern doesn't match - reset matched so other cases can try
matched = false;
Logger.debug(
"[Switch] Guard clause failed, skipping case",
);
statementsToExecute = member.statements;
continue; // Skip to next case member
}
} catch (e) {
environment = prevEnv;
rethrow;
}
environment = prevEnv;
}
// Execute statements in the pattern environment
final prevEnv = environment;
environment = tempEnvironment;
try {
for (final statement in member.statements) {
statement.accept<Object?>(this);
}
} on BreakException catch (e) {
environment = prevEnv;
if (e.label == null ||
_currentStatementLabels.contains(e.label)) {
break; // Exit the loop over members
} else {
rethrow;
}
} on ContinueException catch (e) {
environment = prevEnv;
if (e.label != null &&
labelToIndex.containsKey(e.label)) {
startIndex = labelToIndex[e.label]!;
matched = true;
execute = true;
throw ContinueSwitchLabel();
} else {
rethrow;
}
} catch (e) {
environment = prevEnv;
rethrow;
}
environment = prevEnv;
// Pattern cases do not fall through - break out
break;
} on PatternMatchD4rtException catch (e) {
Logger.debug(
"[Switch] Pattern ${pattern.runtimeType} did not match: ${e.message}",
);
// Pattern didn't match, continue to next case
}
}
statementsToExecute = member.statements;
}
} else if (member is SSwitchDefault) {
Logger.debug("[Switch] Reached default case.");
if (!matched || execute) {
// Execute default if no match OR fallthrough
execute = true;
}
statementsToExecute = member.statements;
} else {
throw StateD4rtException(
'Unknown switch member type: ${member.runtimeType}',
);
}
// Execute statements if needed (either matched this round or fell through)
if (execute) {
Logger.debug(
"[Switch] Executing statements for matched/fallthrough/default...",
);
try {
for (final statement in statementsToExecute) {
statement.accept<Object?>(this);
}
// Fall-through continues if no break
} on BreakException catch (e) {
Logger.debug(
"[Switch] Caught BreakException (label: ${e.label}) with current labels: $_currentStatementLabels",
);
if (e.label == null ||
_currentStatementLabels.contains(e.label)) {
// Unlabeled break OR labeled break targeting this switch.
Logger.debug("[Switch] Breaking switch.");
execute = false; // Stop execution after this block
break; // Exit the loop over members
} else {
// Labeled break targeting an outer construct.
Logger.debug("[Switch] Rethrowing outer break...");
rethrow;
}
} on ContinueException catch (e) {
if (e.label != null && labelToIndex.containsKey(e.label)) {
// Continue to a labeled case in this switch
Logger.debug("[Switch] Continue to labeled case: ${e.label}");
startIndex = labelToIndex[e.label]!;
matched =
true; // Mark as matched so we execute the target case
execute = true;
throw ContinueSwitchLabel(); // Signal to restart from target index
} else {
// 'continue' without a label or with unknown label
throw RuntimeD4rtException(
"'continue' is not valid inside a switch case/default block without a loop target.",
);
}
}
}
// Cluster C2: Dart 3 SwitchPatternCase with a constant pattern
// does not fall through. After running its body, exit the
// member loop so subsequent cases are not re-executed.
//
// Cluster C12: …but only break when the matching case actually
// had statements. Dart 3 multi-case grouping —
// case A:
// case B:
// stmt;
// — is parsed as two SSwitchPatternCase nodes: A with empty
// statements and B with the body. The match fires on A, then
// we must fall through to B so the body runs. Breaking out on
// A would skip the body and silently return null from the
// enclosing function (see foundation/targetplatform_test.dart's
// switchFor helper).
if (patternCaseMatchedThisIteration &&
statementsToExecute.isNotEmpty) {
execute = false;
break;
}
}
// Normal completion - exit the while(true) loop
break;
} on ContinueSwitchLabel {
// Restart execution from startIndex (already set before throwing)
continue;
}
}
} finally {
environment = previousEnvironment;
}
return null; // Switch statements don't produce a value.
}