| // Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| package com.android.tools.r8.ir.desugar; |
| |
| import com.android.tools.r8.errors.CompilationError; |
| import com.android.tools.r8.graph.AppView; |
| import com.android.tools.r8.graph.DexClass; |
| import com.android.tools.r8.graph.DexEncodedMethod; |
| import com.android.tools.r8.graph.DexItemFactory; |
| import com.android.tools.r8.graph.DexMethod; |
| import com.android.tools.r8.graph.DexString; |
| import com.android.tools.r8.graph.DexType; |
| import com.android.tools.r8.graph.MethodAccessFlags; |
| import com.android.tools.r8.ir.code.Invoke; |
| import com.android.tools.r8.ir.synthetic.ForwardMethodSourceCode; |
| import com.android.tools.r8.ir.synthetic.SynthesizedCode; |
| import com.google.common.collect.Sets; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.IdentityHashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| // Default and static method interface desugaring processor for classes. |
| // Adds default interface methods into the class when needed. |
| final class ClassProcessor { |
| |
| private final AppView<?> appView; |
| private final DexItemFactory dexItemFactory; |
| private final InterfaceMethodRewriter rewriter; |
| // Set of already processed classes. |
| private final Set<DexClass> processedClasses = Sets.newIdentityHashSet(); |
| // Maps already created methods into default methods they were generated based on. |
| private final Map<DexEncodedMethod, DexEncodedMethod> createdMethods = new IdentityHashMap<>(); |
| |
| ClassProcessor(AppView<?> appView, InterfaceMethodRewriter rewriter) { |
| this.appView = appView; |
| this.dexItemFactory = appView.dexItemFactory(); |
| this.rewriter = rewriter; |
| } |
| |
| final Set<DexEncodedMethod> getForwardMethods() { |
| return createdMethods.keySet(); |
| } |
| |
| final void process(DexClass clazz) { |
| assert !clazz.isInterface(); |
| if (clazz.isNotProgramClass()) { |
| // We assume that library classes don't need to be processed, since they |
| // are provided by a runtime not supporting default interface methods. |
| // We also skip classpath classes, which results in sub-optimal behavior |
| // in case classpath superclass when processed adds a default method which |
| // could have been reused in this class otherwise. |
| return; |
| } |
| if (!processedClasses.add(clazz)) { |
| return; // Has already been processed. |
| } |
| |
| // Ensure superclasses are processed first. We need it since we use information |
| // about methods added to superclasses when we decide if we want to add a default |
| // method to class `clazz`. |
| DexType superType = clazz.superType; |
| // If superClass definition is missing, just skip this part and let real processing of its |
| // subclasses report the error if it is required. |
| DexClass superClass = superType == null ? null : appView.definitionFor(superType); |
| if (superClass != null && superType != dexItemFactory.objectType) { |
| if (superClass.isInterface()) { |
| throw new CompilationError("Interface `" + superClass.toSourceString() |
| + "` used as super class of `" + clazz.toSourceString() + "`."); |
| } |
| process(superClass); |
| } |
| |
| // When inheriting from a library class, the library class may implement interfaces to |
| // desugar. We therefore need to look the interfaces of the library classes. |
| boolean desugaredLibraryLookup = |
| superClass != null |
| && superClass.isLibraryClass() |
| && appView.options().desugaredLibraryConfiguration.getEmulateLibraryInterface().size() |
| > 0; |
| |
| if (clazz.interfaces.isEmpty() && !desugaredLibraryLookup) { |
| // Since superclass has already been processed and it has all missing methods |
| // added, these methods will be inherited by `clazz`, and only need to be revised |
| // in case this class has *additional* interfaces implemented, which may change |
| // the entire picture of the default method selection in runtime. |
| return; |
| } |
| |
| // Collect the default interface methods to be added to this class. |
| List<DexEncodedMethod> methodsToImplement = |
| collectMethodsToImplement(clazz, desugaredLibraryLookup); |
| if (methodsToImplement.isEmpty()) { |
| return; |
| } |
| |
| // Add the methods. |
| List<DexEncodedMethod> newForwardingMethods = new ArrayList<>(methodsToImplement.size()); |
| for (DexEncodedMethod method : methodsToImplement) { |
| assert method.accessFlags.isPublic() && !method.accessFlags.isAbstract(); |
| DexEncodedMethod newMethod = addForwardingMethod(method, clazz); |
| newForwardingMethods.add(newMethod); |
| createdMethods.put(newMethod, method); |
| } |
| clazz.appendVirtualMethods(newForwardingMethods); |
| } |
| |
| private DexEncodedMethod addForwardingMethod(DexEncodedMethod defaultMethod, DexClass clazz) { |
| DexMethod method = defaultMethod.method; |
| DexClass target = appView.definitionFor(method.holder); |
| // NOTE: Never add a forwarding method to methods of classes unknown or coming from android.jar |
| // even if this results in invalid code, these classes are never desugared. |
| assert target != null; |
| // In desugared library, emulated interface methods can be overridden by retarget lib members. |
| DexMethod forwardMethod = |
| target.isInterface() |
| ? rewriter.defaultAsMethodOfCompanionClass(method) |
| : retargetMethod(method); |
| // New method will have the same name, proto, and also all the flags of the |
| // default method, including bridge flag. |
| DexMethod newMethod = dexItemFactory.createMethod(clazz.type, method.proto, method.name); |
| MethodAccessFlags newFlags = defaultMethod.accessFlags.copy(); |
| // Some debuggers (like IntelliJ) automatically skip synthetic methods on single step. |
| newFlags.setSynthetic(); |
| ForwardMethodSourceCode.Builder forwardSourceCodeBuilder = |
| ForwardMethodSourceCode.builder(newMethod); |
| forwardSourceCodeBuilder |
| .setReceiver(clazz.type) |
| .setTarget(forwardMethod) |
| .setInvokeType(Invoke.Type.STATIC) |
| .setIsInterface(false); // Holder is companion class, not an interface. |
| return new DexEncodedMethod( |
| newMethod, |
| newFlags, |
| defaultMethod.annotations, |
| defaultMethod.parameterAnnotationsList, |
| new SynthesizedCode(forwardSourceCodeBuilder::build)); |
| } |
| |
| private DexMethod retargetMethod(DexMethod method) { |
| Map<DexString, Map<DexType, DexType>> retargetCoreLibMember = |
| appView.options().desugaredLibraryConfiguration.getRetargetCoreLibMember(); |
| Map<DexType, DexType> typeMap = retargetCoreLibMember.get(method.name); |
| assert typeMap != null; |
| assert typeMap.get(method.holder) != null; |
| return dexItemFactory.createMethod( |
| typeMap.get(method.holder), |
| dexItemFactory.prependTypeToProto(method.holder, method.proto), |
| method.name); |
| } |
| |
| // For a given class `clazz` inspects all interfaces it implements directly or |
| // indirectly and collect a set of all default methods to be implemented |
| // in this class. |
| private List<DexEncodedMethod> collectMethodsToImplement( |
| DexClass clazz, boolean desugaredLibraryLookup) { |
| DefaultMethodsHelper helper = new DefaultMethodsHelper(); |
| DexClass current = clazz; |
| List<DexEncodedMethod> accumulatedVirtualMethods = new ArrayList<>(); |
| // Collect candidate default methods by inspecting interfaces implemented |
| // by this class as well as its superclasses. |
| // |
| // We assume here that interfaces implemented by java.lang.Object don't |
| // have default methods to desugar since they are library interfaces. And we assume object |
| // methods don't hide any default interface methods. Default interface method matching Object's |
| // methods is supposed to fail with a compilation error. |
| // Note that this last assumption will be broken if Object API is augmented with a new method in |
| // the future. |
| while (current.type != dexItemFactory.objectType) { |
| for (DexType type : current.interfaces.values) { |
| helper.merge(rewriter.getOrCreateInterfaceInfo(clazz, current, type)); |
| } |
| |
| // TODO(anyone): Using clazz here instead of current looks suspicious, should this be hoisted |
| // out of the loop or changed to current? |
| accumulatedVirtualMethods.addAll(clazz.virtualMethods()); |
| |
| List<DexEncodedMethod> defaultMethodsInDirectInterface = helper.createFullList(); |
| |
| List<DexEncodedMethod> toBeImplementedFromDirectInterface = |
| new ArrayList<>(defaultMethodsInDirectInterface.size()); |
| hideCandidates(accumulatedVirtualMethods, |
| defaultMethodsInDirectInterface, |
| toBeImplementedFromDirectInterface); |
| // toBeImplementedFromDirectInterface are those that we know for sure we need to implement by |
| // looking at the already desugared super classes. |
| // Remaining methods in defaultMethodsInDirectInterface are those methods we need to look at |
| // the hierarchy to know how they should be handled. |
| if (toBeImplementedFromDirectInterface.isEmpty() |
| && defaultMethodsInDirectInterface.isEmpty() |
| && !desugaredLibraryLookup) { |
| // No interface with default in direct hierarchy, nothing to do: super already has all that |
| // is needed. |
| return Collections.emptyList(); |
| } |
| |
| if (current.superType == null) { |
| // TODO(anyone): Can this ever happen? It seems the loop stops on Object. |
| break; |
| } else { |
| DexClass superClass = appView.definitionFor(current.superType); |
| if (superClass != null) { |
| // TODO(b/138988172): Can we avoid traversing the full hierarchy for each type? |
| InterfaceMethodRewriter.reportDependencyEdge(superClass, current, appView); |
| current = superClass; |
| } else { |
| String message = "Default method desugaring of `" + clazz.toSourceString() + "` failed"; |
| if (current == clazz) { |
| message += " because its super class `" + |
| clazz.superType.toSourceString() + "` is missing"; |
| } else { |
| message += |
| " because it's hierarchy is incomplete. The class `" |
| + current.superType.toSourceString() |
| + "` is missing and it is the declared super class of `" |
| + current.toSourceString() + "`"; |
| } |
| throw new CompilationError(message); |
| } |
| } |
| } |
| |
| List<DexEncodedMethod> candidates = helper.createCandidatesList(); |
| if (candidates.isEmpty()) { |
| return candidates; |
| } |
| |
| // Remove from candidates methods defined in class or any of its superclasses. |
| List<DexEncodedMethod> toBeImplemented = new ArrayList<>(candidates.size()); |
| current = clazz; |
| Map<DexString, Map<DexType, DexType>> retargetCoreLibMember = |
| appView.options().desugaredLibraryConfiguration.getRetargetCoreLibMember(); |
| while (true) { |
| // In desugared library look-up, methods from library classes cannot hide methods from |
| // emulated interfaces (the method being desugared implied the implementation is not |
| // present in the library class), except through retarget core lib member. |
| if (desugaredLibraryLookup && current.isLibraryClass()) { |
| Iterator<DexEncodedMethod> iterator = candidates.iterator(); |
| while (iterator.hasNext()) { |
| DexEncodedMethod candidate = iterator.next(); |
| if (rewriter.isEmulatedInterface(candidate.method.holder) |
| && current.lookupVirtualMethod(candidate.method) != null) { |
| // A library class overrides an emulated interface method. This override is valid |
| // only if it goes through retarget core lib member, else it needs to be implemented. |
| Map<DexType, DexType> typeMap = retargetCoreLibMember.get(candidate.method.name); |
| if (typeMap != null && typeMap.containsKey(current.type)) { |
| // A rewrite needs to be performed, but instead of rewriting to the companion class, |
| // D8/R8 needs to rewrite to the retarget member. |
| toBeImplemented.add(current.lookupVirtualMethod(candidate.method)); |
| } else { |
| toBeImplemented.add(candidate); |
| iterator.remove(); |
| } |
| } |
| } |
| } |
| // Hide candidates by virtual method of the class. |
| hideCandidates(current.virtualMethods(), candidates, toBeImplemented); |
| if (candidates.isEmpty()) { |
| return toBeImplemented; |
| } |
| |
| DexType superType = current.superType; |
| DexClass superClass = null; |
| if (superType != null) { |
| superClass = appView.definitionFor(superType); |
| // It's available or we would have failed while analyzing the hierarchy for interfaces. |
| assert superClass != null; |
| } |
| if (superClass == null || superType == dexItemFactory.objectType) { |
| // Note that default interface methods must never have same |
| // name/signature as any method in java.lang.Object (JLS ยง9.4.1.2). |
| |
| // Everything still in candidate list is not hidden. |
| toBeImplemented.addAll(candidates); |
| |
| return toBeImplemented; |
| } |
| current = superClass; |
| } |
| } |
| |
| private void hideCandidates(List<DexEncodedMethod> virtualMethods, |
| List<DexEncodedMethod> candidates, List<DexEncodedMethod> toBeImplemented) { |
| Iterator<DexEncodedMethod> it = candidates.iterator(); |
| while (it.hasNext()) { |
| DexEncodedMethod candidate = it.next(); |
| for (DexEncodedMethod encoded : virtualMethods) { |
| if (candidate.method.match(encoded)) { |
| // Found a methods hiding the candidate. |
| DexEncodedMethod basedOnCandidate = createdMethods.get(encoded); |
| if (basedOnCandidate != null) { |
| // The method we found is a method we have generated for a default interface |
| // method in a superclass. If the method is based on the same candidate we don't |
| // need to re-generate this method again since it is going to be inherited. |
| if (basedOnCandidate != candidate) { |
| // Need to re-generate since the inherited version is |
| // based on a different candidate. |
| toBeImplemented.add(candidate); |
| } |
| } |
| |
| // Done with this candidate. |
| it.remove(); |
| break; |
| } |
| } |
| } |
| } |
| } |