Skip to content

GROOVY-12117: replace super trait search with more complete helper stub#2678

Open
eric-milles wants to merge 1 commit into
masterfrom
GROOVY-12117
Open

GROOVY-12117: replace super trait search with more complete helper stub#2678
eric-milles wants to merge 1 commit into
masterfrom
GROOVY-12117

Conversation

@eric-milles

Copy link
Copy Markdown
Member

In order to support some code assist or type inferencing for traits, I needed to fill out the trait helper stub a little more. This solution also works for GROOVY-12117 instead of the super trait method search. That is why I could not get the 12117 test case to trip locally.

@eric-milles eric-milles requested a review from paulk-asert July 8, 2026 15:46

@github-actions github-actions Bot 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.

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.50.

Benchmark suite Current: 5f40130 Previous: aa30a69 Ratio
org.apache.groovy.bench.AryBench.java ( {"n":"10"} ) 0.006551610504040221 ms/op 0.004037354064242993 ms/op 1.62

This comment was automatically generated by workflow using github-action-benchmark.

@codecov-commenter

codecov-commenter commented Jul 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.00000% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.6961%. Comparing base (523fd1e) to head (5f40130).
⚠️ Report is 3 commits behind head on master.

Files with missing lines Patch % Lines
...va/org/codehaus/groovy/transform/trait/Traits.java 88.0000% 0 Missing and 3 partials ⚠️
Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##               master      #2678        +/-   ##
==================================================
- Coverage     68.6980%   68.6961%   -0.0019%     
+ Complexity      34121      34118         -3     
==================================================
  Files            1536       1536                
  Lines          128912     128933        +21     
  Branches        23368      23375         +7     
==================================================
+ Hits            88560      88572        +12     
- Misses          32522      32527         +5     
- Partials         7830       7834         +4     
Files with missing lines Coverage Δ
...oovy/transform/trait/TraitReceiverTransformer.java 88.8325% <ø> (+0.2753%) ⬆️
...va/org/codehaus/groovy/transform/trait/Traits.java 86.5772% <88.0000%> (-0.3459%) ⬇️

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

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

JMH summary — classic (commit bea529e)

Speedup vs trailing 90-day baseline on gh-pages. Higher = faster.
1.00 = in line with history. Per-benchmark ratio, geomean within group.
Time-per-op units inverted so direction is consistent. The calibrated
column divides out this runner's speed vs the baseline hardware, as
measured by Groovy-independent pure-Java ruler benchmarks.

Group Speedup Calibrated n
bench 1.037 × 0.996 × 78
core 1.024 × 1.065 × 77
grails 0.983 × 1.034 × 80

Runner calibration (this run vs baseline hardware): bench 1.04× (26 rulers) · core-ag 0.92× (3 rulers) · core-hz 1.01× (3 rulers) · grails-ad 0.91× (3 rulers) · grails-ez 0.98× (3 rulers)

Baseline: dev/bench/jmh/<part>/classic/data.js on gh-pages, trailing 90 days. Daily dashboard · Per-suite raw data

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

JMH summary — indy (commit bea529e)

Speedup vs trailing 90-day baseline on gh-pages. Higher = faster.
1.00 = in line with history. Per-benchmark ratio, geomean within group.
Time-per-op units inverted so direction is consistent. The calibrated
column divides out this runner's speed vs the baseline hardware, as
measured by Groovy-independent pure-Java ruler benchmarks.

Group Speedup Calibrated n
bench 0.979 × 1.002 × 78
core 3.666 × 3.525 × 77
grails 2.177 × 2.058 × 80

Runner calibration (this run vs baseline hardware): bench 0.98× (26 rulers) · core-ag 1.07× (3 rulers) · core-hz 1.00× (3 rulers) · grails-ad 1.06× (3 rulers) · grails-ez 1.06× (3 rulers)

Baseline: dev/bench/jmh/<part>/indy/data.js on gh-pages, trailing 90 days. Daily dashboard · Per-suite raw data

@paulk-asert

Copy link
Copy Markdown
Contributor

AI said the PR broke ordering, so I added an existing test to show that - and the PR breaks now - but the fix should be easy.

AI read below:


Thanks for this — replacing the super-trait search with a fuller helper stub is a nice simplification, and modelling the not-yet-lowered helper as a real stub (rather than the empty GROOVY-7909 one) is the right direction for the code-assist/inferencing goal. It also happens to fix a latent bug the old empty stub had: in SuperCallTraitTransformer, isStatic is derived from the helper's methods, so T.super.staticMethod() in the not-yet-lowered window previously received this instead of this.class; a populated stub makes that window behave like the lowered case.

One behavioural concern before this lands, though: as written, the stub drops the original method's annotations, which makes @Virtual static dispatch depend on trait transform order — exactly the order-independence this PR's own javadoc says it preserves.

Why

HelperClassStub.helperMethod adds only @Implemented, whereas the real helper method (TraitASTTransformation.processMethod) copies all of the original method's annotations, including @Virtual. TraitReceiverTransformer.findConcreteMethod's result is then branched on by hasAnnotation(methodNode, VIRTUAL_TYPE) to choose dynamic dispatch vs. declarer-bound dispatch. So for this.m(x) where m is a @Virtual public static on a super trait:

  • super trait already lowered → real helper method carries @Virtual → dynamic-dispatch path ✔
  • super trait not yet lowered (stub window) → stub method lacks @Virtual → declarer-bound path ✘

The removed getDeclaredMethods fallback returned the original MethodNode with @Virtual intact, so it was order-independent here. The failure is silent — the implementer's static override just stops being visible from trait code depending on sibling-trait declaration order.

Reproducer

This is the sub-trait-first counterpart of the existing Groovy11985.testSuperTraitPublicStaticIsPolymorphic:

@Test
void testSuperTraitPublicStaticIsPolymorphicSubTraitFirst() {
    GroovyAssert.assertScript '''
        import groovy.transform.Virtual
        trait Mid extends Base { static String greet() { hello() } }   // sub-trait declared FIRST
        trait Base { @Virtual static String hello() { 'base' } }
        class C implements Mid {}
        class D implements Mid { static String hello() { 'override' } }
        assert C.greet() == 'base'
        assert D.greet() == 'override'
    '''
}

Suggested fix

Copy the original method's annotations onto the stub method, mirroring processMethod (which excludes @Override). No new imports needed — AnnotationNode and ClassHelper are already imported in Traits.java.

             var m = new MethodNode(method.getName(), mods, method.getReturnType(), helperParams, method.getExceptions(), null);
-            m.addAnnotation(Traits.IMPLEMENTED_CLASSNODE);
+            // Mirror processMethod's annotation copy: findConcreteMethod dispatches on
+            // @Virtual, so a stub method missing it silently picks declarer-bound dispatch.
+            for (AnnotationNode annotation : method.getAnnotations()) {
+                if (!annotation.getClassNode().equals(ClassHelper.OVERRIDE_TYPE)) {
+                    m.addAnnotation(annotation);
+                }
+            }
             m.setGenericsTypes(method.getGenericsTypes());

(Dropping the explicit @Implemented is intentional: the real helper methods don't carry it either — processMethod adds @Implemented to the original interface method, not to the helper copy.)

With this change the full trait suite still passes (TraitASTTransformationTest, TraitStaticDispatchMatrix, TraitGenericsMatrix, VirtualAnnotationTest, Groovy11985/12104/12105/12106/12112), and the reproducer above goes green.

Optional fidelity tweaks (not required to fix the bug)

Since the stub now feeds code-assist/inferencing, a couple of small alignments with processMethod/createSelfParameter would make stub signatures match the lowered ones:

  • self parameter: use trait.getPlainNodeReference() and the names $self / $static$self (Traits.THIS_OBJECT / Traits.STATIC_THIS_OBJECT) instead of the raw generic node and "traitImplementer" — avoids leaking unbound placeholders into Class<T<X>>;
  • add ACC_FINAL for final instance methods, as processMethod does.

@sonarqubecloud

sonarqubecloud Bot commented Jul 9, 2026

Copy link
Copy Markdown

@testlens-app

testlens-app Bot commented Jul 9, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: 5f40130
▶️ Tests: 87054 executed
⚪️ Checks: 31/31 completed


Learn more about TestLens at testlens.app.

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