Skip to content

GROOVY-12522: super.name() and super.name find an extension method that the class overrides - #3048

Open
codeconsole wants to merge 3 commits into
apache:masterfrom
codeconsole:super-extension-method
Open

codeconsole wants to merge 3 commits into
apache:masterfrom
codeconsole:super-extension-method

Conversation

@codeconsole

@codeconsole codeconsole commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

When a class declares its own version of an extension method of its superclass, such as first() in an ArrayList subclass, getProperties(), or getText() in a File subclass, super.first(), super.properties and super.getText() in dynamic Groovy and under @TypeChecked threw MissingMethodException or MissingPropertyException, or called getText(File, String) with a null charset. Under @CompileStatic, super.text += v compiles to a dynamic property access and failed the same way. They now call the superclass's extension method, as they do when the class does not declare one. This happens in 4.0.33 to master.

class Numbers extends ArrayList<Integer> {
    Numbers() { super([1, 2, 3]) }
    Integer first() { -1 }
    def test() { super.first() }  // threw MissingMethodException; now 1
}

How

For super usage, the meta-class replaces a method the class declares with the MOP method super$N$name of the superclass method it overrides, and drops it when there is none. An extension method has no MOP method, so the class's method was dropped, and the extension method it had replaced in the method index was not restored. It now is: the extension method of the superclass, or of an interface it implements, with the same parameters, the one for the most specific type. The lookup is built once per class, by name.

Tests

groovy.ThisAndSuperTest, in all three modes, and in a closure in dynamic Groovy and under @TypeChecked. They fail without the change. Under @CompileStatic, a super call to an extension method in a closure fails on master for another reason, with NoSuchMethodError; GROOVY-12519 (#3045) fixes that, so the closure case here runs in dynamic Groovy and under @TypeChecked only. 14,936 tests in the groovy, gls, bugs, org.codehaus.groovy.classgen, transform, runtime, reflection, vmplugin, control and ast packages and the stub generator tests pass. groovy-contracts, groovy-typecheckers, groovy-ginq, groovy-macro-library, groovy-json, groovy-xml and groovy-templates pass their tests, and every subproject compiles its main and test sources. The Checkstyle and CodeNarc gates pass.

…at the class overrides

For super usage, a method the class declares is replaced by the MOP
method super$N$name of the superclass method it overrides, or dropped
when there is none. An extension method has no MOP method, so a class
that declares its own first(), getProperties() or, extending File,
getText() lost the extension method it had replaced in the method
index: in dynamic Groovy and under @TypeChecked, super.first() threw a
MissingMethodException, super.properties a MissingPropertyException,
and super.getText() called getText(File, String) with a null charset.
Under @CompileStatic, super.text += 'x' compiles to a dynamic property
access and failed the same way. The method is now replaced by the
extension method of the superclass, or of an interface it implements,
with the same parameters, the one for the most specific type.
@codecov-commenter

codecov-commenter commented Oct 26, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 73.6879%. Comparing base (3869743) to head (8184868).

Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##               master      #3048        +/-   ##
==================================================
+ Coverage     73.6728%   73.6879%   +0.0151%     
- Complexity      42587      42597        +10     
==================================================
  Files            1678       1678                
  Lines          146913     146944        +31     
  Branches        27792      27806        +14     
==================================================
+ Hits           108235     108280        +45     
+ Misses          28526      28520         -6     
+ Partials        10152      10144         -8     
Files with missing lines Coverage Δ
src/main/java/groovy/lang/MetaClassImpl.java 79.5181% <100.0000%> (+0.3537%) ⬆️

... and 15 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.

@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 — the failure mode is clear, and restoring the hidden extension into methodsForSuper is the right place to fix it.

I think getExtensionMethodsOfSuperclass is incomplete for a common shape:

class C implements List {
def first() { … }
def t() { super.first() }
}

Here c.getSuperclass() is Object, and Object's CachedClass interface set is empty, so List's extension methods are never candidates. ArrayList/File tests pass because the superclass already carries List/etc. in getInterfaces().

Could we also collect interfaces from c itself (or from every type in the hierarchy, including c)? A regression test of the form above would lock it in.

Smaller nits: when two unrelated interfaces both contribute the same extension, the "most specific" check does not order them — a short comment or a deterministic rule would help. Otherwise the approach (isDGM/NewMetaMethod, param equality, most-specific declaring type, one map per class) looks solid, and skipping the CS-closure case until GROOVY-12519 is fine.

Happy to +1 once the implements-only case is covered.

…lf implements

super.name() looked for the overridden extension method among the
superclasses and the interfaces they implement, so in a class whose
superclass is Object, such as one that implements Iterable or delegates
List with @DeleGate, super.first() still threw a MissingMethodException.
The interfaces of the class itself are now searched as well.

The candidates are collected in the order the classes declare their
interfaces, breadth first, after the superclasses, instead of from
CachedClass.getInterfaces(), which is a hash set. Of two extension methods
for unrelated types, the one met first is used.
@codeconsole

Copy link
Copy Markdown
Contributor Author

Fixed in 39b195c. The interfaces of the class itself are now searched too, so super.first() reaches the extension method in a class that implements Iterable, or that delegates List with @Delegate. testSuperDotExtensionMethodOfAnInterfaceOfTheClass covers both cases and fails without the change.

On the ordering nit: candidates are now collected in declaration order, starting with the superclasses (nearest first), then the interfaces of the class and of each superclass, breadth first. They no longer come from CachedClass.getInterfaces(), which is a hash set. When two extension methods belong to unrelated types, the first one met wins, as the Javadoc now says.

The new test runs only in dynamic mode. Under @TypeChecked and @CompileStatic, super.first() in that class is rejected because the method comes from an interface (GROOVY-8299, GROOVY-10494). The qualified Iterable.super.first() is rejected too, with "Abstract method first() cannot be called directly". That also happens in 5.0.8 and 6.0.0, so I've filed it separately as GROOVY-12556.

# Conflicts:
#	src/test/groovy/groovy/ThisAndSuperTest.groovy
@testlens-app

testlens-app Bot commented Oct 30, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: 8184868
▶️ Tests: 123723 executed
⚪️ Checks: 27/27 completed


Learn more about TestLens at testlens.app/docs.

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