Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
68811e5
GROOVY-12514: STC: allow a qualified this that names the current nest…
codeconsole Oct 25, 2026
ecac373
GROOVY-12516: SC: generate one super$ method for several closures cal…
codeconsole Oct 25, 2026
e920fba
GROOVY-12514: allow a qualified super that names the current nested c…
codeconsole Oct 25, 2026
68a832c
GROOVY-12518: SC: access a super property through accessors in closur…
codeconsole Oct 25, 2026
07f4caa
GROOVY-12517: allow a qualified super that names an enclosing class
codeconsole Oct 25, 2026
18c7fd8
GROOVY-12518: SC: use the setter the type checker selected for super.…
codeconsole Oct 25, 2026
d091397
GROOVY-12518: SC: call extension accessors directly for super.name++ …
codeconsole Oct 26, 2026
cdb685d
GROOVY-12517: test Outer.super.x = v with overloaded setters of the s…
codeconsole Oct 29, 2026
0d282e1
Merge remote-tracking branch 'origin/master' into qs-on-master
codeconsole Oct 30, 2026
da0bf45
GROOVY-12517: share one check for Outer.super between the type checke…
codeconsole Oct 30, 2026
585caf6
GROOVY-12517: read and write Outer.super.@name as the field of the su…
codeconsole Oct 30, 2026
2fd3cbe
GROOVY-12517: resolve Outer.super against Outer's superclass for an i…
codeconsole Oct 30, 2026
1b7a08c
GROOVY-12517: describe the new meaning of N.super.name and Outer.supe…
codeconsole Oct 30, 2026
4936199
GROOVY-12566: run the super property tests with closures statically c…
codeconsole Oct 30, 2026
f774694
Merge remote-tracking branch 'origin/master' into qs-on-master
codeconsole Oct 31, 2026
7b7060c
GROOVY-12517: D.super.value() from an inner class of D compiles in ev…
codeconsole Oct 31, 2026
b48fdf2
Merge remote-tracking branch 'origin/master' into qs-on-master
codeconsole Oct 31, 2026
fd90bd4
Merge remote-tracking branch 'origin/master' into qs-on-master
codeconsole Oct 31, 2026
412c44a
GROOVY-12514: SC: Type.this.name within an inner subclass reads the e…
codeconsole Oct 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,36 @@ package-private constructor of a class that a `GroovyClassLoader` defined in
another compilation, such as a class of an earlier `GroovyShell` parse. Dynamic
and `@TypeChecked` code select the constructor at run time as before.

### Groovy 7 — `N.super.name` and `Outer.super.name` reach the member of the superclass (GROOVY-12514, GROOVY-12517)

Within a nested class `N`, `N.super` is now `super`. Within a class nested in
`Outer`, `Outer.super` is the enclosing instance of `Outer` seen as an instance
of `Outer`'s superclass, as in Java. In dynamic Groovy, a property read or
write through either — `N.super.p`, `Outer.super.p = v`, `Outer.super.p++` —
and a field access — `N.super.@f`, `Outer.super.@f = v` — used to reach the
instance's own member: the one `N` or `Outer` declares, or, for an enclosing
instance of a subclass of `Outer`, the subclass's. It now reaches the
superclass's property, accessor or field, as `super.p` and `super.@f` do.
`Outer.super.m()` already called the superclass's method, except for an
enclosing instance of a subclass of `Outer`, where it called the method that
`super.m()` in the subclass would call (`Outer`'s, if `Outer` overrides `m`);
it now calls the method of `Outer`'s superclass. Under `@TypeChecked` and
`@CompileStatic`, these property and field forms failed to compile ("No such
property: super") and now compile with the same meaning.

**Who is affected.** Dynamic code that reads or writes a property or field
through `N.super` or `Outer.super` where `N` or `Outer`, or the class of the
enclosing instance, declares or overrides that member. The access now reaches
the superclass's member; use `this.p`, `N.this.p` or `Outer.this.p` where the
class's own member is intended. Classes compiled by an earlier release keep
the old behaviour until they are recompiled.

**What is unchanged.** Unqualified `super`, `Outer.this`, `Iface.super.m()`
for interface default methods, method calls through `N.super` and through
`Outer.super` on an instance of `Outer` itself, and members that no class
below the superclass declares. `ExpressionUtils.isOuterSuperExpression` is
new; there are no other API changes.

## The binary-compatibility check

The [`subprojects/binary-compatibility/`](subprojects/binary-compatibility)
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/org/apache/groovy/ast/tools/ExpressionUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,19 @@ public static boolean isSuperExpression(final Expression expression) {
return expression instanceof VariableExpression && ((VariableExpression) expression).isSuperExpression();
}

/**
* Checks whether the supplied expression is {@code Type.super} for a class
* {@code Type}: the superclass of the enclosing instance of type {@code Type}.
* For an interface, {@code Type.super} selects its default methods instead.
*
* @param expression the expression to inspect
* @return {@code true} if the expression is {@code Type.super} and {@code Type} is not an interface
*/
public static boolean isOuterSuperExpression(final Expression expression) {
return expression instanceof PropertyExpression pexp && "super".equals(pexp.getPropertyAsString())
&& pexp.getObjectExpression() instanceof ClassExpression && !pexp.getObjectExpression().getType().isInterface();
}

/**
* Checks whether the supplied expression references {@code this} or {@code super}.
*
Expand Down
17 changes: 13 additions & 4 deletions src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@
import static org.apache.groovy.ast.tools.ClassNodeUtils.getField;
import static org.apache.groovy.ast.tools.ClassNodeUtils.getNestHost;
import static org.apache.groovy.ast.tools.ExpressionUtils.isNullConstant;
import static org.apache.groovy.ast.tools.ExpressionUtils.isOuterSuperExpression;
import static org.apache.groovy.ast.tools.ExpressionUtils.isSuperExpression;
import static org.codehaus.groovy.ast.ClassHelper.isClassType;
import static org.codehaus.groovy.ast.ClassHelper.isObjectType;
Expand Down Expand Up @@ -1391,6 +1392,14 @@ public static FieldNode getDeclaredFieldOfCurrentClassOrAccessibleFieldOfSuper(f
);
}

/**
* Determines if an expression is "super" or "Outer.super" for a class: a
* property of it is a property of the superclass of the instance.
*/
private static boolean isSuperOrOuterSuper(final Expression expression) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isSuperOrOuterSuper matches InvocationWriter.isOuterClassSuper / isClassWithSuper (GROOVY-11611). Fine as a local copy for this fix, but consider one shared ExpressionUtils (or package) helper so the !isInterface() guard stays aligned with GROOVY-12526 (#3071).

return isSuperExpression(expression) || isOuterSuperExpression(expression);
}

private void visitAttributeOrProperty(final PropertyExpression pexp, final MethodCallerMultiAdapter adapter) {
ClassNode classNode = controller.getClassNode();
String propertyName = pexp.getPropertyAsString();
Expand Down Expand Up @@ -1534,9 +1543,9 @@ public void visitPropertyExpression(final PropertyExpression expression) {
if (!visited) {
MethodCallerMultiAdapter adapter;
if (controller.getCompileStack().isLHS()) {
adapter = isSuperExpression(objectExpression) ? setPropertyOnSuper : isGroovyObject(expression) ? setGroovyObjectProperty : setProperty;
adapter = isSuperOrOuterSuper(objectExpression) ? setPropertyOnSuper : isGroovyObject(expression) ? setGroovyObjectProperty : setProperty;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Property form now goes through isSuperOrOuterSuper → get/setPropertyOnSuper, but the attribute adapters in visitAttributeExpression (tip ~1550) still key only on isSuperExpression. Outer.super.@q will load Outer.this and then getField without useSuper, i.e. Outer's own field — the same bug shape this PR fixes for Outer.super.q.

Please mirror isSuperOrOuterSuper there (and cover Outer.super.@x / N.super.@x in InnerClassTest), or open a follow-up ticket and call it out in the PR so the @ form is not left silently wrong.

} else {
adapter = isSuperExpression(objectExpression) ? getPropertyOnSuper : isGroovyObject(expression) ? getGroovyObjectProperty : getProperty;
adapter = isSuperOrOuterSuper(objectExpression) ? getPropertyOnSuper : isGroovyObject(expression) ? getGroovyObjectProperty : getProperty;
}
visitAttributeOrProperty(expression, adapter);
}
Expand Down Expand Up @@ -1589,9 +1598,9 @@ public void visitAttributeExpression(final AttributeExpression expression) {
if (!visited) {
MethodCallerMultiAdapter adapter;
if (controller.getCompileStack().isLHS()) {
adapter = isSuperExpression(objectExpression) ? setFieldOnSuper : isGroovyObject(expression) ? setGroovyObjectField : setField;
adapter = isSuperOrOuterSuper(objectExpression) ? setFieldOnSuper : isGroovyObject(expression) ? setGroovyObjectField : setField;
} else {
adapter = isSuperExpression(objectExpression) ? getFieldOnSuper : isGroovyObject(expression) ? getGroovyObjectField : getField;
adapter = isSuperOrOuterSuper(objectExpression) ? getFieldOnSuper : isGroovyObject(expression) ? getGroovyObjectField : getField;
}
visitAttributeOrProperty(expression, adapter);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@

import static org.apache.groovy.ast.tools.ClassNodeUtils.isSubtype;
import static org.apache.groovy.ast.tools.ExpressionUtils.isNullConstant;
import static org.apache.groovy.ast.tools.ExpressionUtils.isOuterSuperExpression;
import static org.apache.groovy.ast.tools.ExpressionUtils.isSuperExpression;
import static org.apache.groovy.ast.tools.ExpressionUtils.isThisExpression;
import static org.apache.groovy.ast.tools.ExpressionUtils.mayHaveSideEffects;
Expand Down Expand Up @@ -165,6 +166,9 @@ public void makeCall(final Expression origin, final Expression receiver, final E
sender = controller.getThisType();
implicitThis = false;
safe = false;
} else if (isOuterSuperExpression(receiver)) {
// "Outer.super.name": the member of Outer's superclass, also for an instance of a subclass of Outer
sender = ((PropertyExpression) receiver).getObjectExpression().getType();
} else {
sender = controller.getClassNode();
}
Expand Down Expand Up @@ -362,14 +366,6 @@ private boolean isClassWithSuper(final Expression exp) {
return false;
}

private boolean isOuterClassSuper(final Expression exp) {
if (isClassWithSuper(exp)) {
PropertyExpression pexp = (PropertyExpression) exp;
return !pexp.getObjectExpression().getType().isInterface();
}
return false;
}


/**
* Supplements {@link ExpressionUtils#isThisExpression isThisExpression}
Expand Down Expand Up @@ -651,7 +647,7 @@ public void writeInvokeMethod(MethodCallExpression call) {
if (isFunctionalInterface(type)) call = transformToRealMethodCall(call, type);
}
MethodCallerMultiAdapter adapter = invokeMethod;
if (isSuperExpression(receiver) || isOuterClassSuper(receiver)) { // GROOVY-11611
if (isSuperExpression(receiver) || isOuterSuperExpression(receiver)) { // GROOVY-11611
adapter = invokeMethodOnSuper;
} else if (isThisExpression(receiver)) {
adapter = invokeMethodOnCurrent;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,18 @@

import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.stmt.EmptyStatement;
import org.codehaus.groovy.classgen.asm.BytecodeHelper;
import org.codehaus.groovy.classgen.asm.MopWriter;
import org.codehaus.groovy.classgen.asm.WriterController;
import org.codehaus.groovy.transform.stc.StaticTypesMarker;

import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;

import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC;

/**
* A MOP Writer that skips the generation of MOP methods. This writer is used
Expand Down Expand Up @@ -53,7 +60,37 @@ public void createMopMethods() {
ClassNode classNode = controller.getClassNode();
LinkedList<MethodNode> requiredMopMethods = classNode.getNodeMetaData(StaticTypesMarker.SUPER_MOP_METHOD_REQUIRED);
if (requiredMopMethods != null) {
generateMopCalls(requiredMopMethods, false);
// one method per name and descriptor, however many closures call it
Set<String> signatures = new HashSet<>();
LinkedList<MethodNode> mopMethods = new LinkedList<>();
for (MethodNode method : requiredMopMethods) {
if (signatures.add(getMopMethodName(method, false) + BytecodeHelper.getMethodDescriptor(method.getReturnType(), method.getParameters()))) {
mopMethods.add(method);
}
}
generateMopCalls(mopMethods, false);
}
}

/**
* Creates the target for a call to the super$ helper that a class gets for
* a method of its superclass listed in its {@link StaticTypesMarker#SUPER_MOP_METHOD_REQUIRED}
* metadata.
*
* @param helperType the class that declares the helper
* @param superMethod the method of the superclass
* @return the helper method
*/
public static MethodNode getSuperMopMethod(final ClassNode helperType, final MethodNode superMethod) {
MethodNode helper = new MethodNode(
getMopMethodName(superMethod, false),
ACC_PUBLIC | ACC_SYNTHETIC,
superMethod.getReturnType(),
superMethod.getParameters(),
superMethod.getExceptions(),
EmptyStatement.INSTANCE
);
helper.setDeclaringClass(helperType);
return helper;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
import static org.codehaus.groovy.transform.stc.StaticTypesMarker.COMPOUND_ASSIGN_TARGET;
import static org.codehaus.groovy.transform.stc.StaticTypesMarker.DIRECT_METHOD_CALL_TARGET;
import static org.codehaus.groovy.transform.stc.StaticTypesMarker.INFERRED_TYPE;
import static org.codehaus.groovy.transform.stc.StaticTypesMarker.SUPER_MOP_METHOD_REQUIRED;
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
import static org.objectweb.asm.Opcodes.DADD;
import static org.objectweb.asm.Opcodes.DCONST_1;
Expand Down Expand Up @@ -168,7 +169,9 @@ private boolean tryStaticCompoundAssignPaths(final String baseName, final Binary
}
Expression leftExpression = expression.getLeftExpression();
if (leftExpression instanceof PropertyExpression pexp
&& !(leftExpression instanceof AttributeExpression)) {
&& !(leftExpression instanceof AttributeExpression)
// "super.name op= value" in a closure or "Outer.super.name op= value" reads and writes through super$ helpers
&& pexp.getNodeMetaData(SUPER_MOP_METHOD_REQUIRED) == null) {

BinaryExpression expressionWithoutAssignment = binX(
leftExpression,
Expand Down Expand Up @@ -224,7 +227,7 @@ public void evaluateEqual(final BinaryExpression expression, final boolean defin
Expression leftExpression = expression.getLeftExpression();
if (leftExpression instanceof PropertyExpression pexp
&& !(leftExpression instanceof AttributeExpression)) {
if (!defineVariable && makeSetProperty(
if (!defineVariable && pexp.getNodeMetaData(SUPER_MOP_METHOD_REQUIRED) == null && makeSetProperty(
pexp.getObjectExpression(),
pexp.getProperty(),
expression.getRightExpression(),
Expand Down
Loading
Loading