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/14 11:21:29 UTC
(groovy) 10/20: GROOVY-12408: a switch that is the last statement of a body is a statement
This is an automated email from the ASF dual-hosted git repository.
paulk-asert pushed a commit to branch GROOVY_6_0_X
in repository https://gitbox.apache.org/repos/asf/groovy.git
commit 97fb434ed1136822a93b101f6b6cd60a1af0092a
Author: Paul King <pa...@asert.com.au>
AuthorDate: Sun Sep 13 20:38:31 2026 +1000
GROOVY-12408: a switch that is the last statement of a body is a statement
GROOVY-12399 restored Java's rule that position, not arm shape, decides
whether a switch produces a value, but carved out a third case: a switch
in implicit-return position stayed an expression that yielded null when
unmatched, so that a method whose body is a bare switch kept its 5.x
value. That carve-out cost more than it bought.
It made one piece of syntax mean two things. Under @CompileStatic a
`case label` with a declared-Object label resolved statically in that
position and dynamically once any statement followed the switch, so
adding a trailing line changed how labels matched. It also required
about 110 lines in AstBuilder to recognise implicit-return position,
duplicating what ReturnAdder already knows, on a different tree.
The carve-out existed because a SwitchStatement was a second-class
citizen in static compilation, not because the semantics needed it.
GROOVY-12405 gave statements the same jump-table dispatch and
GROOVY-12406 the same duplicate-label diagnostic, so nothing is left to
protect. A switch whose value is not used is now a statement wherever it
appears, and Groovy's implicit return does to it what it does to any
other statement, via ReturnAdder.
Three behaviours move, each toward consistency:
- A primitive-returning method whose bare switch matches nothing now
yields the type's default, as `int f(int i) { if (i == 1) return 1 }`
and the colon form always have. The arrow form was the only shape in
the language that threw there, GroovyCastException dynamically and
NullPointerException when statically compiled.
- `case label` in that position matches as it does when the value is
discarded, and as it did in 5.x.
- The node is a SwitchStatement, which is visible to AST transforms.
COMPATIBILITY.md already warns that audience about GROOVY-12255.
Deleted: the UNMATCHED_YIELDS_NULL marker and its three readers, and
isImplicitReturnPosition, returnsValue, isLastStatement and
isLenientPosition from AstBuilder. isSwitchValueUsed keeps the recursion
added for nested arrow arms, since an arm of a switch that is itself a
statement still produces nothing.
---
COMPATIBILITY.md | 33 +++--
.../groovy/ast/tools/SwitchExpressionUtils.java | 12 --
.../apache/groovy/parser/antlr4/AstBuilder.java | 142 +++------------------
.../classgen/asm/SwitchExpressionWriter.java | 31 +----
.../asm/sc/StaticTypesSwitchExpressionWriter.java | 5 +-
.../transform/stc/StaticTypeCheckingVisitor.java | 4 -
src/spec/doc/core-semantics.adoc | 11 +-
.../stc/TypeCheckingExtensionsTest.groovy | 4 +-
.../codehaus/groovy/classgen/Groovy12399.groovy | 38 +++---
.../sc/SwitchExpressionStaticCompileTest.groovy | 4 +-
10 files changed, 68 insertions(+), 216 deletions(-)
diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md
index fc7a95fcd1..9b54168beb 100644
--- a/COMPATIBILITY.md
+++ b/COMPATIBILITY.md
@@ -358,20 +358,25 @@ Groovy 6 compiles switch expressions (JEP 361) as first-class AST
immediately-called closure around a switch statement (the GROOVY-9272
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 closure, a
-script or a method that returns a value) 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
-desugared closure completed without a `return` and the expression evaluated
-to `null`. Under `@TypeChecked` / `@CompileStatic`, a non-exhaustive switch
-expression (no `default`, and not a complete enum) is a compile-time error.
+(GROOVY-12399, GROOVY-12408). A `switch` whose value is not used is a
+`SwitchStatement`, whatever its arm shape, and does nothing when unmatched.
+That includes the last statement of a closure, a script or a method: the
+implicit return applies to the statement, as it does to an `if` or a
+colon-form `switch`. In expression position it is a `SwitchExpression` and
+every path must `yield` or `throw`; write `return switch (...) {...}`, or
+assign the result, to ask for that.
+
+**Who is affected (runtime behaviour).** A switch *expression* whose selector
+matches no arm throws `IllegalStateException`, where the 4.x / 5.x desugared
+closure completed without a `return` and evaluated to `null`. A switch
+*statement* does nothing, as before. The difference is visible where a method
+body is a bare `switch`: it is a statement, so an unmatched selector leaves
+the method's default value (`null`, or `0` / `false` for a primitive return
+type) rather than the `null` a 5.x expression produced, and rather than the
+`GroovyCastException` 5.x raised when coercing that `null` to a primitive.
+Under `@TypeChecked` / `@CompileStatic`, a non-exhaustive switch expression
+(no `default`, and not a complete enum) is a compile-time error; a statement
+needs no `default`.
**Who is affected (AST tools).** Visitors, macros, and AST transforms that
assumed a switch expression was a `MethodCallExpression` wrapping a
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 e9922929e6..34f7668c0a 100644
--- a/src/main/java/org/apache/groovy/ast/tools/SwitchExpressionUtils.java
+++ b/src/main/java/org/apache/groovy/ast/tools/SwitchExpressionUtils.java
@@ -41,18 +41,6 @@ import java.util.List;
*/
public final class SwitchExpressionUtils {
- /**
- * Node metadata key on a {@code SwitchExpression} in implicit-return position
- * (the last statement of a closure, a script or a method that returns a
- * value): 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 d8e0e70922..8a3560d20d 100644
--- a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
+++ b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
@@ -164,7 +164,6 @@ 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;
@@ -1170,7 +1169,7 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
*/
private boolean isSwitchUsedAsStatement(final SwitchExpressionContext ctx) {
StatementContext statement = switchAsStatement(ctx);
- return statement != null && (!isSwitchValueUsed(statement) || isLenientPosition(statement));
+ return statement != null && !isSwitchValueUsed(statement);
}
/** Whether a switch-expression group uses arrow labels, making its sole statement the arm value. */
@@ -2449,48 +2448,30 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
// 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 closure, a script or
- // a method that returns a value) 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 (isLenientPosition(statement)) {
- se.putNodeMetaData(UNMATCHED_YIELDS_NULL, Boolean.TRUE);
- }
+ // not used, or whose arrow block arms do not yield, is a SwitchStatement,
+ // including where it is the last statement of a method or closure: the
+ // implicit return then applies to the statement, as it does to any other
+ // (GROOVY-12408).
+ if (expr instanceof SwitchExpression se && ctx.getParent() instanceof ExpressionStmtAltContext statement
+ && (switchExpressionHasIncompleteArm(se) || !isSwitchValueUsed(statement))) {
+ return configureAST(switchExpressionAsStatement(se), ctx);
}
return configureAST(new ExpressionStatement(expr), ctx);
}
/**
- * Whether the value of a switch expression statement is used: it is in
- * implicit-return position, or it is the expression of an arrow arm whose
- * own switch produces a value. An arm of a switch that is itself a
- * statement produces nothing, so a switch nested there is a statement too.
+ * Whether the value of a switch expression statement is used, which it is
+ * only as the expression of an arrow arm whose own switch produces a value.
+ * An arm of a switch that is itself a statement produces nothing, so a
+ * switch nested there is a statement too.
*/
private boolean isSwitchValueUsed(final StatementContext statement) {
SwitchExpressionContext arm = enclosingArrowArmSwitch(statement);
- if (arm != null) {
- StatementContext owner = switchAsStatement(arm);
- return owner == null || isSwitchValueUsed(owner);
- }
- return !isColonArm(statement) && isImplicitReturnPosition(statement);
- }
-
- /**
- * Whether an unmatched selector completes the switch with {@code null}
- * instead of throwing: implicit-return position, or an arrow arm of a
- * switch that is itself lenient (GROOVY-12399).
- */
- private boolean isLenientPosition(final StatementContext statement) {
- SwitchExpressionContext arm = enclosingArrowArmSwitch(statement);
- if (arm != null) {
- StatementContext owner = switchAsStatement(arm);
- return owner != null && isLenientPosition(owner);
+ if (arm == null) {
+ return false;
}
- return !isColonArm(statement) && isImplicitReturnPosition(statement);
+ StatementContext owner = switchAsStatement(arm);
+ return owner == null || isSwitchValueUsed(owner);
}
/** The switch expression whose arrow arm consists of {@code statement}, else null. */
@@ -2506,13 +2487,6 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
return null;
}
- /** Whether {@code statement} is in a colon arm, whose statements are discarded; its value comes from {@code yield}. */
- private static boolean isColonArm(final StatementContext statement) {
- ParserRuleContext blockStatements = statement.getParent() instanceof BlockStatementContext bs ? bs.getParent() : null;
- return blockStatements != null && blockStatements.getParent() instanceof SwitchBlockStatementExpressionGroupContext group
- && !isArrowGroup(group);
- }
-
/**
* The statement that {@code ctx} alone makes up, or null when the switch is
* part of a larger expression such as {@code foo(switch (x) {...})}, whose
@@ -2527,90 +2501,6 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
&& statement.getStart() == ctx.getStart() && statement.getStop() == ctx.getStop() ? statement : null;
}
- /**
- * Whether {@code statement} is where the compiler would add an implicit
- * {@code return} (see {@code ReturnAdder}): the last statement of a lambda,
- * closure or script body, or of a method that returns a value (not a
- * {@code void} method or a constructor), 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 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) {
- return returnsValue(parent.getParent());
- }
- if (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 the method or constructor declaring a body returns a value: not a
- * {@code void} method, a constructor or a compact constructor, whose bodies
- * get no implicit {@code return} (see {@code ReturnAdder}).
- */
- private boolean returnsValue(final ParserRuleContext declaration) {
- if (!(declaration instanceof MethodDeclarationContext method)) {
- return false; // compact constructor
- }
- if (asBoolean(method.returnType())) {
- return method.returnType().VOID() == null;
- }
- ClassNode classNode = classNodeStack.peek();
- return classNode == null || !this.visitMethodName(method.methodName()).equals(classNode.getNodeMetaData(CLASS_NAME));
- }
-
- /** 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 e3c3b9ef51..3a9c293aa4 100644
--- a/src/main/java/org/codehaus/groovy/classgen/asm/SwitchExpressionWriter.java
+++ b/src/main/java/org/codehaus/groovy/classgen/asm/SwitchExpressionWriter.java
@@ -34,8 +34,6 @@ 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;
@@ -215,39 +213,12 @@ public class SwitchExpressionWriter {
if (defaultStatement != null && !defaultStatement.isEmpty()) {
defaultStatement.visit(controller.getAcg());
if (maybeFallsThrough(defaultStatement)) {
- writeUnmatchedSelector(expression, selectorIndex, selectorType);
+ throwUnmatchedSelector(selectorIndex, selectorType);
}
return;
}
if (completeEnum) {
throwIncompatibleClassChangeError();
- } else {
- writeUnmatchedSelector(expression, selectorIndex, selectorType);
- }
- }
-
- /**
- * Whether an unmatched selector completes the expression with {@code null}
- * rather than throwing: a switch in implicit-return position (GROOVY-12399).
- *
- * @since 6.0.0
- */
- protected static boolean unmatchedYieldsNull(final SwitchExpression expression) {
- return Boolean.TRUE.equals(expression.getNodeMetaData(UNMATCHED_YIELDS_NULL));
- }
-
- /**
- * Completes the expression for an unmatched selector: {@code null} 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 (unmatchedYieldsNull(expression)) {
- // the result type is always a reference type, so a plain null needs no
- // cast; like `yield`, leave the operand stack model empty at the jump
- MethodVisitor mv = controller.getMethodVisitor();
- mv.visitInsn(ACONST_NULL);
- mv.visitJumpInsn(GOTO, controller.getCompileStack().requireSwitchExpressionContext().endLabel);
} else {
throwUnmatchedSelector(selectorIndex, selectorType);
}
diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesSwitchExpressionWriter.java b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesSwitchExpressionWriter.java
index f6523bc531..956e400909 100644
--- a/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesSwitchExpressionWriter.java
+++ b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesSwitchExpressionWriter.java
@@ -219,10 +219,7 @@ public class StaticTypesSwitchExpressionWriter extends SwitchExpressionWriter {
CompileStack compileStack = controller.getCompileStack();
boolean hasDefault = expression.getDefaultStatement() != null && !expression.getDefaultStatement().isEmpty();
- // in implicit-return position an unmatched selector, null or a constant
- // added after compilation, yields null like any other (GROOVY-12399)
- boolean complete = !hasDefault && !unmatchedYieldsNull(expression)
- && group.keys.size() == enumConstantCount(enumType);
+ boolean complete = !hasDefault && group.keys.size() == enumConstantCount(enumType);
// a null selector matches no constant label; with no default it must
// throw ISE, never the complete-enum ICCE
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 1accc410da..1ad292bf24 100644
--- a/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java
+++ b/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java
@@ -158,7 +158,6 @@ 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;
@@ -5285,9 +5284,6 @@ 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 cd31974002..d3768c234a 100644
--- a/src/spec/doc/core-semantics.adoc
+++ b/src/spec/doc/core-semantics.adoc
@@ -220,11 +220,12 @@ 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 closure, a script or a method that returns a value 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.
+and simply does nothing when unmatched. That includes a `switch` that is the
+last statement of a closure, a script or a method: the implicit return applies
+to the statement, exactly as it does to an `if` or a colon-form `switch`, so an
+unmatched selector leaves the body's value at the return type's default. Use
+`return switch (...) {...}`, or assign the result, for the expression rules
+(link:https://issues.apache.org/jira/browse/GROOVY-12408[GROOVY-12408]).
[source,groovy]
----
diff --git a/src/test/groovy/groovy/transform/stc/TypeCheckingExtensionsTest.groovy b/src/test/groovy/groovy/transform/stc/TypeCheckingExtensionsTest.groovy
index c8db0b3325..9064dd5a59 100644
--- a/src/test/groovy/groovy/transform/stc/TypeCheckingExtensionsTest.groovy
+++ b/src/test/groovy/groovy/transform/stc/TypeCheckingExtensionsTest.groovy
@@ -625,7 +625,7 @@ final class TypeCheckingExtensionsTest extends StaticTypeCheckingTestCase {
extension = 'groovy/transform/stc/SwitchExpressionIsCaseExtension.groovy'
shouldFailWithMessages '''
def m(Object o) {
- switch (o) {
+ return switch (o) {
case String -> 's'
default -> 'd'
}
@@ -634,7 +634,7 @@ final class TypeCheckingExtensionsTest extends StaticTypeCheckingTestCase {
shouldFailWithMessages '''
def m(int n) {
- switch (n) {
+ return switch (n) {
case 1..10 -> 'in'
default -> 'out'
}
diff --git a/src/test/groovy/org/codehaus/groovy/classgen/Groovy12399.groovy b/src/test/groovy/org/codehaus/groovy/classgen/Groovy12399.groovy
index b46220f418..797524b75b 100644
--- a/src/test/groovy/org/codehaus/groovy/classgen/Groovy12399.groovy
+++ b/src/test/groovy/org/codehaus/groovy/classgen/Groovy12399.groovy
@@ -22,7 +22,6 @@ import groovy.transform.CompileStatic
import org.codehaus.groovy.control.CompilationUnit
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer
-import org.codehaus.groovy.runtime.typehandling.GroovyCastException
import org.junit.jupiter.api.Test
import static groovy.test.GroovyAssert.assertScript
@@ -364,25 +363,22 @@ final class Groovy12399 {
}
@Test
- void implicitReturnWithPrimitiveReturnTypeUnboxesNullWhenUnmatched() {
- // The switch keeps its value, so a primitive return type converts the
- // null of an unmatched selector. Dynamically the value is untyped and
- // the conversion is a Groovy cast; under @CompileStatic the inferred
- // type is Integer and the conversion is an unboxing, which is what
- // Groovy does for any null wrapper, as `int m() { Integer i = null; i }`
- // shows in either mode. Note that every other way a method falls off
- // its end yields the type's default value instead.
- def script = """
+ void implicitReturnWithPrimitiveReturnTypeYieldsTheDefaultWhenUnmatched() {
+ // The switch is a statement, so an unmatched selector falls off the end
+ // of the method and the implicit return supplies the type's default,
+ // exactly as it does for `int f(int i) { if (i == 1) return 1 }` or for
+ // a colon-form switch. Until GROOVY-12408 the arrow form was the one
+ // shape in the language that threw here instead (GroovyCastException
+ // dynamically, NullPointerException when statically compiled).
+ assertBoth """
int f(int i) {
switch (i) {
case 1 -> 1
}
}
assert f(1) == 1
- f(2)
+ assert f(2) == 0
"""
- shouldFail(GroovyCastException) { assertScript(script) }
- shouldFail(NullPointerException) { new GroovyShell(staticConfig()).evaluate(script) }
}
@Test
@@ -435,11 +431,11 @@ final class Groovy12399 {
}
@Test
- void voidMethodAndConstructorEndWithAStatement() {
+ void voidConstructorAndValueMethodsEndWithAStatement() {
assertScript '''
import groovy.transform.ASTTest
import org.codehaus.groovy.ast.expr.SwitchExpression
- import org.codehaus.groovy.ast.stmt.ExpressionStatement
+ import org.codehaus.groovy.ast.stmt.ReturnStatement
import org.codehaus.groovy.ast.stmt.SwitchStatement
import static org.codehaus.groovy.control.CompilePhase.SEMANTIC_ANALYSIS
@@ -454,16 +450,24 @@ final class Groovy12399 {
})
void v(int i) { switch (i) { case 1 -> println 'one' } }
+ // the value of a method whose last statement is a switch comes
+ // from the implicit return, so the switch is a statement here too
@ASTTest(phase=SEMANTIC_ANALYSIS, value={
- def last = node.code.statements[-1]
- assert last instanceof ExpressionStatement && last.expression instanceof SwitchExpression
+ assert node.code.statements[-1] instanceof SwitchStatement
})
def value(int i) { switch (i) { case 1 -> 'one' } }
+
+ @ASTTest(phase=SEMANTIC_ANALYSIS, value={
+ def last = node.code.statements[-1]
+ assert last instanceof ReturnStatement && last.expression instanceof SwitchExpression
+ })
+ def asked(int i) { return switch (i) { case 1 -> 'one'; default -> 'other' } }
}
def c = new C(2)
c.v(2)
assert c.value(1) == 'one'
assert c.value(2) == null
+ assert c.asked(1) == 'one'
'''
}
diff --git a/src/test/groovy/org/codehaus/groovy/classgen/asm/sc/SwitchExpressionStaticCompileTest.groovy b/src/test/groovy/org/codehaus/groovy/classgen/asm/sc/SwitchExpressionStaticCompileTest.groovy
index 76548b86e6..8e147d87d7 100644
--- a/src/test/groovy/org/codehaus/groovy/classgen/asm/sc/SwitchExpressionStaticCompileTest.groovy
+++ b/src/test/groovy/org/codehaus/groovy/classgen/asm/sc/SwitchExpressionStaticCompileTest.groovy
@@ -126,7 +126,7 @@ final class SwitchExpressionStaticCompileTest extends AbstractBytecodeTestCase {
def bytecode = compile(method: 'm', '''\
@groovy.transform.CompileStatic
String m(int n) {
- switch (n) {
+ return switch (n) {
case 1 -> 'one'
case 300..400 -> 'range'
default -> 'other'
@@ -269,7 +269,7 @@ final class SwitchExpressionStaticCompileTest extends AbstractBytecodeTestCase {
assertScript '''
@groovy.transform.CompileStatic
String m(Object o, Object label) {
- switch (o) {
+ return switch (o) {
case label -> 's'
default -> 'd'
}