Skip to content

GROOVY-12574: Call an outer class method on the right enclosing instance - #3106

Open
codeconsole wants to merge 1 commit into
apache:masterfrom
codeconsole:trait-outer-method
Open

codeconsole wants to merge 1 commit into
apache:masterfrom
codeconsole:trait-outer-method

Conversation

@codeconsole

Copy link
Copy Markdown
Contributor

An implicit-this call from an inner class to a method of an enclosing class now runs on the innermost enclosing instance whose class has the method, in dynamic and statically compiled code, inside and outside closures and lambdas.

Before, a closure in a field initializer of an inner class of a @CompileStatic class failed in each @CompileDynamic constructor:

@CompileStatic
class Outer {
    String pub() { 'pub' }
    class Inner {
        String v = { -> pub() }()
        @CompileDynamic Inner() {}   // GroovyCastException: Outer$Inner cannot be cast to Outer
    }
}

The static compiler gives the call a method target, and the dynamic invocation writer, which writes the initializer into the @CompileDynamic constructor, kept the closure's this object as the receiver. Lambdas, nested closures, private methods, interface default methods and trait methods failed the same way, as did a @CompileStatic inner class of a dynamic class.

A method inherited by an intermediate enclosing class (Outer > Mid extends Base > Inner calling base()) failed verification under @CompileStatic, or ran on the outermost instance when that class inherits the method too. Both invocation writers loaded DeclaringClass.this; they now use the innermost enclosing instance whose class extends or implements the declaring class, as the static writer already did inside closures.

Tests: InnerClassTest.testUsageOfOuterMethod11 and testUsageOfOuterMethod12 (dynamic, @TypeChecked and @CompileStatic). testUsageOfOuterMethod11 and the @CompileStatic case of testUsageOfOuterMethod12 fail without the change; the other two cases guard the dynamic and type-checked paths. The groovy, gls, bugs and org.codehaus.groovy packages pass (15,499 tests). The Checkstyle and CodeNarc gates pass.

Seen in 4.0.33, 5.0.8 and 6.0.0.

An implicit-this call from an inner class to a method of an enclosing
class needs the instance of the class that has the method. When the call
has a method target, the invocation writers picked that instance wrongly
in two ways.

In a closure or lambda, the dynamic writer kept the closure's this
object as the receiver, even when the method belongs to an enclosing
instance of that object. A closure in a field initializer of an inner
class of a @CompileStatic class gets its target from the static
compiler, and the dynamic writer also writes that initializer into each
@CompileDynamic constructor, so

    @CompileStatic
    class Outer {
        String pub() { 'pub' }
        class Inner {
            String v = { -> pub() }()
            @CompileDynamic Inner() {}
        }
    }

failed with "GroovyCastException: Cannot cast object 'Outer$Inner@...'
with class 'Outer$Inner' to class 'Outer'". Lambdas, nested closures,
private methods, interface default methods and trait methods failed the
same way, as did a @CompileStatic inner class of a dynamic class.

Outside a closure, both writers loaded DeclaringClass.this, which walks
the enclosing instances until it reaches the declaring class itself. A
method that an intermediate enclosing class inherits was therefore
called on the wrong instance or, when the outermost class does not
inherit it, on an instance of the wrong type:

    class Base { String base() { 'base' } }
    @CompileStatic
    class Outer {
        class Mid extends Base {
            class Inner { String m() { base() } }
        }
    }

failed verification with "Type 'Outer' (current frame, stack[0]) is not
assignable to 'Base'". When Outer extends Base too, base() ran on the
Outer instance, where dynamic Groovy and Java use the Mid instance.
Property reads through an inherited getter behaved the same.

Both writers now use the innermost enclosing instance whose class
extends or implements the declaring class, as the static writer already
did inside closures.
@testlens-app

testlens-app Bot commented Oct 30, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: f359f7e
▶️ Tests: 102516 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.

LGTM for GROOVY-12574.

Implicit-this calls from an inner class must land on the innermost enclosing instance whose type actually has the method. InvocationWriter.getImplicitReceiverType (skip generated functions, walk this$0, isSubtype) used from both dynamic and SC writeDirectMethodCall fixes the @CompileStatic outer + @CompileDynamic inner ctor / field-initializer closure failure mode.

InnerClassTest coverage is strong: nested Mid/Inner, private/public/default trait/interface methods, lambdas, collect, SC outer + dynamic inner, and parameterized CD/TC/CS with Mid-extends-Base vs Outer-extends-Base shadowing (base:Mid).

CI green. (Codecov comment not yet posted on this PR at review time; tests exercise the new paths.)

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.

2 participants