diff --git a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ast/LabeledStatement.java b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ast/LabeledStatement.java index 6cceb950492..d7684a1bf1f 100644 --- a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ast/LabeledStatement.java +++ b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ast/LabeledStatement.java @@ -21,6 +21,7 @@ import org.eclipse.jdt.internal.compiler.flow.LabelFlowContext; import org.eclipse.jdt.internal.compiler.flow.UnconditionalFlowInfo; import org.eclipse.jdt.internal.compiler.lookup.BlockScope; +import org.eclipse.jdt.internal.compiler.lookup.LocalVariableBinding; public class LabeledStatement extends Statement { @@ -154,6 +155,16 @@ public void traverse( visitor.endVisit(this, blockScope); } + @Override + public LocalVariableBinding[] bindingsWhenComplete() { + // If control can reach past this labeled statement via a break to our own + // label, the pattern variable(s) may not be matched at that exit point, + // so nothing may be treated as definitely matched on completion. + if (this.statement.breaksOut(this.label)) + return NO_VARIABLES; + return this.statement.bindingsWhenComplete(); + } + @Override public boolean doesNotCompleteNormally() { if (this.statement.breaksOut(this.label)) @@ -165,4 +176,4 @@ public boolean doesNotCompleteNormally() { public boolean completesByContinue() { return this.statement instanceof ContinueStatement; // NOT this.statement.continuesAtOuterLabel } -} +} \ No newline at end of file diff --git a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ast/Statement.java b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ast/Statement.java index ba8dfdd386c..d22cb187468 100644 --- a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ast/Statement.java +++ b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ast/Statement.java @@ -340,6 +340,9 @@ public boolean visit(Block block, BlockScope scope) { @Override public boolean visit(BreakStatement breakStatement, BlockScope skope) { + if (breakStatement.label != null && !CharOperation.equals(label, breakStatement.label)) { + return false; + } if (label == null || CharOperation.equals(label, breakStatement.label)) this.breaksOut = true; return false; diff --git a/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/PatternMatching16Test.java b/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/PatternMatching16Test.java index bfcd3365ed8..bae10cc773b 100644 --- a/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/PatternMatching16Test.java +++ b/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/PatternMatching16Test.java @@ -1,4 +1,4 @@ -/******************************************************************************* +/******************************************************************************* * Copyright (c) 2020, 2025 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 @@ -4871,4 +4871,158 @@ public static void main(String [] args) { + "false\n" + "true"); } + // A pattern variable declared in a while-condition must remain in scope after the + // loop when the only exit from the loop body is a 'break' to an *enclosing* label + // (here 'break X' leaves the block X, not the while), so normal loop completion is + // the only way to reach 'a2 = s;' and 's' is definitely matched there. + public void testLabeledBreakOutOfEnclosingBlock() { + runConformTest( + new String[] { + "X.java", + """ + public class X { + public static void foo(Object obj) { + Object obj1 = obj; + Object obj2 = null; + X: { + while (!(obj1 instanceof String s)) { + break X; + } + obj2 = s; + } + System.out.println(obj2); + } + public static void main(String[] argv) { + foo("a"); + } + } + """, + }, + "a"); + } + // A pattern variable from a labeled while-condition must be in scope after the loop + // when the body cannot break out at all (it always throws), so the loop can only be + // left by normal completion where the pattern is matched. + public void testLabeledWhileNoBreak() { + runConformTest( + new String[] { + "X.java", + """ + public class X { + public static void foo(Object obj) { + Object obj1 = obj; + Object obj2 = null; + X: while (!(obj1 instanceof String s)) { + throw new RuntimeException("no match"); + } + obj2 = s; + System.out.println(obj2); + } + public static void main(String[] argv) { + foo("a"); + } + } + """, + }, + "a"); + } + // When 'break X' targets the while loop's own label, the loop may be left with the + // pattern *unmatched*, so the pattern variable must NOT be in scope afterwards and a + // subsequent 'String s' is a legal fresh declaration (must not clash). + public void testLabeledWhileBreakThenRedeclare() { + runConformTest( + new String[] { + "X.java", + """ + public class X { + public static void foo(Object obj) { + Object obj1 = obj; + X: while (!(obj1 instanceof String s)) { + break X; + } + String s = "foo"; + System.out.println(s); + } + public static void main(String[] argv) { + foo("a"); + } + } + """, + }, + "foo"); + } + // When 'break X' targets the while loop's own label, the loop can complete with the + // pattern unmatched, so the pattern variable must not be resolvable after the loop. + public void testLabeledWhileBreakExitsLoop() { + Map options = getCompilerOptions(true); + runNegativeTest( + new String[] { + "X.java", + """ + public class X { + public static void foo(Object obj) { + Object obj1 = obj; + Object obj2 = null; + X: while (!(obj1 instanceof String s)) { + break X; + } + obj2 = s; + System.out.println(obj2); + } + public static void main(String[] argv) { + foo("a"); + } + } + """, + }, + "----------\n" + + "1. ERROR in X.java (at line 8)\n" + + " obj2 = s;\n" + + " ^\n" + + "s cannot be resolved to a variable\n" + + "----------\n", + "", + null, + true, + options); + } + // An inner 'break' that targets the while loop (not the enclosing label X) can leave + // the loop with the pattern unmatched, so the pattern variable must not be resolvable + // after the loop even though control also reaches there via 'break X'. + public void testInnerBreakExitsWhile() { + Map options = getCompilerOptions(true); + runNegativeTest( + new String[] { + "X.java", + """ + public class X { + public static void foo(Object obj) { + Object obj1 = obj; + Object obj2 = null; + X: { + while (!(obj1 instanceof String s)) { + if (obj1 == null) break; + break X; + } + obj2 = s; + } + System.out.println(obj2); + } + public static void main(String[] argv) { + foo("a"); + } + } + """, + }, + "----------\n" + + "1. ERROR in X.java (at line 10)\n" + + " obj2 = s;\n" + + " ^\n" + + "s cannot be resolved to a variable\n" + + "----------\n", + "", + null, + true, + options); + } }