Skip to content

GROOVY-12514, GROOVY-12517: qualified this and super in nested classes under static type checking and compilation - #3049

Open
codeconsole wants to merge 8 commits into
apache:masterfrom
codeconsole:stc-qualified-this-super
Open

codeconsole wants to merge 8 commits into
apache:masterfrom
codeconsole:stc-qualified-this-super

Conversation

@codeconsole

@codeconsole codeconsole commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

This makes qualified this and super in nested classes (N.this, N.super, Outer.super) work under @TypeChecked and @CompileStatic as in Java. Outer.super from a closure or an inner class goes through the super$ helper methods that statically compiled closures use for super calls, so the two bugs of those helpers that it runs into are fixed first: GROOVY-12516 and GROOVY-12518. Each issue has its own commit (GROOVY-12514 one for this and one for super, GROOVY-12518 two more, for overloaded setters and for extension accessors).

Outer.super without an enclosing instance, in a static nested class or in an anonymous class in a static method, is a compile error in every mode since GROOVY-12512; the support for Outer.super here relies on that check.

Outer.super.x = v, ++ and compound assignments with overloaded setters of the superclass choose the setter as super.x = v does since GROOVY-12521; InnerClassTest.testQualifiedSuperOfOuterClassWithOverloadedSetters covers it in all three modes. Two cases combine this with other open pull requests, and the second of each pair to be merged adds their tests:

class Base { String p() { 'base' } }

@CompileStatic
class Outer extends Base {
    String p() { 'outer' }
    static class N extends Base {
        String p() { 'n' }
        String test() { N.super.p() }      // was "No such property: super"; now 'base' (GROOVY-12514)
    }
    class Inner {
        String test() { Outer.super.p() }  // was "No such property: super"; now 'base', on the enclosing instance (GROOVY-12517)
    }
}

GROOVY-12514: N.this and N.super within a nested class N

N.this, which is this, failed under @TypeChecked and @CompileStatic with "No such property: this", and N.super.m() and N.super.p with "No such property: super", in static nested, inner and enum classes and in closures and lambdas there. Both now compile, as in javac. N.super means super in every mode: dynamic Groovy read N.super.p as N.this.p. In a static context they remain errors. Top.this and Top.super in a top-level class keep their error (GROOVY-4321, StaticThisTest).

GROOVY-12517: Outer.super in a class nested in Outer

In an inner class, an anonymous class, a closure or lambda in one of them, or an inner class of an inner class, Outer.super.m() and Outer.super.p failed under @TypeChecked and @CompileStatic with "No such property: super". They now call the superclass's member on the enclosing Outer instance, as in javac. Under @CompileStatic the calls go through super$ helper methods of Outer, since only Outer may call its superclass's methods non-virtually. Property reads and writes, ++ and compound assignments work too. Dynamic Groovy read and wrote Outer.super.p as Outer.this.p; it now uses the superclass's property. Without an enclosing instance of Outer it stays an error.

GROOVY-12516 and GROOVY-12518: super in closures under @CompileStatic

  • GROOVY-12516: two closures or lambdas of one class calling the same super method generated the super$ helper twice, and the class failed to load with ClassFormatError. The helper is generated once.
  • GROOVY-12518: super.p, super.name and super.name = v in a closure or lambda failed with VerifyError (the superclass accessor was called with invokespecial from the closure class); they now go through super$ helpers, like super method calls. super.n++, ++super.n and super.n += v read the property through a dynamic call that failed with MissingPropertyException when the class declares the same property; they now call the superclass's accessors. When the superclass overloads the setter (setX(int) and setX(String)), the setter is the one the type checker selected for the value. When an accessor is an extension method, such as getText() and setText(String) of a File, super.text += v compiled to a dynamic property access; it now calls the extension methods directly.

Behaviour changes

Code that changes meaning in dynamic Groovy and under @TypeChecked, each time to javac's meaning: N.super.p inside N reads the superclass's property instead of N.this.p (GROOVY-12514); Outer.super.p and Outer.super.f in a class nested in Outer use the superclass's member instead of Outer.this's (GROOVY-12517).

Code that failed to compile and now compiles: GROOVY-12514 and GROOVY-12517. Code that failed at run time and now runs under @CompileStatic: GROOVY-12516 and GROOVY-12518.

Tests

Each commit has tests that fail without it, marked with its JIRA key: gls.innerClass.InnerClassTest, ClosuresSTCTest (and ClosuresStaticCompileTest), FieldsAndPropertiesSTCTest (and FieldsAndPropertiesStaticCompileTest) and, for the overloaded setters and the extension accessors, FieldsAndPropertiesStaticCompileTest alone. 13,655 tests in the groovy, gls, bugs, org.codehaus.groovy.classgen, transform and control packages pass, apart from bugs.Groovy12363, which fails on master too when it runs after bugs.Groovy12142 in the same JVM. The Checkstyle and CodeNarc gates pass.

Backport

GROOVY-12514 and GROOVY-12517 change what N.super.p and Outer.super.p mean in dynamic Groovy, so whether to backport them is left to the maintainers; GROOVY-12516 and GROOVY-12518 only make code that failed run.

@codecov-commenter

codecov-commenter commented Oct 26, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.62016% with 25 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.6875%. Comparing base (0c7c1c1) to head (cdb685d).
⚠️ Report is 3 commits behind head on master.

Files with missing lines Patch % Lines
...ovy/classgen/asm/sc/StaticTypesCallSiteWriter.java 74.2424% 4 Missing and 13 partials ⚠️
...va/org/codehaus/groovy/control/ResolveVisitor.java 66.6667% 0 Missing and 3 partials ⚠️
...roovy/transform/stc/StaticTypeCheckingVisitor.java 86.9565% 0 Missing and 3 partials ⚠️
...taticTypesBinaryExpressionMultiTypeDispatcher.java 50.0000% 0 Missing and 1 partial ⚠️
...m/sc/transformers/BinaryExpressionTransformer.java 83.3333% 0 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##               master      #3049        +/-   ##
==================================================
+ Coverage     73.6739%   73.6875%   +0.0136%     
- Complexity      42586      42655        +69     
==================================================
  Files            1678       1678                
  Lines          146911     147029       +118     
  Branches        27791      27836        +45     
==================================================
+ Hits           108235     108342       +107     
+ Misses          28525      28522         -3     
- Partials        10151      10165        +14     
Files with missing lines Coverage Δ
...rg/codehaus/groovy/classgen/AsmClassGenerator.java 85.6618% <100.0000%> (+0.0317%) ⬆️
...vy/classgen/asm/sc/StaticCompilationMopWriter.java 100.0000% <100.0000%> (ø)
.../transformers/MethodCallExpressionTransformer.java 89.1892% <100.0000%> (-1.2870%) ⬇️
...sc/transformers/PropertyExpressionTransformer.java 100.0000% <100.0000%> (ø)
...taticTypesBinaryExpressionMultiTypeDispatcher.java 87.0370% <50.0000%> (+0.7016%) ⬆️
...m/sc/transformers/BinaryExpressionTransformer.java 91.9847% <83.3333%> (-0.2028%) ⬇️
...va/org/codehaus/groovy/control/ResolveVisitor.java 90.7193% <66.6667%> (+0.0979%) ⬆️
...roovy/transform/stc/StaticTypeCheckingVisitor.java 87.4804% <86.9565%> (+0.0142%) ⬆️
...ovy/classgen/asm/sc/StaticTypesCallSiteWriter.java 77.5137% <74.2424%> (-0.4489%) ⬇️

... and 17 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@codeconsole
codeconsole marked this pull request as draft October 26, 2026 16:03
…ed class

N.this inside a nested class N is this, as in Java. The type checker
rejected it with "No such property: this for Class or static property
for class: Box$N", under @TypeChecked and @CompileStatic, for a static
nested, inner or enum class, and also inside a closure or lambda there;
dynamic Groovy ran it, and javac accepts it.

The type checker now types N.this inside N, outside a static context,
as N. In a static context ResolveVisitor still reports its error.
Top.this in a top-level class keeps its error in every mode (see
StaticThisTest, GROOVY-4321).
…ling the same super method

Under @CompileStatic a call to super.m() in a closure or lambda calls
a generated method super$2$m of the enclosing class. The type checker
records the target for every such call, and StaticCompilationMopWriter
generated one method per record, so two closures or lambdas calling
the same super method produced the method twice and the class failed
to load with "ClassFormatError: Duplicate method name super$2$m".
Dynamic Groovy and @TypeChecked run the same code.

The writer now generates one method per name and descriptor; overloads
of the same name keep one method each.
…lass

N.super inside a nested class N is super, as in Java (JLS 15.11.2,
15.12.1). Under @TypeChecked and @CompileStatic, N.super.m() and
N.super.p failed with "No such property: super for Class or static
property for class: Box$N". Dynamic Groovy ran N.super.m(), but read
N.super.p and N.super.q as N.this.p and N.this.q, the subclass's
properties; javac reads the superclass's.

ResolveVisitor now replaces N.super inside a nested class N with
super, so every mode gives it the meaning of super: a call or property
of the superclass, also inside a closure or lambda, for static nested,
inner and enum classes. Under @CompileStatic a closure or lambda then
calls the superclass method through the super$ helper (see
GROOVY-12516). In a static context ResolveVisitor still reports its
error. Top.super in a top-level class keeps its error in every mode
(see StaticThisTest, GROOVY-4321), and Outer.super, naming an outer
class, is not changed here.

In dynamic Groovy, N.super.p inside N now reads the superclass's
property instead of N.this.p.
…es, increments and compound assignments

Under static compilation a property read or written through super inside
a closure or lambda called the superclass accessor with invokespecial from
the closure class, which fails verification. A super method call in a
closure already goes through a super$ helper of the enclosing class; the
type checker now asks for the same helper for the getter and setter of a
super property (also for a property whose accessors are generated later),
and the property access and assignment transformers call it.

super.name++, ++super.name and super.name += value read the property
through ScriptBytecodeAdapter.getPropertyOnSuper (and ++ also wrote it
through setPropertyOnSuper), which throws MissingPropertyException when the
class declares the same property. The call site writer now reads and
writes such a super property through the superclass accessor, directly
with invokespecial, or through the super$ helpers in a closure or lambda.
In a class nested in Outer that has an enclosing instance of Outer (an
inner class, an anonymous class, a closure or lambda in one of them, or an
inner class of an inner class), Outer.super names the superclass of that
instance (JLS 15.11.2, 15.12.1). The type checker rejected it ("No such
property: super"); it now types Outer.super as the superclass of Outer.
Where the class has no enclosing instance of Outer it stays an error.

Under static compilation, a method or property of Outer.super is called
through a super$ helper of Outer, as a super call in a closure is: the
JVM allows invokespecial on Outer's superclass only from within Outer.
Property reads and writes, increments and compound assignments use the
helpers for the accessors.

Dynamic Groovy read and wrote a property of Outer.super as a property of
Outer.this; it now goes to the superclass property, as Outer.super.m()
already went to the superclass method.
…name++ and compound assignments

super.name++, ++super.name, super.name op= value and super.name ?= value
wrote the property through the first one-argument setter the
superclass declares, so with overloads such as setX(int) and
setX(String) the value could go to the wrong one: a GroovyCastException,
a character passed as an int, or, in a closure, a lambda or for
Outer.super.name, the class's own property written instead. The call
site writer now calls the setter the type checker selected for the
value, which is also the one it asked the super$ helper for.

The lookup of a property node whose accessors "are generated later" is
removed: the accessors of every class of the compilation unit exist
before bytecode is generated, so it only ran for a property with a
non-default accessor name, and there it ignored static and private
properties.
…and compound assignments

super.name++, super.name op= value and their kin compiled to dynamic
getPropertyOnSuper and setPropertyOnSuper calls when an accessor of the
property is an extension method, such as getText() and setText(String)
of a File, also in a closure, a lambda and for Outer.super.name. The
call site writer now calls the extension methods directly: the setter
the type checker selected for the value, and the getter it selects
when the superclass declares none.
…uperclass

Outer.super.x = v, ++ and compound assignments choose among the setters
of the superclass, as super.x = v does since GROOVY-12521, in dynamic
Groovy, under @TypeChecked and under @CompileStatic.
@codeconsole
codeconsole force-pushed the stc-qualified-this-super branch from cd35874 to cdb685d Compare October 29, 2026 20:05
@codeconsole
codeconsole marked this pull request as ready for review October 29, 2026 20:05
@testlens-app

testlens-app Bot commented Oct 29, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: cdb685d
▶️ Tests: 123091 executed
⚪️ Checks: 26/26 completed


Learn more about TestLens at testlens.app/docs.

@daniellansun daniellansun left a comment

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.

Thanks for the careful multi-ticket stack on qualified this/super — tip cdb685d.

The shape is right: ResolveVisitor rewrites N.super to super (GROOVY-12514) after the existing Class.this/Class.super checks; STC recognizes N.this and types Outer.super as the enclosing type’s superclass (GROOVY-12517); SC sends Outer.super (and super-from-closure) through Outer’s super$ helpers, with InvocationWriter’s existing Outer.this rewrite for the receiver; StaticCompilationMopWriter dedupes super$ by name+descriptor (GROOVY-12516); and the 12518 property / ++ / += / overloaded-setter / extension-accessor paths are backed by strong parameterized tests. CI is green on this tip.

A few items before I would approve:

  1. COMPATIBILITY.md for the dynamic meaning change
    N.super.p and Outer.super.p / Outer.super.f used to read the subclass / enclosing instance’s own member in dynamic Groovy; they now follow javac (superclass member). That is the same class of silent change we documented for GROOVY-12521. The PR description is clear; please add a short COMPATIBILITY section (Who is affected) before merge — also needed if we cherry-pick to GROOVY_6_0_X.

  2. Outer.super.@field asymmetry
    visitPropertyExpression now uses isSuperOrOuterSuper → get/setPropertyOnSuper, but visitAttributeExpression still keys only on isSuperExpression. Outer.super.@q therefore still hits Outer’s own field. Please mirror the property fix (and test Outer.super.@x / N.super.@x), or file a follow-up JIRA and mention it explicitly so we do not leave the @ form silently wrong. makeSuperPropertySite’s AttributeExpression skip is the SC counterpart of the same gap.

  3. Landing next to #3071 (GROOVY-12526)
    Complementary, not competing: #3071 rejects non-direct Iface.super; this PR enables class N.super / Outer.super. Expect a small ResolveVisitor / STC existsProperty conflict. Keep the interface check as an early path and the N.super rewrite afterward for non-interface nested classes.

  4. Nit: isSuperOrOuterSuper vs InvocationWriter.isOuterClassSuper
    Same predicate in two places (and !isInterface() matters next to #3071). A shared helper would help; not a merge blocker.

  5. Cherry-pick
    After COMPATIBILITY: full tip to GROOVY_6_0_X is appropriate (12516/12518 are pure fixes; 12514/12517 fix STC/SC hard failures and intentionally align dynamic Groovy with javac). Without COMPATIBILITY I would only be comfortable cherry-picking the 12516+12518 commits.

Happy to re-review quickly once COMPATIBILITY is in and the attribute path is either fixed or explicitly deferred with a ticket.

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.

* 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).

}

checkThisAndSuperAsPropertyAccess(xe);
if (isSuperOfCurrentNestedClass(xe)) { // "Type.super" within nested class Type is "super"

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.

Good place for the rewrite (after checkThisAndSuperAsPropertyAccess). When rebasing onto or against #3071 (GROOVY-12526), keep the interface-super rejection as an early path for ClassExpression + interface + "super", and keep this rewrite only for non-interface nested classes — the interface early-return must not swallow N.super.

return true;
}

private static MethodNode findSuperPropertyAccessor(final PropertyExpression expression, final ClassNode superType, final String name, final boolean setter) {

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.

findSuperPropertyAccessor looks up isX then getX on the superclass, while the setter path trusts DIRECT_METHOD_CALL_TARGET. That matches Verifier-generated boolean properties and the new tests. Optional hardening: for getters, prefer DIRECT_METHOD_CALL_TARGET when present (as setters do) so a synthetic isX recorded on SUPER_MOP_METHOD_REQUIRED cannot disagree with a lone getX on the ClassNode.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants