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/08/20 04:43:46 UTC
(groovy) 10/11: GROOVY-12255: Select isCase on switch-expression arms without widening transform
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 a6ba7b89e0e087b60d9a433816553c0cae2f328e
Author: Daniel Sun <su...@apache.org>
AuthorDate: Wed Aug 19 03:31:56 2026 +0900
GROOVY-12255: Select isCase on switch-expression arms without widening transform
Resolve a non-intrinsic case label as label.isCase(selector) in the type
checker and store the chosen MethodNode on the CaseStatement so static
codegen can emit a direct call. Keep ClassCodeExpressionTransformer.transform
generic: SwitchExpression.transformExpression still rewrites only the
selector and labels, and ResolveVisitor, StaticImportVisitor, static
compilation and GINQ visit the node themselves, the same way they walk
closures.
---
COMPATIBILITY.md | 19 +-
.../groovy/ast/tools/SwitchExpressionUtils.java | 241 +++++++++++++++++++++
.../codehaus/groovy/ast/expr/SwitchExpression.java | 19 +-
.../classgen/asm/SwitchExpressionWriter.java | 67 +-----
.../asm/sc/StaticTypesSwitchExpressionWriter.java | 101 ++++-----
.../codehaus/groovy/control/ResolveVisitor.java | 4 +
.../groovy/control/StaticImportVisitor.java | 5 +
.../transform/StaticTypesTransformation.java | 6 +-
.../transformers/StaticCompilationTransformer.java | 5 +
.../transform/stc/StaticTypeCheckingVisitor.java | 50 ++++-
.../stc/SwitchExpressionIsCaseExtension.groovy | 24 ++
.../stc/TypeCheckingExtensionsTest.groovy | 37 ++++
.../ast/tools/SwitchExpressionUtilsTest.groovy | 100 +++++++++
.../codehaus/groovy/classgen/Groovy12255.groovy | 215 ++++++------------
.../sc/SwitchExpressionStaticCompileTest.groovy | 84 ++++++-
.../ginq/provider/collection/GinqAstWalker.groovy | 3 +-
16 files changed, 691 insertions(+), 289 deletions(-)
diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md
index d204c23014..eb64e10cb5 100644
--- a/COMPATIBILITY.md
+++ b/COMPATIBILITY.md
@@ -278,11 +278,20 @@ assumed a switch expression was a `MethodCallExpression` wrapping a
`SwitchStatement` need to handle `SwitchExpression` and `YieldStatement`.
`GroovyCodeVisitor` supplies default methods so existing visitors keep
compiling. `SwitchExpression.transformExpression` rewrites the selector and
-case-label expressions only and does not copy arm statements. A
-`ClassCodeExpressionTransformer` (including `ResolveVisitor`) walks the
-node in place via `visitSwitchExpression`, so nested arm expressions still
-run through resolve and rewrite. A plain `ExpressionTransformer` leaves
-arm bodies untouched.
+case-label expressions only and does not copy arm statements.
+`ClassCodeExpressionTransformer.transform` stays generic — it
+delegates to `transformExpression`, the same way it treats
+`ClosureExpression`. `ResolveVisitor`, `StaticImportVisitor` and the static-compilation
+transformer visit the node from their own `transform` override so
+arm expressions still run through resolve, static-import rewrite
+and static compilation.
+A plain `ExpressionTransformer` leaves arm bodies untouched.
+
+Under `@CompileStatic`, a non-intrinsic `case` label is resolved as
+`label.isCase(selector)` and emitted as a direct method call. An
+`Object`-typed label therefore uses `isCase(Object, Object)` (equals),
+the same as a statically compiled `label.isCase(selector)` call. Dynamic
+Groovy still dispatches `isCase` on the label's runtime class.
**What is *not* claimed.** Matching still uses Groovy `isCase` (Class, regex,
Collection, Closure). `tableswitch` / `lookupswitch` are emitted only when
diff --git a/src/main/java/org/apache/groovy/ast/tools/SwitchExpressionUtils.java b/src/main/java/org/apache/groovy/ast/tools/SwitchExpressionUtils.java
new file mode 100644
index 0000000000..34f7668c0a
--- /dev/null
+++ b/src/main/java/org/apache/groovy/ast/tools/SwitchExpressionUtils.java
@@ -0,0 +1,241 @@
+/*
+ * 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.apache.groovy.ast.tools;
+
+import org.codehaus.groovy.ast.ClassHelper;
+import org.codehaus.groovy.ast.ClassNode;
+import org.codehaus.groovy.ast.DynamicVariable;
+import org.codehaus.groovy.ast.FieldNode;
+import org.codehaus.groovy.ast.expr.ClassExpression;
+import org.codehaus.groovy.ast.expr.ConstantExpression;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.expr.PropertyExpression;
+import org.codehaus.groovy.ast.expr.VariableExpression;
+import org.codehaus.groovy.ast.stmt.CaseStatement;
+
+import java.util.List;
+
+/**
+ * Shared classification of switch-expression labels for static compilation.
+ * Intrinsic dispatch ({@code tableswitch} / {@code lookupswitch} / enum name)
+ * is used only when every case label is a compile-time constant of a type
+ * {@code javac} would switch on. Mixed labels fall back to {@code isCase}.
+ *
+ * @since 6.0.0
+ */
+public final class SwitchExpressionUtils {
+
+ private SwitchExpressionUtils() {
+ }
+
+ /**
+ * True when the selector type and every case label permit tableswitch,
+ * lookupswitch, or enum-name dispatch. An empty case list, a {@code null}
+ * label, or a single non-constant label (a range, a variable, …) makes
+ * the whole switch an {@code isCase} switch.
+ *
+ * @param selectorType the inferred type of the selector
+ * @param caseStatements the case arms
+ * @return {@code true} if codegen can emit an intrinsic switch
+ */
+ public static boolean isOptimizedSwitch(final ClassNode selectorType, final List<CaseStatement> caseStatements) {
+ if (selectorType == null || caseStatements == null || caseStatements.isEmpty()) {
+ return false;
+ }
+ return isOptimizedIntSwitch(selectorType, caseStatements)
+ || isOptimizedStringSwitch(selectorType, caseStatements)
+ || isOptimizedEnumSwitch(selectorType, caseStatements);
+ }
+
+ /**
+ * True when the selector is an integral primitive or wrapper and every
+ * label is an int-family constant.
+ *
+ * @param selectorType the inferred type of the selector
+ * @param caseStatements the case arms
+ * @return {@code true} if int tableswitch / lookupswitch applies
+ */
+ public static boolean isOptimizedIntSwitch(final ClassNode selectorType, final List<CaseStatement> caseStatements) {
+ if (!isIntegralSelector(selectorType) || caseStatements == null || caseStatements.isEmpty()) {
+ return false;
+ }
+ for (CaseStatement caseStatement : caseStatements) {
+ if (intConstant(caseStatement.getExpression()) == null) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * True when the selector is {@link String} and every label is a string constant.
+ *
+ * @param selectorType the inferred type of the selector
+ * @param caseStatements the case arms
+ * @return {@code true} if string lookupswitch applies
+ */
+ public static boolean isOptimizedStringSwitch(final ClassNode selectorType, final List<CaseStatement> caseStatements) {
+ if (selectorType == null || !ClassHelper.isStringType(selectorType)
+ || caseStatements == null || caseStatements.isEmpty()) {
+ return false;
+ }
+ for (CaseStatement caseStatement : caseStatements) {
+ if (stringConstant(caseStatement.getExpression()) == null) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * True when the selector is an enum and every label names a constant of that enum.
+ *
+ * @param selectorType the inferred type of the selector
+ * @param caseStatements the case arms
+ * @return {@code true} if enum-name dispatch applies
+ */
+ public static boolean isOptimizedEnumSwitch(final ClassNode selectorType, final List<CaseStatement> caseStatements) {
+ ClassNode enumType = unwrapEnumType(selectorType);
+ if (enumType == null || !enumType.isEnum() || caseStatements == null || caseStatements.isEmpty()) {
+ return false;
+ }
+ for (CaseStatement caseStatement : caseStatements) {
+ if (enumConstantName(caseStatement.getExpression(), enumType) == null) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * True for {@code byte}, {@code short}, {@code char}, {@code int} and their wrappers.
+ *
+ * @param type the selector type
+ * @return {@code true} if the type can feed a JVM int switch
+ */
+ public static boolean isIntegralSelector(final ClassNode type) {
+ return isIntegralType(type) || isIntegralWrapper(type);
+ }
+
+ /**
+ * True for the integral primitive types {@code javac} switches on.
+ *
+ * @param type the type to test
+ * @return {@code true} if the type is {@code int}, {@code byte}, {@code short}, or {@code char}
+ */
+ public static boolean isIntegralType(final ClassNode type) {
+ return type != null && (ClassHelper.isPrimitiveInt(type) || ClassHelper.isPrimitiveByte(type)
+ || ClassHelper.isPrimitiveShort(type) || ClassHelper.isPrimitiveChar(type));
+ }
+
+ /**
+ * True for the wrappers of the integral primitive types {@code javac} switches on.
+ *
+ * @param type the type to test
+ * @return {@code true} if the type is {@code Integer}, {@code Byte}, {@code Short}, or {@code Character}
+ */
+ public static boolean isIntegralWrapper(final ClassNode type) {
+ return type != null && (ClassHelper.isWrapperInteger(type) || ClassHelper.isWrapperByte(type)
+ || ClassHelper.isWrapperShort(type) || ClassHelper.isWrapperCharacter(type));
+ }
+
+ /**
+ * Extracts an int-family constant from a case label, or {@code null}.
+ *
+ * @param expression the case label
+ * @return the constant int value, or {@code null} if the label is not an int-family constant
+ */
+ public static Integer intConstant(final Expression expression) {
+ if (!(expression instanceof ConstantExpression constant)) return null;
+ Object value = constant.getValue();
+ if (value instanceof Integer || value instanceof Byte || value instanceof Short) {
+ return ((Number) value).intValue();
+ }
+ if (value instanceof Character) {
+ return (int) (Character) value;
+ }
+ return null;
+ }
+
+ /**
+ * Extracts a string constant from a case label, or {@code null}.
+ *
+ * @param expression the case label
+ * @return the string value, or {@code null} if the label is not a string constant
+ */
+ public static String stringConstant(final Expression expression) {
+ if (expression instanceof ConstantExpression constant && constant.getValue() instanceof String s) {
+ return s;
+ }
+ return null;
+ }
+
+ /**
+ * Returns the enum type behind {@code type}, or {@code null} if it is not an enum.
+ *
+ * @param type the selector type
+ * @return the enum {@link ClassNode}, or {@code null}
+ */
+ public static ClassNode unwrapEnumType(final ClassNode type) {
+ if (type == null) return null;
+ if (type.isEnum()) return type;
+ return type.redirect().isEnum() ? type.redirect() : null;
+ }
+
+ /**
+ * Extracts the enum constant name from a case label of {@code enumType}, or {@code null}.
+ * Accepts {@code EnumType.NAME}, an unqualified name that resolves to that constant,
+ * or a constant expression holding the enum value.
+ *
+ * @param expression the case label
+ * @param enumType the selector enum type
+ * @return the constant name, or {@code null} if the label is not a constant of {@code enumType}
+ */
+ public static String enumConstantName(final Expression expression, final ClassNode enumType) {
+ if (expression instanceof PropertyExpression property
+ && property.getObjectExpression() instanceof ClassExpression classExpression
+ && classExpression.getType().equals(enumType)
+ && property.getProperty() instanceof ConstantExpression name) {
+ return name.getText();
+ }
+ if (expression instanceof VariableExpression variable) {
+ var accessed = variable.getAccessedVariable();
+ if (accessed instanceof FieldNode field && field.isEnum()
+ && (field.getDeclaringClass() == null || field.getDeclaringClass().equals(enumType))) {
+ return field.getName();
+ }
+ if (accessed == null || accessed instanceof DynamicVariable) {
+ // a real local or parameter named like a constant is not a constant label
+ FieldNode field = enumType.getField(variable.getName());
+ if (field != null && field.isEnum()) {
+ return variable.getName();
+ }
+ }
+ }
+ if (expression instanceof ConstantExpression constant && enumType.isResolved()
+ && constant.getValue() instanceof Enum<?> e) {
+ Class<?> declaring = e.getDeclaringClass();
+ Class<?> enumClass = enumType.getTypeClass();
+ if (declaring == enumClass || enumClass.isAssignableFrom(declaring)) {
+ return e.name();
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/org/codehaus/groovy/ast/expr/SwitchExpression.java b/src/main/java/org/codehaus/groovy/ast/expr/SwitchExpression.java
index daa54153ee..8ce9b32569 100644
--- a/src/main/java/org/codehaus/groovy/ast/expr/SwitchExpression.java
+++ b/src/main/java/org/codehaus/groovy/ast/expr/SwitchExpression.java
@@ -18,7 +18,6 @@
*/
package org.codehaus.groovy.ast.expr;
-import org.codehaus.groovy.ast.ClassCodeExpressionTransformer;
import org.codehaus.groovy.ast.GroovyCodeVisitor;
import org.codehaus.groovy.ast.stmt.CaseStatement;
import org.codehaus.groovy.ast.stmt.EmptyStatement;
@@ -39,10 +38,8 @@ import java.util.List;
* <p>
* Arms stay as {@link CaseStatement}s, the same way a
* {@link ClosureExpression} holds a statement body: the case label is an
- * expression, the arm is a statement. A plain {@link ExpressionTransformer}
- * rewrites only the selector and case labels. A
- * {@link ClassCodeExpressionTransformer} walks the tree in place via
- * {@link ClassCodeExpressionTransformer#visitSwitchExpression}.
+ * expression, the arm is a statement. {@link #transformExpression} rewrites
+ * the selector and case labels only and shares the arm statements.
*
* @see SwitchStatement
* @see YieldStatement
@@ -153,18 +150,12 @@ public class SwitchExpression extends Expression {
}
/**
- * A {@link ClassCodeExpressionTransformer} walks this node in place through
- * {@link ClassCodeExpressionTransformer#visitSwitchExpression}, the same
- * pattern {@link ClosureExpression} uses so resolve, static-import and
- * similar rewrites still see nested arm expressions. Any other transformer
- * gets a copy of the selector and case labels; arm statements are shared.
+ * Returns a copy whose selector and case labels have been rewritten.
+ * Arm statements are shared: an {@link ExpressionTransformer} does not
+ * walk statements.
*/
@Override
public Expression transformExpression(final ExpressionTransformer transformer) {
- if (transformer instanceof ClassCodeExpressionTransformer visitor) {
- visitor.visitSwitchExpression(this);
- return this;
- }
List<CaseStatement> transformedCases = new ArrayList<>(caseStatements.size());
for (CaseStatement caseStatement : caseStatements) {
CaseStatement copy = new CaseStatement(
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 05e499ba06..3a9c293aa4 100644
--- a/src/main/java/org/codehaus/groovy/classgen/asm/SwitchExpressionWriter.java
+++ b/src/main/java/org/codehaus/groovy/classgen/asm/SwitchExpressionWriter.java
@@ -18,16 +18,12 @@
*/
package org.codehaus.groovy.classgen.asm;
+import org.apache.groovy.ast.tools.SwitchExpressionUtils;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
-import org.codehaus.groovy.ast.DynamicVariable;
import org.codehaus.groovy.ast.FieldNode;
-import org.codehaus.groovy.ast.expr.ClassExpression;
-import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.ast.expr.Expression;
-import org.codehaus.groovy.ast.expr.PropertyExpression;
import org.codehaus.groovy.ast.expr.SwitchExpression;
-import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.ast.stmt.CaseStatement;
import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.groovy.ast.stmt.YieldStatement;
@@ -160,9 +156,9 @@ public class SwitchExpressionWriter {
* Emits one {@code isCase} test. Shared with {@link StatementWriter} via
* {@link BinaryExpressionHelper#writeIsCase}.
*/
- protected void writeIsCaseComparison(final Expression caseValue,
+ protected void writeIsCaseComparison(final CaseStatement caseStatement,
final int selectorIndex, final ClassNode selectorType) {
- controller.getBinaryExpressionHelper().writeIsCase(selectorIndex, selectorType, caseValue);
+ controller.getBinaryExpressionHelper().writeIsCase(selectorIndex, selectorType, caseStatement.getExpression());
}
//--------------------------------------------------------------------------
@@ -190,7 +186,7 @@ public class SwitchExpressionWriter {
acg.onLineNumber(caseStatement, "visitCaseStatement");
- writeIsCaseComparison(caseStatement.getExpression(), selectorIndex, selectorType);
+ writeIsCaseComparison(caseStatement, selectorIndex, selectorType);
Label miss = controller.getOperandStack().jump(IFEQ);
@@ -274,38 +270,23 @@ public class SwitchExpressionWriter {
}
protected static boolean isIntegralType(final ClassNode type) {
- return ClassHelper.isPrimitiveInt(type) || ClassHelper.isPrimitiveByte(type)
- || ClassHelper.isPrimitiveShort(type) || ClassHelper.isPrimitiveChar(type);
+ return SwitchExpressionUtils.isIntegralType(type);
}
protected static boolean isIntegralWrapper(final ClassNode type) {
- return ClassHelper.isWrapperInteger(type) || ClassHelper.isWrapperByte(type)
- || ClassHelper.isWrapperShort(type) || ClassHelper.isWrapperCharacter(type);
+ return SwitchExpressionUtils.isIntegralWrapper(type);
}
protected static Integer intConstant(final Expression expression) {
- if (!(expression instanceof ConstantExpression constant)) return null;
- Object value = constant.getValue();
- if (value instanceof Integer || value instanceof Byte || value instanceof Short) {
- return ((Number) value).intValue();
- }
- if (value instanceof Character) {
- return (int) (Character) value;
- }
- return null;
+ return SwitchExpressionUtils.intConstant(expression);
}
protected static String stringConstant(final Expression expression) {
- if (expression instanceof ConstantExpression constant && constant.getValue() instanceof String s) {
- return s;
- }
- return null;
+ return SwitchExpressionUtils.stringConstant(expression);
}
protected static ClassNode unwrapEnumType(final ClassNode type) {
- if (type == null) return null;
- if (type.isEnum()) return type;
- return type.redirect().isEnum() ? type.redirect() : null;
+ return SwitchExpressionUtils.unwrapEnumType(type);
}
protected static int enumConstantCount(final ClassNode enumType) {
@@ -321,34 +302,6 @@ public class SwitchExpressionWriter {
}
protected static String enumConstantName(final Expression expression, final ClassNode enumType) {
- if (expression instanceof PropertyExpression property
- && property.getObjectExpression() instanceof ClassExpression classExpression
- && classExpression.getType().equals(enumType)
- && property.getProperty() instanceof ConstantExpression name) {
- return name.getText();
- }
- if (expression instanceof VariableExpression variable) {
- var accessed = variable.getAccessedVariable();
- if (accessed instanceof FieldNode field && field.isEnum()
- && (field.getDeclaringClass() == null || field.getDeclaringClass().equals(enumType))) {
- return field.getName();
- }
- if (accessed == null || accessed instanceof DynamicVariable) {
- // a real local or parameter named like a constant is not a constant label
- FieldNode field = enumType.getField(variable.getName());
- if (field != null && field.isEnum()) {
- return variable.getName();
- }
- }
- }
- if (expression instanceof ConstantExpression constant && enumType.isResolved()
- && constant.getValue() instanceof Enum<?> e) {
- Class<?> declaring = e.getDeclaringClass();
- Class<?> enumClass = enumType.getTypeClass();
- if (declaring == enumClass || enumClass.isAssignableFrom(declaring)) {
- return e.name();
- }
- }
- return null;
+ return SwitchExpressionUtils.enumConstantName(expression, enumType);
}
}
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 536a07188b..3449739e05 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
@@ -18,6 +18,7 @@
*/
package org.codehaus.groovy.classgen.asm.sc;
+import org.apache.groovy.ast.tools.ExpressionUtils;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.MethodNode;
@@ -46,9 +47,9 @@ import java.util.function.Function;
import static org.codehaus.groovy.ast.tools.GeneralUtils.args;
import static org.codehaus.groovy.ast.tools.GeneralUtils.callX;
-import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.chooseBestMethod;
-import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.findDGMMethodsByNameAndArguments;
import static org.objectweb.asm.Opcodes.GOTO;
+import static org.objectweb.asm.Opcodes.ICONST_0;
+import static org.objectweb.asm.Opcodes.ICONST_1;
import static org.objectweb.asm.Opcodes.IFEQ;
import static org.objectweb.asm.Opcodes.IFNULL;
import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
@@ -56,8 +57,8 @@ import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
/**
* Static-compilation writer for {@link SwitchExpression}. Emits
* {@code tableswitch} / {@code lookupswitch} when the selector and labels are
- * constants of a type {@code javac} would switch on, and otherwise a resolved
- * {@code isCase} call rather than a forced dynamic adapter invocation.
+ * constants of a type {@code javac} would switch on, and otherwise the
+ * {@code isCase} method selected by the type checker as a direct call.
*
* @since 6.0.0
*/
@@ -90,16 +91,26 @@ public class StaticTypesSwitchExpressionWriter extends SwitchExpressionWriter {
}
/**
- * Prefers a statically resolved {@code isCase} (DGM overload or instance
- * method) so Class / Collection / Pattern / Closure labels stay correct
- * without going through {@code ScriptBytecodeAdapter}.
+ * Emits the {@code isCase} call selected by the type checker. A literal
+ * {@code case null} is identity, not {@code DGM.isCase(Object,Object)}
+ * (which NPEs). Any other arm must already carry
+ * {@link StaticTypesMarker#DIRECT_METHOD_CALL_TARGET} on the
+ * {@link CaseStatement}.
*/
@Override
- protected void writeIsCaseComparison(final Expression caseValue,
+ protected void writeIsCaseComparison(final CaseStatement caseStatement,
final int selectorIndex, final ClassNode selectorType) {
- MethodNode target = resolveIsCaseTarget(caseValue, selectorType);
+ Expression caseValue = caseStatement.getExpression();
+ if (ExpressionUtils.isNullConstant(caseValue)) {
+ writeNullIdentity(selectorIndex, selectorType);
+ return;
+ }
+ MethodNode target = caseStatement.getNodeMetaData(StaticTypesMarker.DIRECT_METHOD_CALL_TARGET);
if (target == null) {
- super.writeIsCaseComparison(caseValue, selectorIndex, selectorType);
+ // Optimizer miss (e.g. inferred enum type but Object on the stack) or
+ // TypeCheckingMode.SKIP: keep ScriptBytecodeAdapter so the operand
+ // stack still has a boolean for the following IFEQ.
+ super.writeIsCaseComparison(caseStatement, selectorIndex, selectorType);
return;
}
OperandStack operandStack = controller.getOperandStack();
@@ -109,60 +120,38 @@ public class StaticTypesSwitchExpressionWriter extends SwitchExpressionWriter {
call.setMethodTarget(target);
call.putNodeMetaData(StaticTypesMarker.DIRECT_METHOD_CALL_TARGET, target);
call.putNodeMetaData(StaticTypesMarker.INFERRED_TYPE, ClassHelper.boolean_TYPE);
+ if (caseStatement.getNodeMetaData(StaticTypesMarker.PV_METHODS_ACCESS) != null) {
+ call.putNodeMetaData(StaticTypesMarker.PV_METHODS_ACCESS,
+ caseStatement.getNodeMetaData(StaticTypesMarker.PV_METHODS_ACCESS));
+ }
call.setSourcePosition(caseValue);
call.visit(controller.getAcg());
operandStack.doGroovyCast(ClassHelper.boolean_TYPE);
}
- private MethodNode resolveIsCaseTarget(final Expression caseValue, final ClassNode selectorType) {
- ClassNode caseType = controller.getTypeChooser().resolveType(caseValue, controller.getClassNode());
- ClassNode switchArg = ClassHelper.isPrimitiveType(selectorType)
- ? ClassHelper.getWrapper(selectorType) : selectorType;
- MethodNode instance = chooseInstanceIsCase(caseType, switchArg);
- if (instance != null) {
- return instance;
- }
- ClassLoader loader = controller.getSourceUnit().getClassLoader();
- List<MethodNode> methods = findDGMMethodsByNameAndArguments(
- loader, caseType, "isCase", new ClassNode[]{switchArg});
- if (methods.size() != 1) {
- return null;
- }
- MethodNode dgm = methods.get(0);
- // Object.equals-style isCase is not a substitute for runtime dispatch
- if (isGenericObjectIsCase(dgm)) {
- return null;
- }
- return dgm;
- }
-
- private static MethodNode chooseInstanceIsCase(final ClassNode caseType, final ClassNode switchArg) {
- if (caseType == null) return null;
- List<MethodNode> candidates = new ArrayList<>();
- for (MethodNode method : caseType.getMethods("isCase")) {
- if (!method.isStatic() && method.getParameters().length == 1
- && !ClassHelper.isObjectType(method.getDeclaringClass())) {
- candidates.add(method);
- }
- }
- List<MethodNode> best = chooseBestMethod(caseType, candidates, switchArg);
- return best.size() == 1 && !isGenericObjectIsCase(best.get(0)) ? best.get(0) : null;
- }
-
/**
- * {@code isCase(Object)} / {@code isCase(Object,Object)} is equals-style and
- * must not replace {@code ScriptBytecodeAdapter.isCase}.
+ * {@code case null} is {@code selector == null}. A primitive selector
+ * can never be null, so the arm is a compile-time miss.
*/
- private static boolean isGenericObjectIsCase(final MethodNode method) {
- if (ClassHelper.isObjectType(method.getDeclaringClass())) {
- return true;
- }
- var parameters = method.getParameters();
- if (method.isStatic() && parameters.length == 2) {
- return ClassHelper.isObjectType(parameters[0].getType())
- && ClassHelper.isObjectType(parameters[1].getType());
+ private void writeNullIdentity(final int selectorIndex, final ClassNode selectorType) {
+ OperandStack operandStack = controller.getOperandStack();
+ MethodVisitor mv = controller.getMethodVisitor();
+ if (ClassHelper.isPrimitiveType(selectorType)) {
+ mv.visitInsn(ICONST_0);
+ operandStack.push(ClassHelper.boolean_TYPE);
+ return;
}
- return parameters.length == 1 && ClassHelper.isObjectType(parameters[0].getType());
+ operandStack.load(selectorType, selectorIndex);
+ operandStack.remove(1);
+ Label isNull = new Label();
+ Label end = new Label();
+ mv.visitJumpInsn(IFNULL, isNull);
+ mv.visitInsn(ICONST_0);
+ mv.visitJumpInsn(GOTO, end);
+ mv.visitLabel(isNull);
+ mv.visitInsn(ICONST_1);
+ mv.visitLabel(end);
+ operandStack.push(ClassHelper.boolean_TYPE);
}
//--------------------------------------------------------------------------
diff --git a/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java b/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java
index 09bf23e78c..afa449f9f8 100644
--- a/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java
+++ b/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java
@@ -54,6 +54,7 @@ import org.codehaus.groovy.ast.expr.MapExpression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.expr.PropertyExpression;
import org.codehaus.groovy.ast.expr.SpreadMapExpression;
+import org.codehaus.groovy.ast.expr.SwitchExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.ast.stmt.BlockStatement;
import org.codehaus.groovy.ast.stmt.CatchStatement;
@@ -969,6 +970,9 @@ public class ResolveVisitor extends ClassCodeExpressionTransformer {
ret = transformMethodCallExpression((MethodCallExpression) exp);
} else if (exp instanceof ClosureExpression) {
ret = transformClosureExpression((ClosureExpression) exp);
+ } else if (exp instanceof SwitchExpression se) {
+ visitSwitchExpression(se);
+ ret = se;
} else if (exp instanceof ConstructorCallExpression) {
ret = transformConstructorCallExpression((ConstructorCallExpression) exp);
} else if (exp instanceof AnnotationConstantExpression) {
diff --git a/src/main/java/org/codehaus/groovy/control/StaticImportVisitor.java b/src/main/java/org/codehaus/groovy/control/StaticImportVisitor.java
index fd779c8d9c..8786567651 100644
--- a/src/main/java/org/codehaus/groovy/control/StaticImportVisitor.java
+++ b/src/main/java/org/codehaus/groovy/control/StaticImportVisitor.java
@@ -44,6 +44,7 @@ import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.expr.NamedArgumentListExpression;
import org.codehaus.groovy.ast.expr.PropertyExpression;
import org.codehaus.groovy.ast.expr.StaticMethodCallExpression;
+import org.codehaus.groovy.ast.expr.SwitchExpression;
import org.codehaus.groovy.ast.expr.TupleExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.ast.stmt.Statement;
@@ -162,6 +163,10 @@ public class StaticImportVisitor extends ClassCodeExpressionTransformer {
if (exp instanceof ClosureExpression) {
return transformClosureExpression((ClosureExpression) exp);
}
+ if (exp instanceof SwitchExpression se) {
+ visitSwitchExpression(se);
+ return se;
+ }
if (clazz == ConstructorCallExpression.class) {
return transformConstructorCallExpression((ConstructorCallExpression) exp);
}
diff --git a/src/main/java/org/codehaus/groovy/transform/StaticTypesTransformation.java b/src/main/java/org/codehaus/groovy/transform/StaticTypesTransformation.java
index 46e612415a..9dc2c753f7 100644
--- a/src/main/java/org/codehaus/groovy/transform/StaticTypesTransformation.java
+++ b/src/main/java/org/codehaus/groovy/transform/StaticTypesTransformation.java
@@ -28,6 +28,7 @@ import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.PropertyNode;
import org.codehaus.groovy.ast.expr.ClosureExpression;
import org.codehaus.groovy.ast.expr.ConstantExpression;
+import org.codehaus.groovy.ast.expr.SwitchExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.ListExpression;
import org.codehaus.groovy.ast.expr.PropertyExpression;
@@ -100,8 +101,9 @@ public class StaticTypesTransformation implements ASTTransformation, Compilation
@Override
public Expression transform(final Expression expression) {
- if (expression instanceof org.codehaus.groovy.ast.expr.SwitchExpression) {
- return expression.transformExpression(this);
+ if (expression instanceof SwitchExpression se) {
+ se.visit(this);
+ return se;
}
if (expression instanceof ClosureExpression) {
expression.visit(this);
diff --git a/src/main/java/org/codehaus/groovy/transform/sc/transformers/StaticCompilationTransformer.java b/src/main/java/org/codehaus/groovy/transform/sc/transformers/StaticCompilationTransformer.java
index 55373002d6..c7bc105693 100644
--- a/src/main/java/org/codehaus/groovy/transform/sc/transformers/StaticCompilationTransformer.java
+++ b/src/main/java/org/codehaus/groovy/transform/sc/transformers/StaticCompilationTransformer.java
@@ -37,6 +37,7 @@ import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.expr.PropertyExpression;
import org.codehaus.groovy.ast.expr.RangeExpression;
import org.codehaus.groovy.ast.expr.StaticMethodCallExpression;
+import org.codehaus.groovy.ast.expr.SwitchExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.groovy.classgen.asm.sc.StaticTypesTypeChooser;
@@ -165,6 +166,10 @@ public class StaticCompilationTransformer extends ClassCodeExpressionTransformer
if (expr instanceof ClosureExpression) {
return closureExpressionTransformer.transformClosureExpression((ClosureExpression) expr);
}
+ if (expr instanceof SwitchExpression se) {
+ se.visit(this);
+ return se;
+ }
if (expr instanceof BooleanExpression) {
return booleanExpressionTransformer.transformBooleanExpression((BooleanExpression) expr);
}
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 8300f8ca12..b8507757cf 100644
--- a/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java
+++ b/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java
@@ -154,6 +154,8 @@ 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.isIntegralType;
+import static org.apache.groovy.ast.tools.SwitchExpressionUtils.isOptimizedIntSwitch;
import static org.apache.groovy.util.BeanUtils.capitalize;
import static org.apache.groovy.util.BeanUtils.decapitalize;
import static org.codehaus.groovy.ast.ClassHelper.AUTOCLOSEABLE_TYPE;
@@ -4893,9 +4895,10 @@ trying: for (ClassNode[] signature : signatures) {
}
/**
- * Type-checks a switch expression: visits the selector and each arm, unifies
- * the yielded types (JEP 361 poly expression), and reports a non-exhaustive
- * switch when the selector type is statically known (GROOVY-12255).
+ * Type-checks a switch expression: visits the selector and each arm, selects
+ * an {@code isCase} target for non-intrinsic arms, unifies the yielded types
+ * (JEP 361 poly expression), and reports a non-exhaustive switch when the
+ * selector type is statically known.
*
* @since 6.0.0
*/
@@ -4924,6 +4927,7 @@ trying: for (ClassNode[] signature : signatures) {
storeType(expression, resultType);
expression.setType(resultType);
+ typeCheckSwitchExpressionIsCase(expression);
checkSwitchExpressionExhaustiveness(expression);
} finally {
typeCheckingContext.popTemporaryTypeInfo();
@@ -4931,6 +4935,46 @@ trying: for (ClassNode[] signature : signatures) {
}
}
+ /**
+ * Resolves {@code isCase} for every non-null label so type-checking
+ * extensions see the call and codegen can emit a direct call. Primitive
+ * int constant switches skip this: their stack type cannot erase, so
+ * tableswitch is guaranteed. Wrapper, String and enum selectors can
+ * erase to {@code Object} (list {@code getAt}, etc.).
+ */
+ private void typeCheckSwitchExpressionIsCase(final SwitchExpression expression) {
+ Expression selector = expression.getExpression();
+ ClassNode selectorType = getType(selector);
+ if (isIntegralType(selectorType)
+ && isOptimizedIntSwitch(selectorType, expression.getCaseStatements())) {
+ return;
+ }
+ for (CaseStatement caseStatement : expression.getCaseStatements()) {
+ Expression caseValue = caseStatement.getExpression();
+ if (isNullConstant(caseValue)) {
+ continue;
+ }
+ ClassNode caseType = getType(caseValue);
+ VariableExpression dummyReceiver = varX("#case", caseType);
+ dummyReceiver.setSourcePosition(caseValue);
+ VariableExpression dummySelector = varX("#selector", getWrapper(selectorType));
+ dummySelector.setSourcePosition(selector);
+ MethodCallExpression call = callX(dummyReceiver, "isCase", args(dummySelector));
+ call.setImplicitThis(false);
+ call.setSourcePosition(caseValue);
+ visitMethodCallExpression(call);
+ MethodNode target = call.getNodeMetaData(DIRECT_METHOD_CALL_TARGET);
+ if (target == null) {
+ continue;
+ }
+ caseStatement.putNodeMetaData(DIRECT_METHOD_CALL_TARGET, target);
+ Object privateAccess = call.getNodeMetaData(PV_METHODS_ACCESS);
+ if (privateAccess != null) {
+ caseStatement.putNodeMetaData(PV_METHODS_ACCESS, privateAccess);
+ }
+ }
+ }
+
/**
* {@inheritDoc}
*/
diff --git a/src/test-resources/groovy/transform/stc/SwitchExpressionIsCaseExtension.groovy b/src/test-resources/groovy/transform/stc/SwitchExpressionIsCaseExtension.groovy
new file mode 100644
index 0000000000..ef58afc4ee
--- /dev/null
+++ b/src/test-resources/groovy/transform/stc/SwitchExpressionIsCaseExtension.groovy
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+// Reports every selected isCase so switch-expression method selection is visible.
+onMethodSelection { expr, method ->
+ if (method.name == 'isCase') {
+ addStaticTypeError('isCase selected', expr)
+ }
+}
diff --git a/src/test/groovy/groovy/transform/stc/TypeCheckingExtensionsTest.groovy b/src/test/groovy/groovy/transform/stc/TypeCheckingExtensionsTest.groovy
index 75b8f38202..0260bb0b33 100644
--- a/src/test/groovy/groovy/transform/stc/TypeCheckingExtensionsTest.groovy
+++ b/src/test/groovy/groovy/transform/stc/TypeCheckingExtensionsTest.groovy
@@ -619,4 +619,41 @@ final class TypeCheckingExtensionsTest extends StaticTypeCheckingTestCase {
assert !customized.any { it.startsWith('Script') },
'extension script should not inherit the enclosing compilation customizers'
}
+
+ @Test
+ void testSwitchExpressionIsCaseMethodSelection() {
+ extension = 'groovy/transform/stc/SwitchExpressionIsCaseExtension.groovy'
+ shouldFailWithMessages '''
+ def m(Object o) {
+ switch (o) {
+ case String -> 's'
+ default -> 'd'
+ }
+ }
+ ''', 'isCase selected'
+
+ shouldFailWithMessages '''
+ def m(int n) {
+ switch (n) {
+ case 1..10 -> 'in'
+ default -> 'out'
+ }
+ }
+ ''', 'isCase selected'
+ }
+
+ @Test
+ void testSwitchExpressionIntrinsicIntDoesNotSelectIsCase() {
+ extension = 'groovy/transform/stc/SwitchExpressionIsCaseExtension.groovy'
+ assertScript '''
+ int m(int n) {
+ switch (n) {
+ case 1 -> 10
+ case 2 -> 20
+ default -> 0
+ }
+ }
+ assert m(1) == 10
+ '''
+ }
}
diff --git a/src/test/groovy/org/apache/groovy/ast/tools/SwitchExpressionUtilsTest.groovy b/src/test/groovy/org/apache/groovy/ast/tools/SwitchExpressionUtilsTest.groovy
new file mode 100644
index 0000000000..f56916ea79
--- /dev/null
+++ b/src/test/groovy/org/apache/groovy/ast/tools/SwitchExpressionUtilsTest.groovy
@@ -0,0 +1,100 @@
+/*
+ * 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.apache.groovy.ast.tools
+
+import org.codehaus.groovy.ast.ClassHelper
+import org.codehaus.groovy.ast.expr.ConstantExpression
+import org.codehaus.groovy.ast.expr.RangeExpression
+import org.codehaus.groovy.ast.stmt.CaseStatement
+import org.codehaus.groovy.ast.stmt.EmptyStatement
+import org.codehaus.groovy.ast.tools.GeneralUtils
+import org.junit.jupiter.api.Test
+
+import java.time.Month
+
+/**
+ * Classification of switch-expression labels for intrinsic vs {@code isCase} dispatch.
+ */
+final class SwitchExpressionUtilsTest {
+
+ @Test
+ void emptyOrNullCasesAreNotOptimized() {
+ assert !SwitchExpressionUtils.isOptimizedSwitch(ClassHelper.int_TYPE, [])
+ assert !SwitchExpressionUtils.isOptimizedSwitch(ClassHelper.int_TYPE, null)
+ assert !SwitchExpressionUtils.isOptimizedSwitch(null, [intCase(1)])
+ }
+
+ @Test
+ void allIntConstantsOnIntegralSelectorAreOptimized() {
+ assert SwitchExpressionUtils.isOptimizedIntSwitch(ClassHelper.int_TYPE, [intCase(1), intCase(2)])
+ assert SwitchExpressionUtils.isOptimizedIntSwitch(ClassHelper.Integer_TYPE, [intCase((byte) 1)])
+ assert SwitchExpressionUtils.isOptimizedSwitch(ClassHelper.char_TYPE, [charCase((char) 'A')])
+ }
+
+ @Test
+ void mixedIntAndRangeIsNotOptimized() {
+ def mixed = [
+ intCase(1),
+ new CaseStatement(new RangeExpression(GeneralUtils.constX(300), GeneralUtils.constX(400), true), EmptyStatement.INSTANCE)
+ ]
+ assert !SwitchExpressionUtils.isOptimizedSwitch(ClassHelper.int_TYPE, mixed)
+ }
+
+ @Test
+ void stringConstantsOnStringSelectorAreOptimized() {
+ assert SwitchExpressionUtils.isOptimizedStringSwitch(ClassHelper.STRING_TYPE, [stringCase('Foo')])
+ assert !SwitchExpressionUtils.isOptimizedStringSwitch(ClassHelper.OBJECT_TYPE, [stringCase('Foo')])
+ assert !SwitchExpressionUtils.isOptimizedStringSwitch(ClassHelper.STRING_TYPE, [intCase(1)])
+ }
+
+ @Test
+ void enumConstantsOnEnumSelectorAreOptimized() {
+ def enumType = ClassHelper.make(Month)
+ def label = GeneralUtils.propX(GeneralUtils.classX(enumType), 'JANUARY')
+ def cases = [new CaseStatement(label, EmptyStatement.INSTANCE)]
+ assert SwitchExpressionUtils.isOptimizedEnumSwitch(enumType, cases)
+ assert SwitchExpressionUtils.enumConstantName(label, enumType) == 'JANUARY'
+ assert SwitchExpressionUtils.enumConstantName(new ConstantExpression(Month.JANUARY), enumType) == 'JANUARY'
+ assert SwitchExpressionUtils.enumConstantName(intCase(1).expression, enumType) == null
+ }
+
+ @Test
+ void extractorsRejectNonConstants() {
+ assert SwitchExpressionUtils.intConstant(GeneralUtils.constX(1)) == 1
+ assert SwitchExpressionUtils.intConstant(GeneralUtils.constX((short) 2)) == 2
+ assert SwitchExpressionUtils.intConstant(GeneralUtils.constX((char) 'A')) == (int) 'A'
+ assert SwitchExpressionUtils.intConstant(GeneralUtils.constX('x')) == null
+ assert SwitchExpressionUtils.stringConstant(GeneralUtils.constX('x')) == 'x'
+ assert SwitchExpressionUtils.stringConstant(GeneralUtils.constX(1)) == null
+ assert SwitchExpressionUtils.unwrapEnumType(ClassHelper.int_TYPE) == null
+ assert SwitchExpressionUtils.unwrapEnumType(ClassHelper.make(Month)).isEnum()
+ }
+
+ private static CaseStatement intCase(final Object value) {
+ new CaseStatement(GeneralUtils.constX(value), EmptyStatement.INSTANCE)
+ }
+
+ private static CaseStatement charCase(final char value) {
+ new CaseStatement(GeneralUtils.constX(value), EmptyStatement.INSTANCE)
+ }
+
+ private static CaseStatement stringCase(final String value) {
+ new CaseStatement(GeneralUtils.constX(value), EmptyStatement.INSTANCE)
+ }
+}
diff --git a/src/test/groovy/org/codehaus/groovy/classgen/Groovy12255.groovy b/src/test/groovy/org/codehaus/groovy/classgen/Groovy12255.groovy
index 3c061dc55f..852a6f2256 100644
--- a/src/test/groovy/org/codehaus/groovy/classgen/Groovy12255.groovy
+++ b/src/test/groovy/org/codehaus/groovy/classgen/Groovy12255.groovy
@@ -18,6 +18,7 @@
*/
package org.codehaus.groovy.classgen
+import groovy.transform.CompileStatic
import org.codehaus.groovy.ast.ClassCodeExpressionTransformer
import org.codehaus.groovy.ast.expr.ConstantExpression
import org.codehaus.groovy.ast.expr.Expression
@@ -27,7 +28,10 @@ import org.codehaus.groovy.ast.stmt.AssertStatement
import org.codehaus.groovy.ast.stmt.CaseStatement
import org.codehaus.groovy.ast.stmt.YieldStatement
import org.codehaus.groovy.ast.tools.GeneralUtils
+import org.codehaus.groovy.control.CompilerConfiguration
+import org.codehaus.groovy.control.MultipleCompilationErrorsException
import org.codehaus.groovy.control.SourceUnit
+import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer
import org.junit.jupiter.api.Test
import static groovy.test.GroovyAssert.assertScript
@@ -40,6 +44,17 @@ import static groovy.test.GroovyAssert.shouldFail
*/
final class Groovy12255 {
+ 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 arrowExpressionArms() {
assertScript '''
@@ -120,7 +135,7 @@ final class Groovy12255 {
@Test
void groovyIsCaseMatching() {
- assertScript '''
+ assertBoth '''
def r = switch ('abc') {
case String -> 'str'
case Integer -> 'int'
@@ -192,8 +207,7 @@ final class Groovy12255 {
@Test
void compileStaticArrowAndYield() {
- assertScript '''
- @groovy.transform.CompileStatic
+ assertBoth '''
def meth(int a) {
switch (a) {
case 1 -> 'one'
@@ -211,8 +225,7 @@ final class Groovy12255 {
@Test
void compileStaticStringSwitch() {
- assertScript '''
- @groovy.transform.CompileStatic
+ assertBoth '''
String partner(String person) {
switch (person) {
case 'Romeo' -> 'Juliet'
@@ -228,11 +241,10 @@ final class Groovy12255 {
@Test
void compileStaticEnumSwitch() {
- assertScript '''
+ assertBoth '''
import java.time.Month
import static java.time.Month.*
- @groovy.transform.CompileStatic
String quarter(Month month) {
switch (month) {
case JANUARY, FEBRUARY, MARCH -> 'Q1'
@@ -268,7 +280,7 @@ final class Groovy12255 {
@Test
void yieldInsideTryFinally() {
- assertScript '''
+ assertBoth '''
def log = []
def r = switch (1) {
case 1 -> {
@@ -340,6 +352,7 @@ final class Groovy12255 {
@Test
void compileStaticDefiniteAssignmentAfterYield() {
+ // definite assignment is a static-checker concern
assertScript '''
@groovy.transform.CompileStatic
int meth(int n) {
@@ -363,6 +376,7 @@ final class Groovy12255 {
@Test
void nestedExpressionInsideSwitchStatementDifferentEnums() {
+ // unqualified enum constants in case labels are rewritten under STC
assertScript '''
enum Color { RED, BLUE }
enum Size { S, L }
@@ -389,7 +403,7 @@ final class Groovy12255 {
@Test
void returnInsideLoopInSwitchExpressionIsError() {
- def err = shouldFail('''
+ def err = shouldFail(MultipleCompilationErrorsException, '''
def r = switch (1) {
case 1 -> {
for (;;) {
@@ -417,8 +431,7 @@ final class Groovy12255 {
@Test
void compileStaticNullStringSelectorUsesDefault() {
- assertScript '''
- @groovy.transform.CompileStatic
+ assertBoth '''
String m(String s) {
switch (s) {
case 'Foo' -> 'a'
@@ -433,10 +446,9 @@ final class Groovy12255 {
@Test
void compileStaticNullEnumSelectorUsesDefault() {
- assertScript '''
+ assertBoth '''
import java.time.DayOfWeek
- @groovy.transform.CompileStatic
String m(DayOfWeek d) {
switch (d) {
case DayOfWeek.MONDAY -> 'mon'
@@ -450,10 +462,9 @@ final class Groovy12255 {
@Test
void compileStaticNullSelectorOnExhaustiveEnumThrows() {
- def err = shouldFail(IllegalStateException, '''
+ def script = '''
enum Flag { ON, OFF }
- @groovy.transform.CompileStatic
String m(Flag f) {
switch (f) {
case Flag.ON -> 'on'
@@ -461,14 +472,16 @@ final class Groovy12255 {
}
}
m(null)
- ''')
- assert err.message.contains('does not cover')
+ '''
+ def dynamic = shouldFail(IllegalStateException, script)
+ assert dynamic.message.contains('does not cover')
+ def statik = shouldFail(IllegalStateException) { new GroovyShell(staticConfig()).evaluate(script) }
+ assert statik.message.contains('does not cover')
}
@Test
void compileStaticNullIntegerSelectorUsesDefault() {
- assertScript '''
- @groovy.transform.CompileStatic
+ assertBoth '''
String m(Integer n) {
switch (n) {
case 1 -> 'one'
@@ -483,7 +496,7 @@ final class Groovy12255 {
@Test
void labeledBreakOutOfSwitchExpressionIsError() {
- def err = shouldFail('''
+ def err = shouldFail(MultipleCompilationErrorsException, '''
outer:
while (true) {
def r = switch (1) {
@@ -500,7 +513,7 @@ final class Groovy12255 {
@Test
void labeledContinueOutOfSwitchExpressionIsError() {
- def err = shouldFail('''
+ def err = shouldFail(MultipleCompilationErrorsException, '''
outer:
while (true) {
def r = switch (1) {
@@ -517,8 +530,7 @@ final class Groovy12255 {
@Test
void compileStaticForLoopInArmWithImplicitThis() {
- assertScript '''
- @groovy.transform.CompileStatic
+ assertBoth '''
class C {
int n
int run() {
@@ -540,7 +552,7 @@ final class Groovy12255 {
@Test
void labeledBreakSkippingYieldInLastArmIsError() {
- def err = shouldFail('''
+ def err = shouldFail(MultipleCompilationErrorsException, '''
def cond = true
def r = switch (1) {
case 1 -> {
@@ -551,7 +563,7 @@ final class Groovy12255 {
default -> 0
}
''')
- assert err.message.contains('does not support `break`') || err.message.contains('yield')
+ assert err.message.contains('does not support `break`')
}
@Test
@@ -575,6 +587,7 @@ final class Groovy12255 {
@Test
void compileStaticEnumSwitchWithUnqualifiedConstantNames() {
+ // unqualified names are rewritten by StaticTypesTransformation
assertScript '''
import java.time.Month
@@ -594,10 +607,9 @@ final class Groovy12255 {
@Test
void compileStaticEnumSwitchLocalVariableShadowingConstantName() {
- assertScript '''
+ assertBoth '''
import java.time.Month
- @groovy.transform.CompileStatic
String m(Month month, Month JANUARY) {
switch (month) {
case JANUARY -> 'matched local'
@@ -611,8 +623,7 @@ final class Groovy12255 {
@Test
void compileStaticStringSwitchWithHashCollision() {
- assertScript '''
- @groovy.transform.CompileStatic
+ assertBoth '''
String m(String s) {
switch (s) {
case 'Aa' -> 'first' // 'Aa' and 'BB' share a hashCode,
@@ -628,8 +639,7 @@ final class Groovy12255 {
@Test
void compileStaticSparseIntKeysStillDispatch() {
- assertScript '''
- @groovy.transform.CompileStatic
+ assertBoth '''
int m(int n) {
switch (n) {
case 1 -> 10
@@ -647,8 +657,7 @@ final class Groovy12255 {
@Test
void compileStaticNonConstantLabelFallsBackToIsCase() {
- assertScript '''
- @groovy.transform.CompileStatic
+ assertBoth '''
String m(int n) {
switch (n) {
case 1 -> 'one'
@@ -688,7 +697,7 @@ final class Groovy12255 {
@Test
void yieldThroughNestedClosureIsError() {
- def err = shouldFail('''
+ def err = shouldFail(MultipleCompilationErrorsException, '''
def r = switch (1) {
case 1 -> {
def c = { yield 1 }
@@ -750,90 +759,18 @@ final class Groovy12255 {
@Test
void lastColonArmIfWithoutElseIsError() {
- def err = shouldFail('''
+ def err = shouldFail(MultipleCompilationErrorsException, '''
def r = switch (1) {
case 1:
if (true) yield 1
}
''')
- assert err.message.contains('yield') || err.message.contains('throw')
- }
-
- @Test
- void compileStaticYieldInsideTryFinallyIntSwitch() {
- assertScript '''
- @groovy.transform.CompileStatic
- int m(int n) {
- def log = []
- int r = switch (n) {
- case 1 -> {
- try {
- yield 10
- } finally {
- log << 'fin'
- }
- }
- default -> 0
- }
- assert log == ['fin']
- return r
- }
- assert m(1) == 10
- '''
- }
-
- @Test
- void compileStaticYieldInsideTryFinallyStringSwitch() {
- assertScript '''
- @groovy.transform.CompileStatic
- String m(String s) {
- def log = []
- String r = switch (s) {
- case 'Foo' -> {
- try {
- yield 'a'
- } finally {
- log << 'fin'
- }
- }
- default -> 'z'
- }
- assert log == ['fin']
- return r
- }
- assert m('Foo') == 'a'
- '''
- }
-
- @Test
- void compileStaticYieldInsideTryFinallyEnumSwitch() {
- assertScript '''
- import java.time.DayOfWeek
-
- @groovy.transform.CompileStatic
- String m(DayOfWeek d) {
- def log = []
- String r = switch (d) {
- case DayOfWeek.MONDAY -> {
- try {
- yield 'mon'
- } finally {
- log << 'fin'
- }
- }
- default -> 'other'
- }
- assert log == ['fin']
- return r
- }
- assert m(DayOfWeek.MONDAY) == 'mon'
- '''
+ assert err.message.contains('`yield` or `throw` is expected')
}
@Test
void compileStaticSwitchExpressionInFieldInitializer() {
- assertScript '''
- @groovy.transform.CompileStatic
+ assertBoth '''
class C {
final String s = switch (1) {
case 1 -> 'a'
@@ -846,8 +783,7 @@ final class Groovy12255 {
@Test
void compileStaticSwitchExpressionInConstructor() {
- assertScript '''
- @groovy.transform.CompileStatic
+ assertBoth '''
class C {
final String s
C() {
@@ -863,8 +799,7 @@ final class Groovy12255 {
@Test
void compileStaticCharAndByteSelectorsUseIntSwitch() {
- assertScript '''
- @groovy.transform.CompileStatic
+ assertBoth '''
int fromChar(char c) {
switch (c) {
case (char) 'A' -> 1
@@ -872,7 +807,6 @@ final class Groovy12255 {
default -> 0
}
}
- @groovy.transform.CompileStatic
int fromByte(byte b) {
switch (b) {
case (byte) 1 -> 10
@@ -887,8 +821,7 @@ final class Groovy12255 {
@Test
void compileStaticSwitchExpressionInStaticInitializer() {
- assertScript '''
- @groovy.transform.CompileStatic
+ assertBoth '''
class C {
static final String S
static {
@@ -970,7 +903,7 @@ final class Groovy12255 {
}
@Test
- void classCodeExpressionTransformerTransformWalksArmsInPlace() {
+ void classCodeExpressionTransformerTransformDoesNotWalkArms() {
def original = GeneralUtils.switchX(
GeneralUtils.constX(1),
[new CaseStatement(
@@ -991,8 +924,9 @@ final class Groovy12255 {
}
}
def transformed = transformer.transform(original)
- assert transformed.is(original)
- assert original.caseStatements[0].code.expression.text == 'A'
+ assert !transformed.is(original)
+ assert original.caseStatements[0].code.expression.text == 'a'
+ assert transformed.caseStatements[0].code.is(original.caseStatements[0].code)
}
@Test
@@ -1012,7 +946,7 @@ final class Groovy12255 {
@Test
void referenceSelectorWrittenByClosure() {
- assertScript '''
+ assertBoth '''
def x = 1
def cl = { x = 2 }
cl()
@@ -1025,42 +959,23 @@ final class Groovy12255 {
}
@Test
- void referenceSelectorWrittenByClosureUnderCompileStatic() {
- assertScript '''
- @groovy.transform.CompileStatic
- String m() {
- int x = 1
- def cl = { x = 2 }
- cl()
- switch (x) {
- case 2 -> 'ok'
- default -> 'no'
- }
- }
- assert m() == 'ok'
- '''
- }
-
- @Test
- void referenceWrapperSelectorWrittenByClosureUnderCompileStatic() {
- assertScript '''
- @groovy.transform.CompileStatic
- String m() {
- Integer x = 1
- def cl = { x = 2 }
- cl()
- switch (x) {
- case 2 -> 'ok'
- default -> 'no'
- }
+ void referenceWrapperSelectorWrittenByClosure() {
+ assertBoth '''
+ Integer x = 1
+ def cl = { x = 2 }
+ cl()
+ def y = switch (x) {
+ case 2 -> 'ok'
+ default -> 'no'
}
- assert m() == 'ok'
+ assert y == 'ok'
'''
}
@Test
void typeCheckedUnmatchedIsError() {
- def err = shouldFail('''
+ // exhaustiveness is a static type-checker error
+ def err = shouldFail(MultipleCompilationErrorsException, '''
@groovy.transform.TypeChecked
def meth(int a) {
switch (a) {
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 9bd6b7e61c..1ac4b2e35c 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
@@ -134,6 +134,8 @@ final class SwitchExpressionStaticCompileTest extends AbstractBytecodeTestCase {
}
''')
assert bytecode.toString().contains('isCase')
+ assert bytecode.toString().contains('DefaultGroovyMethods')
+ assert !bytecode.toString().contains('ScriptBytecodeAdapter.isCase')
}
@Test
@@ -243,7 +245,9 @@ final class SwitchExpressionStaticCompileTest extends AbstractBytecodeTestCase {
}
@Test
- void staticObjectTypedClassLabelUsesRuntimeIsCase() {
+ void staticFlowTypedClassLabelSelectsClassIsCase() {
+ // `Object label = String` is flow-typed to Class, so method selection
+ // binds DGM.isCase(Class, Object) rather than equals.
assertScript '''
@groovy.transform.CompileStatic
String m(Object o) {
@@ -258,6 +262,23 @@ final class SwitchExpressionStaticCompileTest extends AbstractBytecodeTestCase {
'''
}
+ @Test
+ void staticDeclaredObjectLabelUsesEqualsIsCase() {
+ // A parameter whose static type is Object has no more precise flow type,
+ // so isCase is DGM.isCase(Object, Object) — equals — matching `label.isCase(o)`.
+ assertScript '''
+ @groovy.transform.CompileStatic
+ String m(Object o, Object label) {
+ switch (o) {
+ case label -> 's'
+ default -> 'd'
+ }
+ }
+ assert m('x', String) == 'd'
+ assert m(String, String) == 's'
+ '''
+ }
+
@Test
void staticIsCaseUsesMatchingInstanceOverload() {
assertScript '''
@@ -289,6 +310,67 @@ final class SwitchExpressionStaticCompileTest extends AbstractBytecodeTestCase {
'''
}
+ @Test
+ void staticListGetAtStringSelectorStillDispatches() {
+ assertScript '''
+ @groovy.transform.CompileStatic
+ String m(List<String> names) {
+ switch (names[0]) {
+ case 'Foo' -> 'a'
+ default -> 'z'
+ }
+ }
+ assert m(['Foo']) == 'a'
+ assert m(['Bar']) == 'z'
+ '''
+ }
+
+ @Test
+ void staticListGetAtIntegerSelectorStillDispatches() {
+ assertScript '''
+ @groovy.transform.CompileStatic
+ int m(List<Integer> nums) {
+ switch (nums[0]) {
+ case 1 -> 10
+ default -> 0
+ }
+ }
+ assert m([1]) == 10
+ assert m([2]) == 0
+ '''
+ }
+
+ @Test
+ void staticMapGetAtStringSelectorStillDispatches() {
+ assertScript '''
+ @groovy.transform.CompileStatic
+ String m(Map<String, String> names) {
+ switch (names['k']) {
+ case 'Foo' -> 'a'
+ default -> 'z'
+ }
+ }
+ assert m([k: 'Foo']) == 'a'
+ assert m([k: 'Bar']) == 'z'
+ '''
+ }
+
+ @Test
+ void staticNullLabelOnPrimitiveSelectorNeverMatches() {
+ assertScript '''
+ @groovy.transform.CompileStatic
+ String m(int n) {
+ switch (n) {
+ case null -> 'n'
+ case 1 -> 'one'
+ default -> 'd'
+ }
+ }
+ assert m(1) == 'one'
+ assert m(0) == 'd'
+ '''
+ }
+
@Test
void staticEnumConstantWithClassBodyStillDispatchesByName() {
assertScript '''
diff --git a/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/GinqAstWalker.groovy b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/GinqAstWalker.groovy
index 369deef376..3558e48b58 100644
--- a/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/GinqAstWalker.groovy
+++ b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/GinqAstWalker.groovy
@@ -66,6 +66,7 @@ import org.codehaus.groovy.ast.expr.LambdaExpression
import org.codehaus.groovy.ast.expr.ListExpression
import org.codehaus.groovy.ast.expr.MethodCallExpression
import org.codehaus.groovy.ast.expr.PropertyExpression
+import org.codehaus.groovy.ast.expr.SwitchExpression
import org.codehaus.groovy.ast.expr.TupleExpression
import org.codehaus.groovy.ast.expr.VariableExpression
import org.codehaus.groovy.ast.query.AstQuery
@@ -1333,7 +1334,7 @@ class GinqAstWalker implements GinqAstVisitor<Expression>, SyntaxErrorReportable
expr = ((ListExpression) (new ListExpression(Collections.singletonList(expr)).transformExpression(new ClassCodeExpressionTransformer() {
@Override
Expression transform(Expression expression) {
- if (expression instanceof ClosureExpression) {
+ if (expression instanceof ClosureExpression || expression instanceof SwitchExpression) {
expression.visit(this)
return expression
}