Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 70 additions & 2 deletions src/main/java/groovy/lang/MetaClassImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,13 @@
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.URL;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
Expand Down Expand Up @@ -565,6 +567,11 @@ class MOPIter extends MethodIndexAction {
*/
boolean useThis;

/**
* Extension methods of the superclass by name, per class of the hierarchy.
*/
Map<Class<?>, Map<String, List<MetaMethod>>> superExtensionMethods;

/** {@inheritDoc} */
@Override
public void methodNameAction(final Class<?> c, final MetaMethodIndex.Cache e) {
Expand All @@ -577,7 +584,12 @@ public void methodNameAction(final Class<?> c, final MetaMethodIndex.Cache e) {
if (matchedMethod >= 0) {
methods.set(i, mopMethods[matchedMethod]);
} else if (!useThis && notSuperUseful(method, c)) {
methods.remove(i--); // not fit for super usage
MetaMethod hidden = getHiddenExtensionMethod(method, c);
if (hidden != null) {
methods.set(i, hidden);
} else {
methods.remove(i--); // not fit for super usage
}
}
}
if (!useThis) {
Expand All @@ -592,7 +604,7 @@ public void methodNameAction(final Class<?> c, final MetaMethodIndex.Cache e) {
if (useThis) e.methods = mopMethods[matchedMethod];
else e.methodsForSuper = mopMethods[matchedMethod];
} else if (!useThis && notSuperUseful(method, c)) {
e.methodsForSuper = null; // not fit for super usage
e.methodsForSuper = getHiddenExtensionMethod(method, c); // null: not fit for super usage
}
}
}
Expand Down Expand Up @@ -635,6 +647,62 @@ private int mopArrayIndex(final MetaMethod method, final String mopName) {
return -1;
}

/**
* Finds the extension method of a superclass, or of an interface that
* {@code c} or a superclass implements, that a method of class {@code c}
* overrides: "super.name()" within {@code c} calls it, as there is no
* super$ helper for it. The one for the most specific type wins; of two
* for unrelated types, the one met first in the order of
* {@link #getExtensionMethodsOfSupertypes(Class)}.
*/
private MetaMethod getHiddenExtensionMethod(final MetaMethod method, final Class<?> c) {
if (method.isStatic() || isBridge(method) || method.getDeclaringClass().getTheClass() != c || c.getSuperclass() == null) return null;
if (superExtensionMethods == null) superExtensionMethods = new HashMap<>();
List<MetaMethod> candidates = superExtensionMethods.computeIfAbsent(c, this::getExtensionMethodsOfSupertypes).get(method.getName());
MetaMethod hidden = null;
if (candidates != null) {
for (MetaMethod m : candidates) {
if (MetaMethod.equal(m.getParameterTypes(), method.getParameterTypes())
&& (hidden == null || hidden.getDeclaringClass().getTheClass().isAssignableFrom(m.getDeclaringClass().getTheClass()))) {
hidden = m; // the one for the most specific type
}
}
}
return hidden;
}

/**
* The extension methods of the supertypes of {@code c} by name: those of
* the superclasses, nearest first, then those of the interfaces of
* {@code c} and of each superclass in turn, each in the order the class
* declares them, breadth first. The order is that of the declarations,
* not of {@link CachedClass#getInterfaces()}, which is a hash set.
*/
private Map<String, List<MetaMethod>> getExtensionMethodsOfSupertypes(final Class<?> c) {
Set<Class<?>> types = new LinkedHashSet<>();
for (Class<?> sc = c.getSuperclass(); sc != null; sc = sc.getSuperclass()) {
types.add(sc);
}
Deque<Class<?>> interfaces = new ArrayDeque<>();
for (Class<?> t = c; t != null; t = t.getSuperclass()) {
Collections.addAll(interfaces, t.getInterfaces());
}
while (!interfaces.isEmpty()) {
Class<?> i = interfaces.removeFirst();
if (types.add(i)) Collections.addAll(interfaces, i.getInterfaces());
}

Map<String, List<MetaMethod>> methodsByName = new HashMap<>();
for (Class<?> type : types) {
for (MetaMethod m : ReflectionCache.getCachedClass(type).getNewMetaMethods()) {
if (isDGM(m) && !m.isStatic()) {
methodsByName.computeIfAbsent(m.getName(), name -> new ArrayList<>()).add(m);
}
}
}
return methodsByName;
}

private boolean notSuperUseful(final MetaMethod method, final Class<?> c) {
return !isDGM(method) && (isBridge(method)
|| method.isAbstract() // GROOVY-11929
Expand Down
81 changes: 81 additions & 0 deletions src/test/groovy/groovy/ThisAndSuperTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,87 @@ final class ThisAndSuperTest {
"""
}

// GROOVY-12522: an extension method of the superclass, or of an interface of it, that the class overrides
@ParameterizedTest
@ValueSource(strings=['', '@groovy.transform.TypeChecked', '@groovy.transform.CompileStatic'])
void testSuperDotExtensionMethodTheClassOverrides(String mode) {
assertScript """
$mode
class Numbers extends ArrayList<Integer> {
Numbers() { super([1, 2, 3]) }
Integer first() { -1 }
Map getProperties() { [:] }
List test() {
[super.first(), first(), super.properties.containsKey('class'), properties]
}
}
$mode
class Text extends File {
Text(File file) { super(file.path) }
String getText() { 'own' }
void setText(String s) { throw new UnsupportedOperationException(s) }
List test() {
super.setText('a')
def one = super.getText()
super.text = 'b'
def two = super.text
super.text += 'c'
[one, two, super.text, text]
}
}
assert new Numbers().test() == [1, -1, true, [:]]
def file = File.createTempFile('text', '.txt')
try {
assert new Text(file).test() == ['a', 'b', 'bc', 'own']
} finally {
file.delete()
}
"""
}

// GROOVY-12522: in a closure; @CompileStatic calls a super method from a closure through a super$ helper
@ParameterizedTest
@ValueSource(strings=['', '@groovy.transform.TypeChecked'])
void testSuperDotExtensionMethodTheClassOverridesInClosure(String mode) {
assertScript """
$mode
class Numbers extends ArrayList<Integer> {
Numbers() { super([1, 2, 3]) }
Integer first() { -1 }
List test() {
def c = { -> super.first() }
[c(), first()]
}
}
assert new Numbers().test() == [1, -1]
"""
}

// GROOVY-12522: an extension method of an interface the class itself implements, the superclass being Object;
// static type checking rejects an interface method called through unqualified super (GROOVY-8299, GROOVY-10494)
@Test
void testSuperDotExtensionMethodOfAnInterfaceOfTheClass() {
assertScript '''
class Numbers implements Iterable<Integer> {
Iterator<Integer> iterator() { [1, 2, 3].iterator() }
Integer first() { -1 }
List test() {
[super.first(), first()]
}
}
class Delegating implements List<Integer> {
@Delegate List<Integer> list = [4, 5]
Integer first() { -1 }
Integer last() { -2 }
List test() {
[super.first(), super.last(), first(), last()]
}
}
assert new Numbers().test() == [1, -1]
assert new Delegating().test() == [4, 5, -1, -2]
'''
}

// GROOVY-9851
@Test
void testPrivateSuperMethod() {
Expand Down
Loading