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
                 }