blob: 50d5c1b76115fb320d03eb7648bfd99d79eb0d57 [file] [log] [blame]
// Copyright 2017 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.devtools.build.android.desugar;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSortedSet;
import com.google.devtools.build.android.desugar.io.BitFlags;
import com.google.devtools.build.android.desugar.langmodel.ClassName;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.TreeMap;
import javax.annotation.Nullable;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
/**
* Fixer of classes that extend interfaces with default methods to declare any missing methods
* explicitly and call the corresponding companion method generated by {@link InterfaceDesugaring}.
*/
public class DefaultMethodClassFixer extends ClassVisitor {
private final boolean useGeneratedBaseClasses;
private final ClassReaderFactory classpath;
private final ClassReaderFactory bootclasspath;
private final ClassLoader targetLoader;
private final DependencyCollector depsCollector;
@Nullable private final CoreLibrarySupport coreLibrarySupport;
private final LinkedHashSet<String> instanceMethods = new LinkedHashSet<>();
private boolean isInterface;
private String internalName;
private ImmutableList<String> directInterfaces;
private String superName;
/** Interface whose companion was chosen as base class, if any. */
@Nullable private Class<?> newSuperName;
/** This method node caches <clinit>, and flushes out in {@code visitEnd()}; */
private MethodNode clInitMethodNode;
private ImmutableSortedSet<Class<?>> interfacesToStub;
public DefaultMethodClassFixer(
ClassVisitor dest,
boolean useGeneratedBaseClasses,
ClassReaderFactory classpath,
DependencyCollector depsCollector,
@Nullable CoreLibrarySupport coreLibrarySupport,
ClassReaderFactory bootclasspath,
ClassLoader targetLoader) {
super(Opcodes.ASM7, dest);
this.useGeneratedBaseClasses = useGeneratedBaseClasses;
this.classpath = classpath;
this.coreLibrarySupport = coreLibrarySupport;
this.bootclasspath = bootclasspath;
this.targetLoader = targetLoader;
this.depsCollector = depsCollector;
}
@Override
public void visit(
int version,
int access,
String name,
String signature,
String superName,
String[] interfaces) {
checkState(this.directInterfaces == null);
isInterface = BitFlags.isSet(access, Opcodes.ACC_INTERFACE);
internalName = name;
checkArgument(
superName != null || "java/lang/Object".equals(name), // ASM promises this
"Type without superclass: %s",
name);
this.directInterfaces = ImmutableList.copyOf(interfaces);
this.superName = superName;
if (!isInterface
&& (mayNeedInterfaceStubsForEmulatedSuperclass()
|| defaultMethodsDefined(directInterfaces))) {
TreeMap<Class<?>, Long> allInterfaces = new TreeMap<>(SubtypeComparator.INSTANCE);
for (String direct : interfaces) {
if (InterfaceDesugaring.getCompanionClassName(direct).equals(name)) {
checkState(useGeneratedBaseClasses, "%s shouldn't implement %s", name, direct);
continue; // InterfaceDesugaring already made stubs for this interface
}
// Loading ensures all transitively implemented interfaces can be loaded, which is necessary
// to produce correct default method stubs in all cases. We could do without classloading
// but it's convenient to rely on Class.isAssignableFrom to compute subtype relationships,
// and we'd still have to insist that all transitively implemented interfaces can be loaded.
// We don't load the visited class, however, in case it's generated.
Class<?> itf = loadFromInternal(direct);
collectInterfaces(itf, allInterfaces);
}
if (mayNeedInterfaceStubsForEmulatedSuperclass()) {
// Collect interfaces inherited from emulated superclasses as well, to handle things like
// extending AbstractList without explicitly implementing List.
for (Class<?> clazz = loadFromInternal(superName);
clazz != null;
clazz = clazz.getSuperclass()) {
for (Class<?> itf : clazz.getInterfaces()) {
collectInterfaces(itf, allInterfaces);
}
}
}
if (useGeneratedBaseClasses && "java/lang/Object".equals(superName)) {
// If the class directly extends Object, use the generated base class for the interface with
// the most default methods instead. This can help avoid stubbed default methods when
// they're already defined in the base class.
// This is an imperfect heuristic, not least b/c we don't know yet what methods would need
// to be stubbed, but we decide this here up-front b/c that lets us fix up the class's
// constructors' super calls when we see them.
// Note inherited default methods are included in the count, so we will choose subtypes over
// supertypes, which is desirable.
long maxBaseMethodCount = 0;
for (Map.Entry<Class<?>, Long> itf : allInterfaces.entrySet()) {
if (itf.getValue() > maxBaseMethodCount) {
maxBaseMethodCount = itf.getValue();
newSuperName = itf.getKey();
superName = InterfaceDesugaring.getCompanionClassName(internalName(itf.getKey()));
signature = null; // Changing superclass invalidates signature
}
}
if (newSuperName != null && !bootclasspath.isKnown(internalName(newSuperName))) {
// Record dependency on the chosen companion class
depsCollector.assumeCompanionClass(
internalName, InterfaceDesugaring.getCompanionClassName(internalName(newSuperName)));
}
}
interfacesToStub = ImmutableSortedSet.copyOfSorted(allInterfaces.navigableKeySet());
} else {
interfacesToStub = ImmutableSortedSet.of();
}
super.visit(version, access, name, signature, superName, interfaces);
}
@Override
public void visitEnd() {
if (!interfacesToStub.isEmpty()) {
// Inherited methods take precedence over default methods, so visit all superclasses and
// figure out what methods they declare before stubbing in any missing default methods.
recordInheritedMethods();
stubMissingDefaultAndBridgeMethods();
// Check whether there are interfaces with default methods and <clinit>. If yes, the following
// method call will return a list of interface fields to access in the <clinit> to trigger
// the initialization of these interfaces.
ImmutableList<String> companionsToTriggerInterfaceClinit =
collectOrderedCompanionsToTriggerInterfaceClinit(directInterfaces);
if (!companionsToTriggerInterfaceClinit.isEmpty()) {
if (clInitMethodNode == null) {
clInitMethodNode = new MethodNode(Opcodes.ACC_STATIC, "<clinit>", "()V", null, null);
}
desugarClinitToTriggerInterfaceInitializers(companionsToTriggerInterfaceClinit);
}
}
if (clInitMethodNode != null && super.cv != null) { // Write <clinit> to the chained visitor.
clInitMethodNode.accept(super.cv);
}
super.visitEnd();
}
private boolean isClinitAlreadyDesugared(
ImmutableList<String> companionsToAccessToTriggerInterfaceClinit) {
InsnList instructions = clInitMethodNode.instructions;
if (instructions.size() <= companionsToAccessToTriggerInterfaceClinit.size()) {
// The <clinit> must end with RETURN, so if the instruction count is less than or equal to
// the companion class count, this <clinit> has not been desugared.
return false;
}
Iterator<AbstractInsnNode> iterator = instructions.iterator();
for (String companion : companionsToAccessToTriggerInterfaceClinit) {
if (!iterator.hasNext()) {
return false;
}
AbstractInsnNode insn = iterator.next();
if (!(insn instanceof MethodInsnNode)) {
return false;
}
MethodInsnNode methodInsnNode = (MethodInsnNode) insn;
if (methodInsnNode.getOpcode() != Opcodes.INVOKESTATIC
|| !methodInsnNode.owner.equals(companion)
|| !methodInsnNode.name.equals(
InterfaceDesugaring.COMPANION_METHOD_TO_TRIGGER_INTERFACE_CLINIT_NAME)) {
return false;
}
checkState(
methodInsnNode.desc.equals(
InterfaceDesugaring.COMPANION_METHOD_TO_TRIGGER_INTERFACE_CLINIT_DESC),
"Inconsistent method desc: %s vs %s",
methodInsnNode.desc,
InterfaceDesugaring.COMPANION_METHOD_TO_TRIGGER_INTERFACE_CLINIT_DESC);
}
return true;
}
private void desugarClinitToTriggerInterfaceInitializers(
ImmutableList<String> companionsToTriggerInterfaceClinit) {
if (isClinitAlreadyDesugared(companionsToTriggerInterfaceClinit)) {
return;
}
InsnList desugarInsts = new InsnList();
for (String companionClass : companionsToTriggerInterfaceClinit) {
desugarInsts.add(
new MethodInsnNode(
Opcodes.INVOKESTATIC,
companionClass,
InterfaceDesugaring.COMPANION_METHOD_TO_TRIGGER_INTERFACE_CLINIT_NAME,
InterfaceDesugaring.COMPANION_METHOD_TO_TRIGGER_INTERFACE_CLINIT_DESC,
false));
}
if (clInitMethodNode.instructions.size() == 0) {
clInitMethodNode.instructions.insert(new InsnNode(Opcodes.RETURN));
}
clInitMethodNode.instructions.insertBefore(
clInitMethodNode.instructions.getFirst(), desugarInsts);
}
@Override
public MethodVisitor visitMethod(
int access, String name, String desc, String signature, String[] exceptions) {
// Keep track of instance methods implemented in this class for later.
if (!isInterface) {
recordIfInstanceMethod(access, name, desc);
}
if ("<clinit>".equals(name)) {
checkState(clInitMethodNode == null, "This class fixer has been used. ");
clInitMethodNode = new MethodNode(access, name, desc, signature, exceptions);
return clInitMethodNode;
}
MethodVisitor dest = super.visitMethod(access, name, desc, signature, exceptions);
if (newSuperName != null && "<init>".equals(name)) {
// Since we changed the base class we need to fix up super constructor calls.
return new ConstructorFixer(
dest, InterfaceDesugaring.getCompanionClassName(internalName(newSuperName)));
}
return dest;
}
private boolean mayNeedInterfaceStubsForEmulatedSuperclass() {
return coreLibrarySupport != null
&& !coreLibrarySupport.isEmulatedCoreClassOrInterface(internalName)
&& coreLibrarySupport.isEmulatedCoreClassOrInterface(superName);
}
private void stubMissingDefaultAndBridgeMethods() {
Class<?> superclass = loadFromInternal(superName);
boolean mayNeedStubsForSuperclass = mayNeedInterfaceStubsForEmulatedSuperclass();
for (Class<?> interfaceToVisit : interfacesToStub) {
// if J extends I, J is allowed to redefine I's default methods. The comparator we used
// above makes sure we visit J before I in that case so we can use J's definition.
if (!mayNeedStubsForSuperclass && interfaceToVisit.isAssignableFrom(superclass)) {
// superclass is also rewritten and already implements this interface, so we _must_ skip it.
continue;
}
stubMissingDefaultAndBridgeMethods(internalName(interfaceToVisit), mayNeedStubsForSuperclass);
}
}
private void stubMissingDefaultAndBridgeMethods(
String implemented, boolean mayNeedStubsForSuperclass) {
ClassReader bytecode;
boolean isBootclasspath;
if (bootclasspath.isKnown(implemented)) {
if (coreLibrarySupport != null
&& (coreLibrarySupport.isRenamedCoreLibrary(implemented)
|| coreLibrarySupport.isEmulatedCoreClassOrInterface(implemented))) {
bytecode = checkNotNull(bootclasspath.readIfKnown(implemented), implemented);
isBootclasspath = true;
} else {
// Default methods from interfaces on the bootclasspath that we're not renaming or emulating
// are assumed available at runtime, so just ignore them.
return;
}
} else {
bytecode =
checkNotNull(
classpath.readIfKnown(implemented),
"Couldn't find interface %s implemented by %s",
implemented,
internalName);
isBootclasspath = false;
}
bytecode.accept(
new DefaultMethodStubber(isBootclasspath, mayNeedStubsForSuperclass),
ClassReader.SKIP_DEBUG);
}
private Class<?> loadFromInternal(String internalName) {
try {
return targetLoader.loadClass(internalName.replace('/', '.'));
} catch (ClassNotFoundException | NoClassDefFoundError e) {
throw new IllegalStateException(
"Couldn't load " + internalName + " to stub default methods for " + this.internalName, e);
}
}
/** Returns the internal name used for this class in bytecode. */
static String internalName(Class<?> clazz) {
return clazz.getName().replace('.', '/');
}
private void collectInterfaces(Class<?> itf, Map<Class<?>, Long> dest) {
checkArgument(itf.isInterface());
if (dest.containsKey(itf)) {
return;
}
if (useGeneratedBaseClasses && hasCompanionClass(itf)) {
// Count inherited and declared default methods
long defaultMethodCount = Arrays.stream(itf.getMethods()).filter(Method::isDefault).count();
dest.put(itf, defaultMethodCount);
} else {
// Use -1 if there's no base class we could use. This makes sure we don't try to use a
// companion class as base class that doesn't exist.
dest.put(itf, -1L);
}
for (Class<?> implemented : itf.getInterfaces()) {
collectInterfaces(implemented, dest);
}
}
private boolean hasCompanionClass(Class<?> itf) {
checkArgument(itf.isInterface());
if (Arrays.stream(itf.getDeclaredMethods()).noneMatch(m -> m.isDefault() && !m.isBridge())) {
return false;
}
String implemented = internalName(itf);
if (implemented.startsWith("java/")
|| implemented.startsWith(ClassName.IN_PROCESS_LABEL + "java/")) {
return coreLibrarySupport != null
&& (coreLibrarySupport.isRenamedCoreLibrary(implemented)
|| coreLibrarySupport.isEmulatedCoreClassOrInterface(implemented));
} else {
return !bootclasspath.isKnown(implemented);
}
}
private void recordInheritedMethods() {
InstanceMethodRecorder recorder =
new InstanceMethodRecorder(mayNeedInterfaceStubsForEmulatedSuperclass());
if (newSuperName != null) {
checkState(useGeneratedBaseClasses);
// If we're using a generated base class, walk the implemented interfaces and record default
// methods we won't need to stub. That reflects the methods that will be in the generated
// base class (and its superclasses, if applicable), without looking at the generated class.
// We go through sub-interfaces before their super-interfaces (same order as when we stub)
// to encounter overriding before overridden default methods. We don't record methods from
// interfaces the chosen base class doesn't implement, even if those methods also appear in
// interfaces we would normally record later. That ensures we generate a stub when a default
// method is available in the base class but needs to be overridden due to an overriding
// default method in a sub-interface not implemented by the base class.
LinkedHashSet<String> allSeen = new LinkedHashSet<>();
for (Class<?> itf : interfacesToStub) {
boolean willBeInBaseClass = itf.isAssignableFrom(newSuperName);
for (Method m : itf.getDeclaredMethods()) {
if (!m.isDefault()) {
continue;
}
String desc = Type.getMethodDescriptor(m);
if (coreLibrarySupport != null) {
// Foreshadow any type renaming to avoid issues with double-desugaring (b/111447199)
desc = coreLibrarySupport.getRemapper().mapMethodDesc(desc);
}
String methodKey = m.getName() + ":" + desc;
if (allSeen.add(methodKey) && willBeInBaseClass) {
// Only record never-seen methods, and only for super-types of newSuperName (see longer
// explanation above)
instanceMethods.add(methodKey);
}
}
}
// Fall through to the logic below to record j.l.Object's methods.
checkState(superName.equals("java/lang/Object"));
}
// Walk superclasses
String internalName = superName;
while (internalName != null) {
ClassReader bytecode = bootclasspath.readIfKnown(internalName);
if (bytecode == null) {
bytecode =
checkNotNull(
classpath.readIfKnown(internalName), "Superclass not found: %s", internalName);
}
bytecode.accept(recorder, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG);
internalName = bytecode.getSuperName();
}
}
/**
* Starting from the given interfaces, this method scans the interface hierarchy, finds the
* interfaces that have default methods and <clinit>, and returns the companion class names of
* these interfaces.
*
* <p>Note that the returned companion classes are ordered in the order of the interface
* initialization, which is consistent with the JVM behavior. For example, "class A implements I1,
* I2", the returned list would be [I1$$CC, I2$$CC], not [I2$$CC, I1$$CC].
*/
private ImmutableList<String> collectOrderedCompanionsToTriggerInterfaceClinit(
ImmutableList<String> interfaces) {
ImmutableList.Builder<String> companionCollector = ImmutableList.builder();
LinkedHashSet<String> visitedInterfaces = new LinkedHashSet<>();
for (String anInterface : interfaces) {
collectOrderedCompanionsToTriggerInterfaceClinit(
anInterface, visitedInterfaces, companionCollector);
}
return companionCollector.build();
}
private void collectOrderedCompanionsToTriggerInterfaceClinit(
String anInterface,
LinkedHashSet<String> visitedInterfaces,
ImmutableList.Builder<String> companionCollector) {
if (!visitedInterfaces.add(anInterface)) {
return;
}
ClassReader bytecode = classpath.readIfKnown(anInterface);
if (bytecode == null || bootclasspath.isKnown(anInterface)) {
return;
}
String[] parentInterfaces = bytecode.getInterfaces();
if (parentInterfaces != null && parentInterfaces.length > 0) {
for (String parentInterface : parentInterfaces) {
collectOrderedCompanionsToTriggerInterfaceClinit(
parentInterface, visitedInterfaces, companionCollector);
}
}
InterfaceInitializationNecessityDetector necessityDetector =
new InterfaceInitializationNecessityDetector(bytecode.getClassName());
bytecode.accept(necessityDetector, ClassReader.SKIP_DEBUG);
if (necessityDetector.needsToInitialize()) {
// If we need to initialize this interface, we initialize its companion class, and its
// companion class will initialize the interface then. This desigin decision is made to avoid
// access issue, e.g., package-private interfaces.
companionCollector.add(InterfaceDesugaring.getCompanionClassName(anInterface));
}
}
/**
* Recursively searches the given interfaces for default methods not implemented by this class
* directly. If this method returns true we need to think about stubbing missing default methods.
*/
private boolean defaultMethodsDefined(ImmutableList<String> interfaces) {
for (String implemented : interfaces) {
if (useGeneratedBaseClasses
&& InterfaceDesugaring.getCompanionClassName(implemented).equals(internalName)) {
continue; // InterfaceDesugaring already made stubs for this interface
}
ClassReader bytecode;
if (bootclasspath.isKnown(implemented)) {
if (coreLibrarySupport != null
&& coreLibrarySupport.isEmulatedCoreClassOrInterface(implemented)) {
return true; // need to stub in emulated interface methods such as Collection.stream()
} else if (coreLibrarySupport != null
&& coreLibrarySupport.isRenamedCoreLibrary(implemented)) {
// Check default methods of renamed interfaces
bytecode = checkNotNull(bootclasspath.readIfKnown(implemented), implemented);
} else {
continue;
}
} else {
bytecode = classpath.readIfKnown(implemented);
if (bytecode == null) {
// Interface isn't on the classpath, which indicates incomplete classpaths. Record missing
// dependency so we can check it later. If we don't check then we may get runtime
// failures or wrong behavior from default methods that should've been stubbed in.
// TODO(kmb): Print a warning so people can start fixing their deps?
depsCollector.missingImplementedInterface(internalName, implemented);
continue;
}
}
// Class in classpath and bootclasspath is a bad idea but in any event, assume the
// bootclasspath will take precedence like in a classloader.
// We can skip code attributes as we just need to find default methods to stub.
DefaultMethodFinder finder = new DefaultMethodFinder();
bytecode.accept(finder, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG);
if (finder.foundDefaultMethods()) {
return true;
}
}
return false;
}
/** Returns {@code true} for non-bridge default methods not in {@link #instanceMethods}. */
private boolean shouldStubAsDefaultMethod(int access, String name, String desc) {
// Ignore private methods, which technically aren't default methods and can only be called from
// other methods defined in the interface. This also ignores lambda body methods, which is fine
// as we don't want or need to stub those. Also ignore bridge methods as javac adds them to
// concrete classes as needed anyway and we handle them separately for generated lambda classes.
// Note that an exception is that, if a bridge method is for a default interface method, javac
// will NOT generate the bridge method in the implementing class. So we need extra logic to
// handle these bridge methods.
return isNonBridgeDefaultMethod(access) && !recordedInstanceMethod(name, desc);
}
private static boolean isNonBridgeDefaultMethod(int access) {
return BitFlags.noneSet(
access,
Opcodes.ACC_ABSTRACT | Opcodes.ACC_STATIC | Opcodes.ACC_BRIDGE | Opcodes.ACC_PRIVATE);
}
/**
* Check whether an interface method is a bridge method for a default interface method. This type
* of bridge methods is special, as they are not put in the implementing classes by javac.
*/
private boolean shouldStubAsBridgeDefaultMethod(int access, String name, String desc) {
return BitFlags.isSet(access, Opcodes.ACC_BRIDGE | Opcodes.ACC_PUBLIC)
&& BitFlags.noneSet(access, Opcodes.ACC_ABSTRACT | Opcodes.ACC_STATIC)
&& !recordedInstanceMethod(name, desc);
}
private void recordIfInstanceMethod(int access, String name, String desc) {
if (BitFlags.noneSet(access, Opcodes.ACC_STATIC)) {
// Record all declared instance methods, including abstract, bridge, and native methods, as
// they all take precedence over default methods.
if (coreLibrarySupport != null) {
// Foreshadow any type renaming to avoid issues with double-desugaring (b/111447199)
desc = coreLibrarySupport.getRemapper().mapMethodDesc(desc);
}
instanceMethods.add(name + ":" + desc);
}
}
private boolean recordedInstanceMethod(String name, String desc) {
if (coreLibrarySupport != null) {
// Foreshadow any type renaming to avoid issues with double-desugaring (b/111447199)
desc = coreLibrarySupport.getRemapper().mapMethodDesc(desc);
}
return instanceMethods.contains(name + ":" + desc);
}
/**
* Visitor for interfaces that produces delegates in the class visited by the outer {@link
* DefaultMethodClassFixer} for every default method encountered.
*/
private class DefaultMethodStubber extends ClassVisitor {
private final boolean isBootclasspathInterface;
private final boolean mayNeedStubsForSuperclass;
private String stubbedInterfaceName;
public DefaultMethodStubber(
boolean isBootclasspathInterface, boolean mayNeedStubsForSuperclass) {
super(Opcodes.ASM7);
this.isBootclasspathInterface = isBootclasspathInterface;
this.mayNeedStubsForSuperclass = mayNeedStubsForSuperclass;
}
@Override
public void visit(
int version,
int access,
String name,
String signature,
String superName,
String[] interfaces) {
checkArgument(BitFlags.isSet(access, Opcodes.ACC_INTERFACE));
checkState(stubbedInterfaceName == null);
stubbedInterfaceName = name;
}
@Override
public MethodVisitor visitMethod(
int access, String name, String desc, String signature, String[] exceptions) {
if (shouldStubAsDefaultMethod(access, name, desc)) {
// Remember we stubbed this method in case it's also defined by subsequently visited
// interfaces. javac would force the method to be defined explicitly if there any two
// definitions conflict, but see stubMissingDefaultMethods() for how we deal with default
// methods redefined in interfaces extending another.
recordIfInstanceMethod(access, name, desc);
String owner = InterfaceDesugaring.getCompanionClassName(stubbedInterfaceName);
if (!isBootclasspathInterface) {
// Don't record these dependencies, as we can't check them
depsCollector.assumeCompanionClass(internalName, owner);
}
// Add this method to the class we're desugaring and stub in a body to call the default
// implementation in the interface's companion class. ijar omits these methods when setting
// ACC_SYNTHETIC modifier, so don't.
// Signatures can be wrong, e.g., when type variables are introduced, instantiated, or
// refined in the class we're processing, so drop them.
MethodVisitor stubMethod =
DefaultMethodClassFixer.this.visitMethod(access, name, desc, (String) null, exceptions);
String receiverName = stubbedInterfaceName;
String calledMethodName = name + InterfaceDesugaring.DEFAULT_COMPANION_METHOD_SUFFIX;
if (mayNeedStubsForSuperclass) {
// Reflect what CoreLibraryInvocationRewriter would do if it encountered a super-call to a
// moved implementation of an emulated method. Equivalent to emitting the invokespecial
// super call here and relying on CoreLibraryInvocationRewriter for the rest
Class<?> emulatedImplementation =
coreLibrarySupport.getCoreInterfaceRewritingTarget(
Opcodes.INVOKESPECIAL, superName, name, desc, /*itf=*/ false);
if (emulatedImplementation != null && !emulatedImplementation.isInterface()) {
receiverName = internalName(emulatedImplementation);
owner = checkNotNull(coreLibrarySupport.getMoveTarget(receiverName, name));
calledMethodName = name;
}
}
int slot = 0;
stubMethod.visitVarInsn(Opcodes.ALOAD, slot++); // load the receiver
Type neededType = Type.getMethodType(desc);
for (Type arg : neededType.getArgumentTypes()) {
stubMethod.visitVarInsn(arg.getOpcode(Opcodes.ILOAD), slot);
slot += arg.getSize();
}
stubMethod.visitMethodInsn(
Opcodes.INVOKESTATIC,
owner,
calledMethodName,
InterfaceDesugaring.companionDefaultMethodDescriptor(receiverName, desc),
/*isInterface=*/ false);
stubMethod.visitInsn(neededType.getReturnType().getOpcode(Opcodes.IRETURN));
stubMethod.visitMaxs(0, 0); // rely on class writer to compute these
stubMethod.visitEnd();
return null; // don't visit the visited interface's default method
} else if (shouldStubAsBridgeDefaultMethod(access, name, desc)) {
recordIfInstanceMethod(access, name, desc);
MethodVisitor stubMethod =
DefaultMethodClassFixer.this.visitMethod(access, name, desc, (String) null, exceptions);
// If we're visiting a bootclasspath interface then we most likely don't have the code.
// That means we can't just copy the method bodies as we're trying to do below.
if (isBootclasspathInterface) {
// Synthesize a "bridge" method that calls the true implementation
Method bridged = findBridgedMethod(name, desc);
checkState(
bridged != null,
"TODO: Can't stub core interface bridge method %s.%s %s in %s",
stubbedInterfaceName,
name,
desc,
internalName);
int slot = 0;
stubMethod.visitVarInsn(Opcodes.ALOAD, slot++); // load the receiver
Type neededType = Type.getType(bridged);
Type[] neededArgTypes = neededType.getArgumentTypes();
Type[] parameterTypes = Type.getArgumentTypes(desc);
for (int i = 0; i < neededArgTypes.length; ++i) {
Type arg = neededArgTypes[i];
stubMethod.visitVarInsn(arg.getOpcode(Opcodes.ILOAD), slot);
if (!arg.equals(parameterTypes[i])) {
checkState(
arg.getSort() == Type.ARRAY || arg.getSort() == Type.OBJECT,
"Can't cast parameter %s from in bridge for %s.%s%s to %s",
i,
stubbedInterfaceName,
name,
desc,
arg.getClassName());
stubMethod.visitTypeInsn(Opcodes.CHECKCAST, arg.getInternalName());
}
slot += arg.getSize();
}
// Just call the bridged method directly on the visited class using invokevirtual
stubMethod.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
internalName,
name,
neededType.getDescriptor(),
/*isInterface=*/ false);
stubMethod.visitInsn(neededType.getReturnType().getOpcode(Opcodes.IRETURN));
stubMethod.visitMaxs(0, 0); // rely on class writer to compute these
stubMethod.visitEnd();
return null; // don't visit the visited interface's bridge method
} else {
// For bridges we just copy their bodies instead of going through the companion class.
// Meanwhile, we also need to desugar the copied method bodies, so that any calls to
// interface methods are correctly handled.
return new InterfaceDesugaring.InterfaceInvocationRewriter(
stubMethod,
stubbedInterfaceName,
bootclasspath,
targetLoader,
depsCollector,
internalName);
}
} else {
return null; // not a default or bridge method or the class already defines this method.
}
}
/**
* Returns a non-bridge interface method with given name that a method with the given descriptor
* can bridge to, if any such method can be found.
*/
@Nullable
private Method findBridgedMethod(String name, String desc) {
Type[] paramTypes = Type.getArgumentTypes(desc);
Type returnType = Type.getReturnType(desc);
Class<?> itf = loadFromInternal(stubbedInterfaceName);
checkArgument(itf.isInterface(), "Should be an interface: %s", stubbedInterfaceName);
// 1. Find the bridge method we're trying to implement
Method bridge = null;
for (Method m : itf.getDeclaredMethods()) {
if (m.isBridge()
&& m.getName().equals(name)
&& Arrays.equals(paramTypes, Type.getArgumentTypes(m))
&& returnType.equals(Type.getReturnType(m))) {
bridge = m;
break;
}
}
checkState(bridge != null, "Couldn't find bridge %s.%s %s", stubbedInterfaceName, name, desc);
checkState(bridge.getParameterCount() == paramTypes.length);
// 2. Try to find the method being bridged
Method result = null;
next_method:
for (Method m : itf.getDeclaredMethods()) {
if (m.isBridge() || Modifier.isStatic(m.getModifiers())) {
continue;
}
if (!m.getName().equals(name)) {
continue;
}
if (Arrays.equals(paramTypes, Type.getArgumentTypes(m))) {
// All argument types match, only return type will differ: this is the method we want
return m;
} else if (m.getParameterCount() == bridge.getParameterCount()) {
for (int i = 0; i < m.getParameterCount(); ++i) {
if (!bridge.getParameterTypes()[i].isAssignableFrom(m.getParameterTypes()[i])) {
continue next_method;
}
}
// All of m's parameter types are subtypes of the bridge's parameter types (or primitives)
if (result == null) {
result = m;
} else {
// Bail if we find multiple methods that could be bridged
return null;
}
}
}
return result;
}
}
/**
* Visitor for interfaces that recursively searches interfaces for default method declarations.
*/
private class DefaultMethodFinder extends ClassVisitor {
@SuppressWarnings("hiding")
private ImmutableList<String> interfaces;
private boolean found;
public DefaultMethodFinder() {
super(Opcodes.ASM7);
}
@Override
public void visit(
int version,
int access,
String name,
String signature,
String superName,
String[] interfaces) {
checkArgument(BitFlags.isSet(access, Opcodes.ACC_INTERFACE));
checkState(this.interfaces == null);
this.interfaces = ImmutableList.copyOf(interfaces);
}
public boolean foundDefaultMethods() {
return found;
}
@Override
public void visitEnd() {
if (!found) {
found = defaultMethodsDefined(this.interfaces);
}
}
@Override
public MethodVisitor visitMethod(
int access, String name, String desc, String signature, String[] exceptions) {
if (!found && shouldStubAsDefaultMethod(access, name, desc)) {
// Found a default method we're not ignoring (instanceMethods at this point contains methods
// the top-level visited class implements itself).
found = true;
}
return null; // we don't care about the actual code in these methods
}
}
private class InstanceMethodRecorder extends ClassVisitor {
private final boolean ignoreEmulatedMethods;
private String className;
public InstanceMethodRecorder(boolean ignoreEmulatedMethods) {
super(Opcodes.ASM7);
this.ignoreEmulatedMethods = ignoreEmulatedMethods;
}
@Override
public void visit(
int version,
int access,
String name,
String signature,
String superName,
String[] interfaces) {
checkArgument(!BitFlags.isInterface(access));
className = name; // updated every time we start visiting another superclass
super.visit(version, access, name, signature, superName, interfaces);
}
@Override
public MethodVisitor visitMethod(
int access, String name, String desc, String signature, String[] exceptions) {
if (ignoreEmulatedMethods
&& !BitFlags.isStatic(access) // short-circuit
&& coreLibrarySupport.getCoreInterfaceRewritingTarget(
Opcodes.INVOKEVIRTUAL, className, name, desc, /*itf=*/ false)
!= null) {
// *don't* record emulated core library method implementations in immediate subclasses of
// emulated core library clasess so that they can be stubbed (since the inherited
// implementation may be missing at runtime).
return null;
}
recordIfInstanceMethod(access, name, desc);
return null;
}
}
/**
* Detector to determine whether an interface needs to be initialized when it is loaded.
*
* <p>If the interface has a default method, and its <clinit> initializes any of its fields, then
* this interface needs to be initialized.
*/
private static class InterfaceInitializationNecessityDetector extends ClassVisitor {
private final String internalName;
private boolean hasFieldInitializedInClinit;
private boolean hasDefaultMethods;
public InterfaceInitializationNecessityDetector(String internalName) {
super(Opcodes.ASM7);
this.internalName = internalName;
}
public boolean needsToInitialize() {
return hasDefaultMethods && hasFieldInitializedInClinit;
}
@Override
public void visit(
int version,
int access,
String name,
String signature,
String superName,
String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
checkState(
internalName.equals(name),
"Inconsistent internal names: expected=%s, real=%s",
internalName,
name);
checkArgument(
BitFlags.isSet(access, Opcodes.ACC_INTERFACE),
"This class visitor is only used for interfaces.");
}
@Override
public MethodVisitor visitMethod(
int access, String name, String desc, String signature, String[] exceptions) {
if (!hasDefaultMethods) {
hasDefaultMethods = isNonBridgeDefaultMethod(access);
}
if ("<clinit>".equals(name)) {
return new MethodVisitor(Opcodes.ASM7) {
@Override
public void visitFieldInsn(int opcode, String owner, String name, String desc) {
if (opcode == Opcodes.PUTSTATIC && internalName.equals(owner)) {
hasFieldInitializedInClinit = true;
}
}
};
}
return null; // Do not care about the code.
}
}
/** Method visitor that changes super constructor calls to a new base class. */
private static class ConstructorFixer extends MethodVisitor {
private final String newSuperName;
/**
* This helps us find the very first constructor invocation we visit. We're looking for a
* sequence ALOAD_0 ; INVOKESPECIAL java.lang.Object() while avoiding "new Object()" which
* results in a NEW followed by the same INVOKESPECIAL for Object's constructor that we're
* looking for. Note that anonymous inner class constructors store captured variables into
* fields before calling super(), so we have to tolerate some unrelated logic, but since
* Object's constructor is parameterless we thankfully don't have to worry about handwritten
* code for constructor arguments.
*/
private boolean looking = true;
ConstructorFixer(MethodVisitor dest, String newSuperName) {
super(Opcodes.ASM7, dest);
this.newSuperName = newSuperName;
}
@Override
public void visitMethodInsn(
int opcode, String owner, String name, String descriptor, boolean isInterface) {
if (looking
&& opcode == Opcodes.INVOKESPECIAL
&& "<init>".equals(name)
&& "java/lang/Object".equals(owner)) {
// Call new base class's constructor instead of j.l.Object(). Note owner can be the
// visited class for this() constructor calls, and we don't need to touch those here.
owner = newSuperName;
}
// Since Object's constructor has no arguments it must be the very first INVOKE (because there
// can't be any preceding logic to set up constructor arguments), so stop looking regardless
// of whether we rewrote that INVOKE or not (but ignore inserted JaCoCo invocations).
if (looking && (opcode != Opcodes.INVOKESTATIC || !"$jacocoInit".equals(name))) {
looking = false;
}
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}
@Override
public void visitTypeInsn(int opcode, String type) {
if (opcode == Opcodes.NEW && "java/lang/Object".equals(type)) {
// NEW java/lang/Object is followed by the very INVOKESPECIAL we're looking for, but we
// don't want to rewrite it. Since there can't be a super() call after this, just stop.
looking = false;
}
super.visitTypeInsn(opcode, type);
}
}
/** Comparator for classes and interfaces that compares by whether subtyping relationship. */
enum SubtypeComparator implements Comparator<Class<?>> {
/** Orders subtypes before supertypes and breaks ties lexicographically. */
INSTANCE;
@Override
public int compare(Class<?> o1, Class<?> o2) {
if (o1 == o2) {
return 0;
}
// order subtypes before supertypes
if (o1.isAssignableFrom(o2)) { // o1 is supertype of o2
return 1; // we want o1 to come after o2
}
if (o2.isAssignableFrom(o1)) { // o2 is supertype of o1
return -1; // we want o2 to come after o1
}
// o1 and o2 aren't comparable so arbitrarily impose lexicographical ordering
return o1.getName().compareTo(o2.getName());
}
}
}