You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2026/09/13 09:54:59 UTC
(groovy) 01/05: GROOVY-12399: arrow switch statement with expression arms is compiled as a switch expression and throws when unmatched
This is an automated email from the ASF dual-hosted git repository.
paulk-asert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git
commit ee370a647797c8670efa63141fe70e2b0c256f3a
Author: Paul King <pa...@asert.com.au>
AuthorDate: Fri Sep 11 14:27:14 2026 +1000
GROOVY-12399: arrow switch statement with expression arms is compiled as a switch expression and throws when unmatched
A switch with -> arms is parsed as a SwitchExpression (switchStatement is
colon-only). Since GROOVY-12255 it was only rewritten to a SwitchStatement
when an arrow block arm did not yield, so a statement whose arms are single
expressions (or single-statement blocks) kept the expression rules: an
unmatched selector threw IllegalStateException and, under @TypeChecked /
@CompileStatic, a missing default was a compile error.
switch (i) {
case 1 -> r = 'one'
}
As in Java (JEP 361) the arrow only decides fall-through; the position
decides whether a value is produced. The parser now builds a SwitchStatement
for any switch whose value is not used, whatever the arm shape; expression
positions (declaration, return, argument, the arm of an enclosing switch
expression) keep the strict rules.
Implicit-return position (the last statement of a method, closure or script
body, followed through blocks, if/else branches, try/catch blocks and
colon-switch arms as ReturnAdder does) has no Java equivalent: the switch
keeps its value, as `collect { switch (it) {...} }` needs, but an unmatched
selector yields null instead of throwing and no exhaustiveness is required,
which is what 4.x/5.x did. The SwitchExpression carries
SwitchExpressionUtils.UNMATCHED_YIELDS_NULL; the writer completes with null
and the type checker skips the exhaustiveness check.
---
COMPATIBILITY.md | 13 +-
.../groovy/ast/tools/SwitchExpressionUtils.java | 11 +
.../apache/groovy/parser/antlr4/AstBuilder.java | 100 +++++++-
.../classgen/asm/SwitchExpressionWriter.java | 23 +-
.../transform/stc/StaticTypeCheckingVisitor.java | 4 +
src/spec/doc/core-semantics.adoc | 10 +
.../codehaus/groovy/classgen/Groovy12255.groovy | 7 +-
.../codehaus/groovy/classgen/Groovy12399.groovy | 280 +++++++++++++++++++++
8 files changed, 432 insertions(+), 16 deletions(-)
diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md
index b5b73636c1..65cb6caf54 100644
--- a/COMPATIBILITY.md
+++ b/COMPATIBILITY.md
@@ -356,10 +356,15 @@ Release notes for the 6.0 beta line should call this out (JIRA
Groovy 6 compiles switch expressions (JEP 361) as first-class AST
(`SwitchExpression` / `YieldStatement`) instead of desugaring them to an
immediately-called closure around a switch statement (the GROOVY-9272
-implementation shipped in 4.x / 5.x). A `switch` used as a statement
-whose `->` block arms do not `yield` is a `SwitchStatement` (those blocks
-need not `yield`); the same syntax in expression position is a
-`SwitchExpression` (every path must `yield` or `throw`).
+implementation shipped in 4.x / 5.x). As in Java (JEP 361) the `->` arm
+only decides fall-through; the position decides whether a value is produced
+(GROOVY-12399). A `switch` whose value is not used is a `SwitchStatement`,
+whatever its arm shape, and does nothing when unmatched. In expression
+position it is a `SwitchExpression` (every path must `yield` or `throw`).
+A switch in implicit-return position (the last statement of a method,
+closure or script body) keeps its value, but an unmatched selector yields
+`null` rather than throwing and no exhaustiveness is required, as in 4.x /
+5.x; write `return switch (...) {...}` for the strict expression rules.
**Who is affected (runtime behaviour).** A dynamic switch expression whose
selector matches no arm now throws `IllegalStateException`. Previously the
diff --git a/src/main/java/org/apache/groovy/ast/tools/SwitchExpressionUtils.java b/src/main/java/org/apache/groovy/ast/tools/SwitchExpressionUtils.java
index 34f7668c0a..50f1842ca7 100644
--- a/src/main/java/org/apache/groovy/ast/tools/SwitchExpressionUtils.java
+++ b/src/main/java/org/apache/groovy/ast/tools/SwitchExpressionUtils.java
@@ -41,6 +41,17 @@ import java.util.List;
*/
public final class SwitchExpressionUtils {
+ /**
+ * Node metadata key on a {@code SwitchExpression} in implicit-return position
+ * (the last statement of a method, closure or script body): the switch keeps
+ * its value, but an unmatched selector yields {@code null} instead of throwing
+ * and the type checker does not require exhaustiveness (GROOVY-12399).
+ *
+ * @since 6.0.0
+ */
+ public static final String UNMATCHED_YIELDS_NULL = "_SWITCH_UNMATCHED_YIELDS_NULL";
+
+
private SwitchExpressionUtils() {
}
diff --git a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
index 8363a81ef0..525ef0da5b 100644
--- a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
+++ b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
@@ -164,6 +164,7 @@ import java.util.stream.Collectors;
import static groovy.lang.Tuple.tuple;
import static org.apache.groovy.parser.antlr4.GroovyParser.*;
+import static org.apache.groovy.ast.tools.SwitchExpressionUtils.UNMATCHED_YIELDS_NULL;
import static org.apache.groovy.parser.antlr4.util.PositionConfigureUtils.configureAST;
import static org.apache.groovy.parser.antlr4.util.PositionConfigureUtils.configureEndPosition;
import static org.codehaus.groovy.ast.tools.GeneralUtils.assignX;
@@ -2449,18 +2450,101 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
@Override
public Statement visitCommandExprAlt(final CommandExprAltContext ctx) {
Expression expr = this.visitCommandExpression(ctx.commandExpression());
- // Statement-position `switch` with `->` is parsed as a SwitchExpression
- // (switchStatement is colon-only). If an arrow block does not yield,
- // rewrite it to SwitchStatement so the block need not yield (JEP 361
- // statement rules). Expression-position incomplete arms already errored.
- if (expr instanceof SwitchExpression se
- && ctx.getParent() instanceof ExpressionStmtAltContext
- && switchExpressionHasIncompleteArm(se)) {
- return configureAST(switchExpressionAsStatement(se), ctx);
+ // A `switch` with `->` is parsed as a SwitchExpression (switchStatement is
+ // colon-only), but as in Java (JEP 361) the arrow only decides fall-through;
+ // the position decides whether a value is produced. A switch whose value is
+ // not used, or whose arrow block arms do not yield, is a SwitchStatement.
+ // In implicit-return position (the last statement of a method, closure or
+ // script body) the value is used, but an unmatched selector yields null
+ // rather than throwing, as it did in 4.x/5.x (GROOVY-12399).
+ if (expr instanceof SwitchExpression se && ctx.getParent() instanceof ExpressionStmtAltContext statement) {
+ if (switchExpressionHasIncompleteArm(se) || !isSwitchValueUsed(statement)) {
+ return configureAST(switchExpressionAsStatement(se), ctx);
+ }
+ if (isImplicitReturnPosition(statement)) {
+ se.putNodeMetaData(UNMATCHED_YIELDS_NULL, Boolean.TRUE);
+ }
}
return configureAST(new ExpressionStatement(expr), ctx);
}
+ /**
+ * Whether the value of a switch expression statement is used: it is the
+ * expression of an arrow arm of an enclosing switch expression, or it is
+ * in implicit-return position.
+ */
+ private static boolean isSwitchValueUsed(final StatementContext statement) {
+ ParserRuleContext blockStatements = statement.getParent() instanceof BlockStatementContext bs ? bs.getParent() : null;
+ if (blockStatements != null && blockStatements.getParent() instanceof SwitchBlockStatementExpressionGroupContext) {
+ return true;
+ }
+ return isImplicitReturnPosition(statement);
+ }
+
+ /**
+ * Whether {@code statement} is where the compiler would add an implicit
+ * {@code return} (see {@code ReturnAdder}): the last statement of a method,
+ * lambda, closure or script body, followed through nested blocks, the
+ * branches of an {@code if}, the try and catch blocks of a {@code try}, a
+ * {@code synchronized} block and the arms of a colon-form switch statement
+ * (allowing for a trailing {@code break}). Loop bodies and {@code finally}
+ * blocks are not return positions.
+ */
+ private static boolean isImplicitReturnPosition(final ParserRuleContext statement) {
+ ParserRuleContext parent = statement.getParent();
+ if (parent instanceof LabeledStmtAltContext || parent instanceof ConditionalStmtAltContext
+ || parent instanceof TryCatchStmtAltContext) {
+ return isImplicitReturnPosition(parent);
+ }
+ if (parent instanceof IfElseStatementContext || parent instanceof SwitchStatementContext
+ || parent instanceof TryCatchStatementContext) {
+ return isImplicitReturnPosition(parent); // the containing statement alternative
+ }
+ if (parent instanceof CatchClauseContext) {
+ return isImplicitReturnPosition(parent.getParent());
+ }
+ if (parent instanceof MethodBodyContext || parent instanceof LambdaBodyContext || parent instanceof ClosureContext) {
+ return true;
+ }
+ if (parent instanceof BlockStmtAltContext || parent instanceof SynchronizedStmtAltContext) {
+ return isImplicitReturnPosition(parent);
+ }
+ if (parent instanceof BlockStatementsOptContext) {
+ return isImplicitReturnPosition(parent.getParent()); // block or closure
+ }
+ if (parent instanceof BlockContext) {
+ return isImplicitReturnPosition(parent.getParent()); // methodBody, lambdaBody, blockStmtAlt, synchronized, try, catch, finally, class body...
+ }
+ if (parent instanceof BlockStatementContext) {
+ return isLastStatement((BlockStatementsContext) parent.getParent(), parent) && isImplicitReturnPosition(parent.getParent());
+ }
+ if (parent instanceof BlockStatementsContext blockStatements) {
+ ParserRuleContext owner = blockStatements.getParent();
+ if (owner instanceof SwitchBlockStatementGroupContext) {
+ return isImplicitReturnPosition(owner.getParent()); // the switch statement
+ }
+ return isImplicitReturnPosition(owner); // blockStatementsOpt
+ }
+ if (parent instanceof ScriptStatementContext) {
+ ParserRuleContext script = parent.getParent();
+ ScriptStatementContext last = null;
+ for (ParseTree child : script.children) {
+ if (child instanceof ScriptStatementContext candidate && candidate.statement() != null) last = candidate;
+ }
+ return last == parent;
+ }
+ return false;
+ }
+
+ /** Whether {@code blockStatement} is the last one of {@code blockStatements}, allowing for a trailing {@code break}. */
+ private static boolean isLastStatement(final BlockStatementsContext blockStatements, final ParserRuleContext blockStatement) {
+ List<? extends BlockStatementContext> list = blockStatements.blockStatement();
+ int n = list.size();
+ if (n == 0) return false;
+ if (list.get(n - 1) == blockStatement) return true;
+ return n > 1 && list.get(n - 2) == blockStatement && list.get(n - 1).statement() instanceof BreakStmtAltContext;
+ }
+
@Override
public Expression visitCommandExpression(final CommandExpressionContext ctx) {
boolean hasArgumentList = asBoolean(ctx.enhancedArgumentListInPar());
diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/SwitchExpressionWriter.java b/src/main/java/org/codehaus/groovy/classgen/asm/SwitchExpressionWriter.java
index 3a9c293aa4..4950ae17a2 100644
--- a/src/main/java/org/codehaus/groovy/classgen/asm/SwitchExpressionWriter.java
+++ b/src/main/java/org/codehaus/groovy/classgen/asm/SwitchExpressionWriter.java
@@ -34,6 +34,8 @@ import org.objectweb.asm.MethodVisitor;
import java.util.List;
import static org.codehaus.groovy.ast.tools.GeneralUtils.maybeFallsThrough;
+import static org.apache.groovy.ast.tools.SwitchExpressionUtils.UNMATCHED_YIELDS_NULL;
+import static org.objectweb.asm.Opcodes.ACONST_NULL;
import static org.objectweb.asm.Opcodes.ATHROW;
import static org.objectweb.asm.Opcodes.DUP;
import static org.objectweb.asm.Opcodes.GOTO;
@@ -213,12 +215,31 @@ public class SwitchExpressionWriter {
if (defaultStatement != null && !defaultStatement.isEmpty()) {
defaultStatement.visit(controller.getAcg());
if (maybeFallsThrough(defaultStatement)) {
- throwUnmatchedSelector(selectorIndex, selectorType);
+ writeUnmatchedSelector(expression, selectorIndex, selectorType);
}
return;
}
if (completeEnum) {
throwIncompatibleClassChangeError();
+ } else {
+ writeUnmatchedSelector(expression, selectorIndex, selectorType);
+ }
+ }
+
+ /**
+ * Completes the expression for an unmatched selector: {@code null} (cast
+ * to the result type) for a switch in implicit-return position, otherwise
+ * an {@code IllegalStateException} (GROOVY-12399).
+ */
+ private void writeUnmatchedSelector(final SwitchExpression expression, final int selectorIndex, final ClassNode selectorType) {
+ if (Boolean.TRUE.equals(expression.getNodeMetaData(UNMATCHED_YIELDS_NULL))) {
+ CompileStack.SwitchExpressionContext context = controller.getCompileStack().requireSwitchExpressionContext();
+ OperandStack operandStack = controller.getOperandStack();
+ controller.getMethodVisitor().visitInsn(ACONST_NULL);
+ operandStack.push(ClassHelper.OBJECT_TYPE);
+ operandStack.doGroovyCast(context.resultType);
+ operandStack.remove(1);
+ controller.getMethodVisitor().visitJumpInsn(GOTO, context.endLabel);
} else {
throwUnmatchedSelector(selectorIndex, selectorType);
}
diff --git a/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java b/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java
index f0e37de4f5..83ce23c485 100644
--- a/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java
+++ b/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java
@@ -158,6 +158,7 @@ import java.util.stream.IntStream;
import static org.apache.groovy.ast.tools.ClassNodeUtils.getNestHost;
import static org.apache.groovy.ast.tools.MethodNodeUtils.withDefaultArgumentMethods;
+import static org.apache.groovy.ast.tools.SwitchExpressionUtils.UNMATCHED_YIELDS_NULL;
import static org.apache.groovy.ast.tools.SwitchExpressionUtils.enumConstantName;
import static org.apache.groovy.ast.tools.SwitchExpressionUtils.intConstant;
import static org.apache.groovy.ast.tools.SwitchExpressionUtils.isIntegralType;
@@ -5282,6 +5283,9 @@ trying: for (ClassNode[] signature : signatures) {
if (expression.getDefaultStatement() != null && !expression.getDefaultStatement().isEmpty()) {
return;
}
+ if (Boolean.TRUE.equals(expression.getNodeMetaData(UNMATCHED_YIELDS_NULL))) {
+ return; // GROOVY-12399: implicit-return position yields null when unmatched
+ }
Expression selector = expression.getExpression();
ClassNode selectorType = getType(selector);
if (selectorType != null && selectorType.isEnum() && coversAllEnumConstants(expression, selectorType)) {
diff --git a/src/spec/doc/core-semantics.adoc b/src/spec/doc/core-semantics.adoc
index 6564b49593..c9b8130fee 100644
--- a/src/spec/doc/core-semantics.adoc
+++ b/src/spec/doc/core-semantics.adoc
@@ -216,6 +216,16 @@ selector throws `IllegalStateException`. Under `@TypeChecked` /
`@CompileStatic` a non-exhaustive switch expression (no `default`, and not a
complete enum) is a compile-time error.
+As in Java, the arrow only decides fall-through; whether a `switch` is a
+statement or an expression is decided by its position
+(link:https://issues.apache.org/jira/browse/GROOVY-12399[GROOVY-12399]).
+A `switch` whose value is not used is a statement, whatever its arm shape,
+and simply does nothing when unmatched. A `switch` that is the last statement
+of a method, closure or script body is that body's implicit return value: it
+keeps its value, but an unmatched selector yields `null` rather than throwing
+and no exhaustiveness is required. Use `return switch (...) {...}` there for
+the strict expression rules.
+
[source,groovy]
----
include::../test/SemanticsTest.groovy[tags=switch_expression,indent=0]
diff --git a/src/test/groovy/org/codehaus/groovy/classgen/Groovy12255.groovy b/src/test/groovy/org/codehaus/groovy/classgen/Groovy12255.groovy
index 722e666fb3..e981e3a8f3 100644
--- a/src/test/groovy/org/codehaus/groovy/classgen/Groovy12255.groovy
+++ b/src/test/groovy/org/codehaus/groovy/classgen/Groovy12255.groovy
@@ -537,7 +537,7 @@ final class Groovy12255 {
enum Flag { ON, OFF }
String m(Flag f) {
- switch (f) {
+ return switch (f) {
case Flag.ON -> 'on'
case Flag.OFF -> 'off'
}
@@ -1045,11 +1045,12 @@ final class Groovy12255 {
@Test
void typeCheckedUnmatchedIsError() {
- // exhaustiveness is a static type-checker error
+ // exhaustiveness is a static type-checker error (in expression position;
+ // implicit-return position yields null when unmatched, GROOVY-12399)
def err = shouldFail(MultipleCompilationErrorsException, '''
@groovy.transform.TypeChecked
def meth(int a) {
- switch (a) {
+ return switch (a) {
case 1 -> 'one'
}
}
diff --git a/src/test/groovy/org/codehaus/groovy/classgen/Groovy12399.groovy b/src/test/groovy/org/codehaus/groovy/classgen/Groovy12399.groovy
new file mode 100644
index 0000000000..622178af3a
--- /dev/null
+++ b/src/test/groovy/org/codehaus/groovy/classgen/Groovy12399.groovy
@@ -0,0 +1,280 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.codehaus.groovy.classgen
+
+import groovy.transform.CompileStatic
+import org.codehaus.groovy.control.CompilerConfiguration
+import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer
+import org.junit.jupiter.api.Test
+
+import static groovy.test.GroovyAssert.assertScript
+import static groovy.test.GroovyAssert.shouldFail
+
+/**
+ * GROOVY-12399: a {@code switch} with arrow arms is a statement or an
+ * expression by position, as in Java (JEP 361), not by syntax. A switch
+ * whose value is not used does nothing when unmatched; a switch in
+ * implicit-return position keeps its value and yields {@code null} when
+ * unmatched, as in 4.x/5.x; every other expression position keeps the
+ * strict rules of GROOVY-12255.
+ */
+final class Groovy12399 {
+
+ private static void assertBoth(final String script) {
+ assertScript(script)
+ new GroovyShell(staticConfig()).evaluate(script)
+ }
+
+ private static CompilerConfiguration staticConfig() {
+ def config = new CompilerConfiguration()
+ config.addCompilationCustomizers(new ASTTransformationCustomizer(CompileStatic))
+ return config
+ }
+
+ @Test
+ void statementWithExpressionArmDoesNothingWhenUnmatched() {
+ assertBoth '''
+ def f(int i) {
+ def r = 'none'
+ switch (i) {
+ case 1 -> r = 'one'
+ }
+ r
+ }
+ assert f(1) == 'one'
+ assert f(42) == 'none'
+ '''
+ }
+
+ @Test
+ void statementWithClassLabelDoesNothingWhenUnmatched() {
+ assertBoth '''
+ def f(Object o) {
+ def r = 'none'
+ switch (o) {
+ case String -> r = 'str'
+ }
+ r
+ }
+ assert f('x') == 'str'
+ assert f(42) == 'none'
+ '''
+ }
+
+ @Test
+ void statementWithSingleStatementBlockArmDoesNothingWhenUnmatched() {
+ assertBoth '''
+ def f(int i) {
+ def r = 'none'
+ switch (i) {
+ case 1 -> { r = 'one' }
+ }
+ r
+ }
+ assert f(42) == 'none'
+ '''
+ }
+
+ @Test
+ void statementAsLastStatementOfLoopBody() {
+ assertBoth '''
+ def log = []
+ for (i in [1, 2, 3]) {
+ switch (i) {
+ case 2 -> log << 'two'
+ }
+ }
+ assert log == ['two']
+ '''
+ }
+
+ @Test
+ void statementInsideIfBranchNotInReturnPosition() {
+ assertBoth '''
+ def f(boolean b, int i) {
+ def r = 'none'
+ if (b) {
+ switch (i) {
+ case 1 -> r = 'one'
+ }
+ r = r + '!'
+ }
+ r
+ }
+ assert f(true, 42) == 'none!'
+ '''
+ }
+
+ @Test
+ void statementNeedsNoDefaultWhenTypeChecked() {
+ assertScript '''
+ import groovy.transform.TypeChecked
+ @TypeChecked
+ def f(Object o) {
+ def r = 'none'
+ switch (o) {
+ case String -> r = 'str'
+ }
+ r
+ }
+ assert f(42) == 'none'
+ '''
+ }
+
+ @Test
+ void expressionPositionsStillThrowWhenUnmatched() {
+ for (form in ['def r = switch (i) { case 1 -> \'one\' }; r',
+ 'return switch (i) { case 1 -> \'one\' }',
+ 'Objects.toString(switch (i) { case 1 -> \'one\' })']) {
+ assertScript """
+ import static groovy.test.GroovyAssert.shouldFail
+ def f(int i) {
+ $form
+ }
+ assert f(1) == 'one'
+ def e = shouldFail(IllegalStateException) { f(42) }
+ assert e.message.contains('does not cover the value 42')
+ """
+ }
+ }
+
+ @Test
+ void expressionPositionsStillRequireExhaustivenessWhenTypeChecked() {
+ for (form in ['def r = switch (i) { case 1 -> \'one\' }; r',
+ 'return switch (i) { case 1 -> \'one\' }']) {
+ def err = shouldFail """
+ import groovy.transform.TypeChecked
+ @TypeChecked
+ def f(int i) {
+ $form
+ }
+ """
+ assert err.message.contains('does not cover all possible input values')
+ }
+ }
+
+ @Test
+ void implicitReturnKeepsValueAndYieldsNullWhenUnmatched() {
+ assertBoth '''
+ def f(int i) {
+ switch (i) {
+ case 1 -> 'one'
+ }
+ }
+ assert f(1) == 'one'
+ assert f(42) == null
+ '''
+ }
+
+ @Test
+ void implicitReturnInClosure() {
+ assertBoth '''
+ def names = [1, 2, 3].collect {
+ switch (it) {
+ case 1 -> 'one'
+ case 2 -> 'two'
+ }
+ }
+ assert names == ['one', 'two', null]
+ '''
+ }
+
+ @Test
+ void implicitReturnAsLastScriptStatement() {
+ assert new GroovyShell().evaluate('''
+ def i = 1
+ switch (i) {
+ case 1 -> 'one'
+ }
+ ''') == 'one'
+ assert new GroovyShell().evaluate('''
+ def i = 42
+ switch (i) {
+ case 1 -> 'one'
+ }
+ ''') == null
+ }
+
+ @Test
+ void implicitReturnThroughIfElseTryAndColonSwitch() {
+ assertBoth '''
+ def viaIf(boolean b, int i) {
+ if (b) {
+ switch (i) { case 1 -> 'one' }
+ } else {
+ 'else'
+ }
+ }
+ assert viaIf(true, 1) == 'one'
+ assert viaIf(true, 42) == null
+ assert viaIf(false, 1) == 'else'
+
+ def viaTry(int i) {
+ try {
+ switch (i) { case 1 -> 'one' }
+ } catch (e) {
+ 'caught'
+ }
+ }
+ assert viaTry(1) == 'one'
+ assert viaTry(42) == null
+
+ def viaColonSwitch(int outer, int i) {
+ switch (outer) {
+ case 0:
+ switch (i) { case 1 -> 'one' }
+ break
+ default:
+ 'other'
+ }
+ }
+ assert viaColonSwitch(0, 1) == 'one'
+ assert viaColonSwitch(0, 42) == null
+ assert viaColonSwitch(9, 1) == 'other'
+ '''
+ }
+
+ @Test
+ void implicitReturnNeedsNoDefaultWhenTypeChecked() {
+ assertScript '''
+ import groovy.transform.TypeChecked
+ @TypeChecked
+ def f(int i) {
+ switch (i) {
+ case 1 -> 'one'
+ }
+ }
+ assert f(1) == 'one'
+ assert f(42) == null
+ '''
+ }
+
+ @Test
+ void implicitReturnWithNonYieldingBlockArmIsAStatement() {
+ assertBoth '''
+ def f(int i) {
+ switch (i) {
+ case 1 -> { println 'one' }
+ }
+ }
+ assert f(1) == null
+ assert f(42) == null
+ '''
+ }
+}