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 ce0fe94d0502745978c9243b6e0545b2cdad059c
Author: Paul King <[email protected]>
AuthorDate: Wed May 6 17:28:54 2026 +1000

    GROOVY-11998: Better support of intersection types (part 2)
    Resolution + STC. ResolveVisitor resolves components; 
StaticTypeCheckingVisitor.visitCastExpression validates and propagates. 
LAMBDA_MARKERS metadata. Static error messages.
---
 .../groovy/ast/IntersectionTypeClassNode.java      | 56 ++++++++++---
 .../codehaus/groovy/control/ResolveVisitor.java    |  8 ++
 .../transform/stc/StaticTypeCheckingVisitor.java   | 91 +++++++++++++++++++++-
 .../groovy/transform/stc/StaticTypesMarker.java    |  6 +-
 4 files changed, 150 insertions(+), 11 deletions(-)

diff --git 
a/src/main/java/org/codehaus/groovy/ast/IntersectionTypeClassNode.java 
b/src/main/java/org/codehaus/groovy/ast/IntersectionTypeClassNode.java
index d2afb8b341..906481d832 100644
--- a/src/main/java/org/codehaus/groovy/ast/IntersectionTypeClassNode.java
+++ b/src/main/java/org/codehaus/groovy/ast/IntersectionTypeClassNode.java
@@ -18,7 +18,12 @@
  */
 package org.codehaus.groovy.ast;
 
-import 
org.codehaus.groovy.ast.tools.WideningCategories.LowestUpperBoundClassNode;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.StringJoiner;
+
+import static org.objectweb.asm.Opcodes.ACC_FINAL;
+import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
 
 /**
  * Represents a user-written intersection type used as the target of a cast
@@ -35,21 +40,22 @@ import 
org.codehaus.groovy.ast.tools.WideningCategories.LowestUpperBoundClassNod
  * for cast-conversion checks, error messages and (in later phases) bytecode
  * generation via {@code LambdaMetafactory.altMetafactory} markers.
  *
- * <p>This node is built by the parser's {@code AstBuilder} from the
- * {@code intersectionType} rule. At parse time the components have not yet
- * been resolved to bound {@link ClassNode}s, so the {@code upper} bound
- * passed to the parent constructor is a placeholder and the
- * {@code interfaces} array is just the components in user order; resolution
- * and class-vs-interface classification are completed in later phases.
+ * <p>Lifecycle: at parse time the components have not yet been resolved to
+ * bound {@link ClassNode}s, so the constructor places all components in the
+ * inherited {@link #getInterfaces() interfaces} array with {@code Object} as
+ * the placeholder superclass. After {@code ResolveVisitor} resolves each
+ * component it should call {@link #reclassifyComponents()} so that the
+ * interfaces array contains only interface components and the superclass is
+ * the (at most one) class component.
  *
  * @since 5.0.0
  */
-public final class IntersectionTypeClassNode extends LowestUpperBoundClassNode 
{
+public final class IntersectionTypeClassNode extends ClassNode {
 
     private final ClassNode[] components;
 
     public IntersectionTypeClassNode(final ClassNode[] components) {
-        super("IntersectionType", ClassHelper.OBJECT_TYPE, components.clone());
+        super("IntersectionType", ACC_PUBLIC | ACC_FINAL, 
ClassHelper.OBJECT_TYPE, components.clone(), MixinNode.EMPTY_ARRAY);
         if (components.length < 2) {
             throw new IllegalArgumentException("IntersectionTypeClassNode 
requires at least two components");
         }
@@ -62,4 +68,36 @@ public final class IntersectionTypeClassNode extends 
LowestUpperBoundClassNode {
     public ClassNode[] getComponents() {
         return components.clone();
     }
+
+    /**
+     * Reclassifies the components after resolution: separates the (at most
+     * one) class component from the interface components and updates the
+     * inherited superclass and interfaces accordingly. Components are
+     * resolved in place — callers do not need to substitute new instances.
+     */
+    public void reclassifyComponents() {
+        ClassNode klass = null;
+        List<ClassNode> ifaces = new ArrayList<>(components.length);
+        for (ClassNode c : components) {
+            if (c.isInterface()) {
+                ifaces.add(c);
+            } else {
+                klass = c; // STC will validate "at most one" elsewhere
+            }
+        }
+        setSuperClass(klass != null ? klass : ClassHelper.OBJECT_TYPE);
+        setInterfaces(ifaces.toArray(ClassNode.EMPTY_ARRAY));
+    }
+
+    @Override
+    public String getText() {
+        StringJoiner sj = new StringJoiner(" & ", "(", ")");
+        for (ClassNode c : components) sj.add(c.toString(false));
+        return sj.toString();
+    }
+
+    @Override
+    public String toString(final boolean showRedirect) {
+        return getText();
+    }
 }
diff --git a/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java 
b/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java
index c5904834c8..ef522a4121 100644
--- a/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java
+++ b/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java
@@ -32,6 +32,7 @@ import org.codehaus.groovy.ast.GenericsType;
 import org.codehaus.groovy.ast.GenericsType.GenericsTypeName;
 import org.codehaus.groovy.ast.ImportNode;
 import org.codehaus.groovy.ast.InnerClassNode;
+import org.codehaus.groovy.ast.IntersectionTypeClassNode;
 import org.codehaus.groovy.ast.MethodNode;
 import org.codehaus.groovy.ast.ModuleNode;
 import org.codehaus.groovy.ast.Parameter;
@@ -349,6 +350,13 @@ public class ResolveVisitor extends 
ClassCodeExpressionTransformer {
     }
 
     private void resolveOrFail(final ClassNode type, final String msg, final 
ASTNode node, final boolean preferImports) {
+        if (type instanceof IntersectionTypeClassNode it) { // GROOVY-11998
+            for (ClassNode component : it.getComponents()) {
+                resolveOrFail(component, msg, node, preferImports);
+            }
+            it.reclassifyComponents();
+            return;
+        }
         if (type.isRedirectNode() || !type.isPrimaryClassNode()) {
             visitTypeAnnotations(type); // JSR 308 support
         }
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 737e11aff6..f3cb833c23 100644
--- 
a/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java
+++ 
b/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java
@@ -69,6 +69,7 @@ import org.codehaus.groovy.ast.expr.EmptyExpression;
 import org.codehaus.groovy.ast.expr.Expression;
 import org.codehaus.groovy.ast.expr.ExpressionTransformer;
 import org.codehaus.groovy.ast.expr.FieldExpression;
+import org.codehaus.groovy.ast.IntersectionTypeClassNode;
 import org.codehaus.groovy.ast.expr.LambdaExpression;
 import org.codehaus.groovy.ast.expr.ListExpression;
 import org.codehaus.groovy.ast.expr.MapEntryExpression;
@@ -340,7 +341,9 @@ import static 
org.codehaus.groovy.transform.stc.StaticTypesMarker.DYNAMIC_RESOLU
 import static 
org.codehaus.groovy.transform.stc.StaticTypesMarker.IMPLICIT_RECEIVER;
 import static 
org.codehaus.groovy.transform.stc.StaticTypesMarker.INFERRED_RETURN_TYPE;
 import static 
org.codehaus.groovy.transform.stc.StaticTypesMarker.INFERRED_TYPE;
+import static 
org.codehaus.groovy.transform.stc.StaticTypesMarker.LAMBDA_MARKERS;
 import static 
org.codehaus.groovy.transform.stc.StaticTypesMarker.PARAMETER_TYPE;
+import static 
org.codehaus.groovy.transform.stc.StaticTypesMarker.PRIMARY_FUNCTIONAL_TYPE;
 import static 
org.codehaus.groovy.transform.stc.StaticTypesMarker.PV_FIELDS_ACCESS;
 import static 
org.codehaus.groovy.transform.stc.StaticTypesMarker.PV_FIELDS_MUTATION;
 import static 
org.codehaus.groovy.transform.stc.StaticTypesMarker.PV_METHODS_ACCESS;
@@ -4791,7 +4794,11 @@ trying: for (ClassNode[] signature : signatures) {
     public void visitCastExpression(final CastExpression expression) {
         ClassNode target = expression.getType();
         Expression source = expression.getExpression();
-        applyTargetType(target, source); // GROOVY-9997
+        if (target instanceof IntersectionTypeClassNode) { // GROOVY-11998
+            validateAndApplyIntersectionCast((IntersectionTypeClassNode) 
target, expression, source);
+        } else {
+            applyTargetType(target, source); // GROOVY-9997
+        }
 
         source.visit(this);
 
@@ -4800,7 +4807,89 @@ trying: for (ClassNode[] signature : signatures) {
         }
     }
 
+    /**
+     * Validates JLS §4.9 well-formedness of an intersection cast target,
+     * and for lambda / method-reference / closure operands picks the
+     * SAM-bearing component as the primary functional target so that
+     * parameter inference (and downstream lambda factory generation)
+     * can proceed. Additional components are stored as
+     * {@link StaticTypesMarker#LAMBDA_MARKERS} on both the cast and
+     * source for use by the bytecode writers.
+     */
+    private void validateAndApplyIntersectionCast(final 
IntersectionTypeClassNode target,
+                                                  final CastExpression 
expression,
+                                                  final Expression source) {
+        ClassNode[] components = target.getComponents();
+        int classCount = 0;
+        boolean classNotFirst = false;
+        for (int i = 0, n = components.length; i < n; i += 1) {
+            ClassNode c = components[i];
+            if (isPrimitiveType(c)) {
+                addStaticTypeError("Intersection type components must be 
reference types: " + prettyPrintType(c), expression);
+            }
+            if (!c.isInterface()) {
+                classCount += 1;
+                if (i != 0) classNotFirst = true;
+                if (Modifier.isFinal(c.getModifiers())) {
+                    addStaticTypeError("Intersection type may not include the 
final class " + prettyPrintType(c), expression);
+                }
+            }
+        }
+        if (classCount > 1) {
+            addStaticTypeError("Intersection type may include at most one 
class component: " + prettyPrintType(target), expression);
+        }
+        if (classNotFirst) {
+            addStaticTypeError("Class component of intersection type must come 
first: " + prettyPrintType(target), expression);
+        }
+
+        boolean isFunctionalSource = source instanceof ClosureExpression
+                                  || source instanceof 
MethodReferenceExpression;
+        if (!isFunctionalSource) return; // non-functional cast: just 
decompose check below
+
+        ClassNode primary = null;
+        List<ClassNode> markers = new ArrayList<>(components.length);
+        for (ClassNode c : components) {
+            if (!c.isInterface()) { markers.add(c); continue; }
+            MethodNode sam = findSAM(c);
+            if (sam == null) {
+                markers.add(c); // marker interface
+            } else if (primary == null) {
+                primary = c;
+            } else {
+                addStaticTypeError("Intersection type for lambda/closure has 
multiple functional interface components: "
+                        + prettyPrintType(primary) + " and " + 
prettyPrintType(c), expression);
+                markers.add(c);
+            }
+        }
+        if (primary == null) {
+            addStaticTypeError("Intersection type for lambda/closure target 
has no functional interface component: " + prettyPrintType(target), expression);
+            return;
+        }
+        applyTargetType(primary, source);
+        expression.putNodeMetaData(PRIMARY_FUNCTIONAL_TYPE, primary);
+        source.putNodeMetaData(PRIMARY_FUNCTIONAL_TYPE, primary);
+        if (!markers.isEmpty()) {
+            source.putNodeMetaData(LAMBDA_MARKERS, markers);
+            expression.putNodeMetaData(LAMBDA_MARKERS, markers);
+        }
+        if (source instanceof LambdaExpression) {
+            for (ClassNode m : markers) {
+                if (m.equals(ClassHelper.SERIALIZABLE_TYPE)
+                        || 
m.implementsInterface(ClassHelper.SERIALIZABLE_TYPE)) {
+                    ((LambdaExpression) source).setSerializable(true);
+                    break;
+                }
+            }
+        }
+    }
+
     protected boolean checkCast(final ClassNode targetType, final Expression 
source) {
+        if (targetType instanceof IntersectionTypeClassNode it) { // 
GROOVY-11998
+            for (ClassNode component : it.getComponents()) {
+                if (!checkCast(component, source)) return false;
+            }
+            return true;
+        }
         if (isNullConstant(source)) {
             return !isPrimitiveType(targetType) || 
isPrimitiveBoolean(targetType); // GROOVY-6577
         }
diff --git 
a/src/main/java/org/codehaus/groovy/transform/stc/StaticTypesMarker.java 
b/src/main/java/org/codehaus/groovy/transform/stc/StaticTypesMarker.java
index 028b97b1ea..6ccee7e84e 100644
--- a/src/main/java/org/codehaus/groovy/transform/stc/StaticTypesMarker.java
+++ b/src/main/java/org/codehaus/groovy/transform/stc/StaticTypesMarker.java
@@ -63,5 +63,9 @@ public enum StaticTypesMarker {
     /** list of {@code return null} statements recorded on a method before its 
body is rewritten, so a downstream checker can still report them as non-null 
violations */
     INFERRED_NON_NULL_RETURN_VIOLATIONS,
     /** GEP-15: stores the resolved compound-assignment {@code MethodNode} 
(e.g. {@code plusAssign}) on a {@code BinaryExpression} when the static type 
checker has located one, signalling to codegen that the receiver should be 
mutated in place rather than {@code x = x.plus(y)}-desugared */
-    COMPOUND_ASSIGN_TARGET
+    COMPOUND_ASSIGN_TARGET,
+    /** GROOVY-11998: for an intersection-cast lambda or method reference, the 
SAM-bearing component picked from the intersection */
+    PRIMARY_FUNCTIONAL_TYPE,
+    /** GROOVY-11998: for an intersection cast on a lambda, method reference 
or closure, the additional marker interfaces to thread to {@code 
LambdaMetafactory.altMetafactory} */
+    LAMBDA_MARKERS
 }

Reply via email to