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 }
