Skip to content

Commit 11a776b

Browse files
committed
GROOVY-12105: Unqualified super.m(...) from a static trait method throws MissingMethodException — should be a compile error
1 parent e4a9bb8 commit 11a776b

3 files changed

Lines changed: 181 additions & 10 deletions

File tree

src/main/java/org/codehaus/groovy/transform/trait/TraitReceiverTransformer.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.codehaus.groovy.ast.Variable;
3131
import org.codehaus.groovy.ast.expr.ArgumentListExpression;
3232
import org.codehaus.groovy.ast.expr.BinaryExpression;
33+
import org.codehaus.groovy.ast.expr.ClassExpression;
3334
import org.codehaus.groovy.ast.expr.ClosureExpression;
3435
import org.codehaus.groovy.ast.expr.ConstantExpression;
3536
import org.codehaus.groovy.ast.expr.DeclarationExpression;
@@ -119,6 +120,28 @@ public Expression transform(final Expression exp) {
119120
return transformBinaryExpression((BinaryExpression) exp);
120121
} else if (exp instanceof MethodCallExpression mce) {
121122
String obj = mce.getObjectExpression().getText();
123+
// GROOVY-12105: in a static trait method, the parser/resolver
124+
// rewrites `super.m(...)` to a static call on the trait's
125+
// declared superclass (typically Object) before we see it. Reject
126+
// that pattern at compile time, pointing at `T.super.m(...)` as
127+
// the supported explicit form. The discriminator is
128+
// `mce.isImplicitThis()`: the rewritten super call carries
129+
// `isImplicitThis=true` (because the user wrote no explicit
130+
// receiver — the ClassExpression was synthesised by the
131+
// resolver), while an explicit `ClassName.m()` call from
132+
// user source has `isImplicitThis=false`. The synthesised
133+
// ClassExpression also has no source position (line=-1), an
134+
// independent signal of the same fact.
135+
if (ClassHelper.isClassType(weaved.getOriginType())
136+
&& mce.isImplicitThis()
137+
&& mce.getObjectExpression() instanceof ClassExpression ce
138+
&& traitClass.getSuperClass() != null
139+
&& ce.getType().equals(traitClass.getSuperClass())) {
140+
unit.addError(new SyntaxException(
141+
"'super' is not allowed in a static trait method; use '" + traitClass.getNameWithoutPackage() + ".super." + mce.getMethodAsString() + "(...)' for explicit trait-anchored dispatch",
142+
mce.getLineNumber(), mce.getColumnNumber()));
143+
return mce;
144+
}
122145
if ("super".equals(obj)) {
123146
return transformSuperMethodCall(mce); // super.m(x) --> $self.Ttrait$super$m(x)
124147
} else if ("this".equals(obj)) {
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.codehaus.groovy.transform.traitx
20+
21+
import org.codehaus.groovy.control.MultipleCompilationErrorsException
22+
import org.junit.Test
23+
24+
import static groovy.test.GroovyAssert.assertScript
25+
import static groovy.test.GroovyAssert.shouldFail
26+
27+
/**
28+
* Unqualified {@code super.m(...)} from a static trait method is rejected
29+
* at compile time (GROOVY-12105). Without the reject, the call previously
30+
* compiled successfully and threw {@code MissingMethodException} at
31+
* runtime — the trait helper does not walk the trait chain for statics.
32+
* The supported explicit form is {@code T.super.m(...)} (GEP-22 § this,
33+
* super, and stackable traits item 3).
34+
*
35+
* <p>This restriction applies to <em>trait</em> static methods only.
36+
* Groovy continues to permit unqualified {@code super.m(...)} in static
37+
* methods of plain classes, where it dispatches to the superclass's
38+
* static method (Java-conventional behaviour for class hierarchies) —
39+
* see {@link #unqualifiedSuper_inPlainClassStatic_stillWorks()}.
40+
*/
41+
final class Groovy12105 {
42+
43+
/** Unqualified super in static trait method → compile error. */
44+
@Test
45+
void unqualifiedSuper_inStaticTraitMethod_isCompileError() {
46+
def err = shouldFail(MultipleCompilationErrorsException) {
47+
assertScript '''
48+
trait Base {
49+
static String m() { 'Base' }
50+
}
51+
trait V extends Base {
52+
static String m() { 'V' }
53+
static callSuper() { super.m() } // <-- rejected
54+
}
55+
class Impl implements V { }
56+
Impl.callSuper()
57+
'''
58+
}
59+
assert err.message.contains("'super' is not allowed in a static trait method")
60+
assert err.message.contains('V.super.m(')
61+
}
62+
63+
/** Unqualified super in static trait method, no super-trait → compile error. */
64+
@Test
65+
void unqualifiedSuper_inStaticTraitMethod_noSuperTrait_isCompileError() {
66+
def err = shouldFail(MultipleCompilationErrorsException) {
67+
assertScript '''
68+
trait V {
69+
static String m() { 'V' }
70+
static callSuper() { super.m() } // <-- rejected even without a super-trait
71+
}
72+
class Impl implements V { }
73+
Impl.callSuper()
74+
'''
75+
}
76+
assert err.message.contains("'super' is not allowed in a static trait method")
77+
}
78+
79+
/** The supported explicit form T.super.m() still works for static methods. */
80+
@Test
81+
void traitQualifiedSuper_inStaticTraitMethod_works() {
82+
assertScript '''
83+
trait V {
84+
static String m() { 'V' }
85+
static callTSuper() { V.super.m() } // explicit, supported
86+
}
87+
class Impl implements V { }
88+
assert Impl.callTSuper() == 'V'
89+
'''
90+
}
91+
92+
/** Unqualified super in an INSTANCE trait method is unaffected (regression guard). */
93+
@Test
94+
void unqualifiedSuper_inInstanceTraitMethod_stillWorks() {
95+
assertScript '''
96+
trait Base {
97+
def m() { 'Base' }
98+
}
99+
trait V extends Base {
100+
def m() { 'V' }
101+
def callSuper() { super.m() } // instance method, walks chain → 'Base'
102+
}
103+
class Impl implements V { }
104+
assert new Impl().callSuper() == 'Base'
105+
'''
106+
}
107+
108+
/** Plain (non-trait) class static super.m() is unaffected (the reject is trait-only). */
109+
@Test
110+
void unqualifiedSuper_inPlainClassStatic_stillWorks() {
111+
assertScript '''
112+
class Base {
113+
static String m() { 'Base' }
114+
}
115+
class Sub extends Base {
116+
static String m() { 'Sub' }
117+
static String callSuper() { super.m() }
118+
}
119+
assert Sub.callSuper() == 'Base'
120+
'''
121+
}
122+
123+
/**
124+
* Explicit class-qualified static calls in a trait static body — even
125+
* to the trait's own declared superclass — must NOT be rejected. The
126+
* reject heuristic relies on {@code isImplicitThis()} being true (set
127+
* by the resolver when it rewrites {@code super.m()} into a
128+
* synthesised {@code ClassExpression}); explicit user-written
129+
* {@code ClassName.m()} calls have {@code isImplicitThis()} false and
130+
* are left alone.
131+
*/
132+
@Test
133+
void explicitObjectStaticCall_inTraitStatic_isAllowed() {
134+
// Object.toString() is technically a method-not-found at runtime (it's
135+
// an instance method), but the static-analysis surface that matters
136+
// here is whether the call is REJECTED BY THE TRAIT TRANSFORM. It
137+
// must reach the runtime, where MOP behaviour takes over.
138+
assertScript '''
139+
trait V {
140+
static String describe() {
141+
try { Object.toString() } catch (any) { 'mop-not-found' }
142+
}
143+
}
144+
class Impl implements V { }
145+
// No compile error — the heuristic distinguishes this from
146+
// a rewritten super.m() call via mce.isImplicitThis().
147+
assert Impl.describe() in ['java.lang.Object', 'class java.lang.Object', 'mop-not-found']
148+
'''
149+
}
150+
}

src/test/groovy/org/codehaus/groovy/transform/traitx/TraitStaticDispatchMatrix.groovy

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -538,17 +538,15 @@ class TraitStaticDispatchMatrix {
538538
}
539539
}
540540

541-
// ---- Row 15 — unqualified super.m() from trait STATIC method (proposed compile error) ----
542-
// Companion to row 14, same workstream. Unqualified `super.m()` inside a
543-
// trait instance method walks the trait chain (spec item 2); from a trait
544-
// STATIC method the chain is not walked and the call throws
545-
// MissingMethodException at runtime. GEP-22 item 4 proposes rejecting
546-
// the static-context use at compile time, leaving `T.super.m()` as the
547-
// explicit form. NYI until the fix lands; flips red on a build that
548-
// implements the rejection.
541+
// ---- Row 15 — unqualified super.m() from trait STATIC method (GROOVY-12105) ----
542+
// Unqualified `super.m()` inside a trait instance method walks the trait
543+
// chain (spec item 2); from a trait STATIC method the chain is not
544+
// walked. GROOVY-12105 rejects the static-context use at compile time,
545+
// pointing at `T.super.m()` as the explicit form. See Groovy12105.groovy
546+
// for focused coverage (regression-guards for the instance-method and
547+
// plain-class super-call cases). This row is the matrix-level anchor.
549548
@Test
550-
@NotYetImplemented
551-
void row15_unqualified_static_super_inTrait_shouldBeCompileError_PROPOSED() {
549+
void row15_unqualified_static_super_inTrait_isCompileError() {
552550
GroovyAssert.shouldFail(MultipleCompilationErrorsException) {
553551
ev '''
554552
trait Base { static String m() { 'Base' } }

0 commit comments

Comments
 (0)