summaryrefslogtreecommitdiff
path: root/java/com/google/devtools/build/android/desugar
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/google/devtools/build/android/desugar')
-rw-r--r--java/com/google/devtools/build/android/desugar/BytecodeTypeInference.java1
-rw-r--r--java/com/google/devtools/build/android/desugar/ClassReaderFactory.java3
-rw-r--r--java/com/google/devtools/build/android/desugar/ClassVsInterface.java1
-rw-r--r--java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java96
-rw-r--r--java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java521
-rw-r--r--java/com/google/devtools/build/android/desugar/CorePackageRenamer.java45
-rw-r--r--java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java272
-rw-r--r--java/com/google/devtools/build/android/desugar/Desugar.java265
-rw-r--r--java/com/google/devtools/build/android/desugar/EmulatedInterfaceRewriter.java92
-rw-r--r--java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java54
-rw-r--r--java/com/google/devtools/build/android/desugar/Java7Compatibility.java1
-rw-r--r--java/com/google/devtools/build/android/desugar/LambdaClassFixer.java1
-rw-r--r--java/com/google/devtools/build/android/desugar/LambdaClassMaker.java12
-rw-r--r--java/com/google/devtools/build/android/desugar/LambdaDesugaring.java1
-rw-r--r--java/com/google/devtools/build/android/desugar/LongCompareMethodRewriter.java20
-rw-r--r--java/com/google/devtools/build/android/desugar/ObjectsRequireNonNullMethodRewriter.java31
-rw-r--r--java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java59
-rw-r--r--java/com/google/devtools/build/android/desugar/io/BitFlags.java (renamed from java/com/google/devtools/build/android/desugar/BitFlags.java)4
-rw-r--r--java/com/google/devtools/build/android/desugar/io/CoreLibraryRewriter.java (renamed from java/com/google/devtools/build/android/desugar/CoreLibraryRewriter.java)33
-rw-r--r--java/com/google/devtools/build/android/desugar/io/DirectoryInputFileProvider.java (renamed from java/com/google/devtools/build/android/desugar/DirectoryInputFileProvider.java)2
-rw-r--r--java/com/google/devtools/build/android/desugar/io/DirectoryOutputFileProvider.java (renamed from java/com/google/devtools/build/android/desugar/DirectoryOutputFileProvider.java)4
-rw-r--r--java/com/google/devtools/build/android/desugar/io/FieldInfo.java (renamed from java/com/google/devtools/build/android/desugar/FieldInfo.java)4
-rw-r--r--java/com/google/devtools/build/android/desugar/io/HeaderClassLoader.java (renamed from java/com/google/devtools/build/android/desugar/HeaderClassLoader.java)7
-rw-r--r--java/com/google/devtools/build/android/desugar/io/IndexedInputs.java (renamed from java/com/google/devtools/build/android/desugar/IndexedInputs.java)4
-rw-r--r--java/com/google/devtools/build/android/desugar/io/InputFileProvider.java (renamed from java/com/google/devtools/build/android/desugar/InputFileProvider.java)17
-rw-r--r--java/com/google/devtools/build/android/desugar/io/OutputFileProvider.java (renamed from java/com/google/devtools/build/android/desugar/OutputFileProvider.java)17
-rw-r--r--java/com/google/devtools/build/android/desugar/io/ThrowingClassLoader.java27
-rw-r--r--java/com/google/devtools/build/android/desugar/io/ZipInputFileProvider.java (renamed from java/com/google/devtools/build/android/desugar/ZipInputFileProvider.java)2
-rw-r--r--java/com/google/devtools/build/android/desugar/io/ZipOutputFileProvider.java (renamed from java/com/google/devtools/build/android/desugar/ZipOutputFileProvider.java)4
-rw-r--r--java/com/google/devtools/build/android/desugar/scan/KeepReference.java51
-rw-r--r--java/com/google/devtools/build/android/desugar/scan/KeepScanner.java303
-rw-r--r--java/com/google/devtools/build/android/desugar/scan/PrefixReferenceScanner.java405
32 files changed, 2153 insertions, 206 deletions
diff --git a/java/com/google/devtools/build/android/desugar/BytecodeTypeInference.java b/java/com/google/devtools/build/android/desugar/BytecodeTypeInference.java
index 783069f..ce36071 100644
--- a/java/com/google/devtools/build/android/desugar/BytecodeTypeInference.java
+++ b/java/com/google/devtools/build/android/desugar/BytecodeTypeInference.java
@@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkState;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.android.desugar.io.BitFlags;
import java.util.ArrayList;
import java.util.Optional;
import javax.annotation.Nullable;
diff --git a/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java b/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java
index bae5251..aff9bab 100644
--- a/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java
+++ b/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java
@@ -13,6 +13,9 @@
// limitations under the License.
package com.google.devtools.build.android.desugar;
+import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter;
+import com.google.devtools.build.android.desugar.io.IndexedInputs;
+import com.google.devtools.build.android.desugar.io.InputFileProvider;
import java.io.IOException;
import java.io.InputStream;
import javax.annotation.Nullable;
diff --git a/java/com/google/devtools/build/android/desugar/ClassVsInterface.java b/java/com/google/devtools/build/android/desugar/ClassVsInterface.java
index cb62deb..2724454 100644
--- a/java/com/google/devtools/build/android/desugar/ClassVsInterface.java
+++ b/java/com/google/devtools/build/android/desugar/ClassVsInterface.java
@@ -16,6 +16,7 @@ package com.google.devtools.build.android.desugar;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
+import com.google.devtools.build.android.desugar.io.BitFlags;
import java.util.HashMap;
import javax.annotation.Nullable;
import org.objectweb.asm.ClassReader;
diff --git a/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java b/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java
new file mode 100644
index 0000000..381a344
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java
@@ -0,0 +1,96 @@
+// Copyright 2018 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 org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+/**
+ * Rewriter of default and static interface methods defined in some core libraries.
+ *
+ * <p>This is conceptually similar to call site rewriting in {@link InterfaceDesugaring} but here
+ * we're doing it for certain bootclasspath methods and in particular for invokeinterface and
+ * invokevirtual, which are ignored in regular {@link InterfaceDesugaring}.
+ */
+public class CoreLibraryInvocationRewriter extends ClassVisitor {
+
+ private final CoreLibrarySupport support;
+
+ public CoreLibraryInvocationRewriter(ClassVisitor cv, CoreLibrarySupport support) {
+ super(Opcodes.ASM6, cv);
+ this.support = support;
+ }
+
+ @Override
+ public MethodVisitor visitMethod(
+ int access, String name, String desc, String signature, String[] exceptions) {
+ MethodVisitor result = super.visitMethod(access, name, desc, signature, exceptions);
+ return result != null ? new CoreLibraryMethodInvocationRewriter(result) : null;
+ }
+
+ private class CoreLibraryMethodInvocationRewriter extends MethodVisitor {
+ public CoreLibraryMethodInvocationRewriter(MethodVisitor mv) {
+ super(Opcodes.ASM6, mv);
+ }
+
+ @Override
+ public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
+ Class<?> coreInterface =
+ support.getCoreInterfaceRewritingTarget(opcode, owner, name, desc, itf);
+
+ if (coreInterface != null) {
+ String coreInterfaceName = coreInterface.getName().replace('.', '/');
+ name =
+ InterfaceDesugaring.normalizeInterfaceMethodName(
+ name, name.startsWith("lambda$"), opcode == Opcodes.INVOKESTATIC);
+ if (opcode == Opcodes.INVOKESTATIC) {
+ checkState(owner.equals(coreInterfaceName));
+ } else {
+ desc = InterfaceDesugaring.companionDefaultMethodDescriptor(coreInterfaceName, desc);
+ }
+
+ if (opcode == Opcodes.INVOKESTATIC || opcode == Opcodes.INVOKESPECIAL) {
+ checkArgument(itf || opcode == Opcodes.INVOKESPECIAL,
+ "Expected interface to rewrite %s.%s : %s", owner, name, desc);
+ owner = coreInterface.isInterface()
+ ? InterfaceDesugaring.getCompanionClassName(coreInterfaceName)
+ : checkNotNull(support.getMoveTarget(coreInterfaceName, name));
+ } else {
+ checkState(coreInterface.isInterface());
+ owner = coreInterfaceName + "$$Dispatch";
+ }
+
+ opcode = Opcodes.INVOKESTATIC;
+ itf = false;
+ } else {
+ String newOwner = support.getMoveTarget(owner, name);
+ if (newOwner != null) {
+ if (opcode != Opcodes.INVOKESTATIC) {
+ // assuming a static method
+ desc = InterfaceDesugaring.companionDefaultMethodDescriptor(owner, desc);
+ opcode = Opcodes.INVOKESTATIC;
+ }
+ owner = newOwner;
+ itf = false; // assuming a class
+ }
+ }
+ super.visitMethodInsn(opcode, owner, name, desc, itf);
+ }
+ }
+}
diff --git a/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java
new file mode 100644
index 0000000..f247074
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java
@@ -0,0 +1,521 @@
+// Copyright 2018 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 static java.util.stream.Stream.concat;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.LinkedHashMultimap;
+import com.google.common.collect.Multimap;
+import com.google.devtools.build.android.desugar.io.BitFlags;
+import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter;
+import com.google.errorprone.annotations.Immutable;
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import javax.annotation.Nullable;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+/**
+ * Helper that keeps track of which core library classes and methods we want to rewrite.
+ */
+class CoreLibrarySupport {
+
+ private static final Object[] EMPTY_FRAME = new Object[0];
+ private static final String[] EMPTY_LIST = new String[0];
+
+ private final CoreLibraryRewriter rewriter;
+ private final ClassLoader targetLoader;
+ /** Internal name prefixes that we want to move to a custom package. */
+ private final ImmutableSet<String> renamedPrefixes;
+ private final ImmutableSet<String> excludeFromEmulation;
+ /** Internal names of interfaces whose default and static interface methods we'll emulate. */
+ private final ImmutableSet<Class<?>> emulatedInterfaces;
+ /** Map from {@code owner#name} core library members to their new owners. */
+ private final ImmutableMap<String, String> memberMoves;
+
+ /** For the collection of definitions of emulated default methods (deterministic iteration). */
+ private final Multimap<String, EmulatedMethod> emulatedDefaultMethods =
+ LinkedHashMultimap.create();
+
+ public CoreLibrarySupport(
+ CoreLibraryRewriter rewriter,
+ ClassLoader targetLoader,
+ List<String> renamedPrefixes,
+ List<String> emulatedInterfaces,
+ List<String> memberMoves,
+ List<String> excludeFromEmulation) {
+ this.rewriter = rewriter;
+ this.targetLoader = targetLoader;
+ checkArgument(
+ renamedPrefixes.stream().allMatch(prefix -> prefix.startsWith("java/")), renamedPrefixes);
+ this.renamedPrefixes = ImmutableSet.copyOf(renamedPrefixes);
+ this.excludeFromEmulation = ImmutableSet.copyOf(excludeFromEmulation);
+
+ ImmutableSet.Builder<Class<?>> classBuilder = ImmutableSet.builder();
+ for (String itf : emulatedInterfaces) {
+ checkArgument(itf.startsWith("java/util/"), itf);
+ Class<?> clazz = loadFromInternal(rewriter.getPrefix() + itf);
+ checkArgument(clazz.isInterface(), itf);
+ classBuilder.add(clazz);
+ }
+ this.emulatedInterfaces = classBuilder.build();
+
+ // We can call isRenamed and rename below b/c we initialized the necessary fields above
+ // Use LinkedHashMap to tolerate identical duplicates
+ LinkedHashMap<String, String> movesBuilder = new LinkedHashMap<>();
+ Splitter splitter = Splitter.on("->").trimResults().omitEmptyStrings();
+ for (String move : memberMoves) {
+ List<String> pair = splitter.splitToList(move);
+ checkArgument(pair.size() == 2, "Doesn't split as expected: %s", move);
+ checkArgument(pair.get(0).startsWith("java/"), "Unexpected member: %s", move);
+ int sep = pair.get(0).indexOf('#');
+ checkArgument(sep > 0 && sep == pair.get(0).lastIndexOf('#'), "invalid member: %s", move);
+ checkArgument(!isRenamedCoreLibrary(pair.get(0).substring(0, sep)),
+ "Original renamed, no need to move it: %s", move);
+ checkArgument(isRenamedCoreLibrary(pair.get(1)), "Target not renamed: %s", move);
+ checkArgument(!this.excludeFromEmulation.contains(pair.get(0)),
+ "Retargeted invocation %s shouldn't overlap with excluded", move);
+
+ String value = renameCoreLibrary(pair.get(1));
+ String existing = movesBuilder.put(pair.get(0), value);
+ checkArgument(existing == null || existing.equals(value),
+ "Two move destinations %s and %s configured for %s", existing, value, pair.get(0));
+ }
+ this.memberMoves = ImmutableMap.copyOf(movesBuilder);
+ }
+
+ public boolean isRenamedCoreLibrary(String internalName) {
+ String unprefixedName = rewriter.unprefix(internalName);
+ if (!unprefixedName.startsWith("java/") || renamedPrefixes.isEmpty()) {
+ return false; // shortcut
+ }
+ // Rename any classes desugar might generate under java/ (for emulated interfaces) as well as
+ // configured prefixes
+ return looksGenerated(unprefixedName)
+ || renamedPrefixes.stream().anyMatch(prefix -> unprefixedName.startsWith(prefix));
+ }
+
+ public String renameCoreLibrary(String internalName) {
+ internalName = rewriter.unprefix(internalName);
+ return (internalName.startsWith("java/"))
+ ? "j$/" + internalName.substring(/* cut away "java/" prefix */ 5)
+ : internalName;
+ }
+
+ @Nullable
+ public String getMoveTarget(String owner, String name) {
+ return memberMoves.get(rewriter.unprefix(owner) + '#' + name);
+ }
+
+ /**
+ * Returns {@code true} for java.* classes or interfaces that are subtypes of emulated interfaces.
+ * Note that implies that this method always returns {@code false} for user-written classes.
+ */
+ public boolean isEmulatedCoreClassOrInterface(String internalName) {
+ return getEmulatedCoreClassOrInterface(internalName) != null;
+ }
+
+ /** Includes the given method definition in any applicable core interface emulation logic. */
+ public void registerIfEmulatedCoreInterface(
+ int access,
+ String owner,
+ String name,
+ String desc,
+ String[] exceptions) {
+ Class<?> emulated = getEmulatedCoreClassOrInterface(owner);
+ if (emulated == null) {
+ return;
+ }
+ checkArgument(emulated.isInterface(), "Shouldn't be called for a class: %s.%s", owner, name);
+ checkArgument(
+ BitFlags.noneSet(
+ access,
+ Opcodes.ACC_ABSTRACT | Opcodes.ACC_NATIVE | Opcodes.ACC_STATIC | Opcodes.ACC_BRIDGE),
+ "Should only be called for default methods: %s.%s", owner, name);
+ emulatedDefaultMethods.put(
+ name + ":" + desc, EmulatedMethod.create(access, emulated, name, desc, exceptions));
+ }
+
+ /**
+ * If the given invocation needs to go through a companion class of an emulated or renamed
+ * core interface, this methods returns that interface. This is a helper method for
+ * {@link CoreLibraryInvocationRewriter}.
+ *
+ * <p>This method can only return non-{@code null} if {@code owner} is a core library type.
+ * It usually returns an emulated interface, unless the given invocation is a super-call to a
+ * core class's implementation of an emulated method that's being moved (other implementations
+ * of emulated methods in core classes are ignored). In that case the class is returned and the
+ * caller can use {@link #getMoveTarget} to find out where to redirect the invokespecial to.
+ */
+ // TODO(kmb): Rethink this API and consider combining it with getMoveTarget().
+ @Nullable
+ public Class<?> getCoreInterfaceRewritingTarget(
+ int opcode, String owner, String name, String desc, boolean itf) {
+ if (looksGenerated(owner)) {
+ // Regular desugaring handles generated classes, no emulation is needed
+ return null;
+ }
+ if (!itf && opcode == Opcodes.INVOKESTATIC) {
+ // Ignore static invocations on classes--they never need rewriting (unless moved but that's
+ // handled separately).
+ return null;
+ }
+ if ("<init>".equals(name)) {
+ return null; // Constructors aren't rewritten
+ }
+
+ Class<?> clazz;
+ if (isRenamedCoreLibrary(owner)) {
+ // For renamed invocation targets we just need to do what InterfaceDesugaring does, that is,
+ // only worry about invokestatic and invokespecial interface invocations; nothing to do for
+ // classes and invokeinterface. InterfaceDesugaring ignores bootclasspath interfaces,
+ // so we have to do its work here for renamed interfaces.
+ if (itf
+ && (opcode == Opcodes.INVOKESTATIC || opcode == Opcodes.INVOKESPECIAL)) {
+ clazz = loadFromInternal(owner);
+ } else {
+ return null;
+ }
+ } else {
+ // If not renamed, see if the owner needs emulation.
+ clazz = getEmulatedCoreClassOrInterface(owner);
+ if (clazz == null) {
+ return null;
+ }
+ }
+ checkArgument(itf == clazz.isInterface(), "%s expected to be interface: %s", owner, itf);
+
+ if (opcode == Opcodes.INVOKESTATIC) {
+ // Static interface invocation always goes to the given owner
+ checkState(itf); // we should've bailed out above.
+ return clazz;
+ }
+
+ // See if the invoked method is a default method, which will need rewriting. For invokespecial
+ // we can only get here if its a default method, and invokestatic we handled above.
+ Method callee = findInterfaceMethod(clazz, name, desc);
+ if (callee != null && callee.isDefault()) {
+ if (isExcluded(callee)) {
+ return null;
+ }
+
+ if (!itf && opcode == Opcodes.INVOKESPECIAL) {
+ // See if the invoked implementation is moved; note we ignore all other overrides in classes
+ Class<?> impl = clazz; // we know clazz is not an interface because !itf
+ while (impl != null) {
+ String implName = impl.getName().replace('.', '/');
+ if (getMoveTarget(implName, name) != null) {
+ return impl;
+ }
+ impl = impl.getSuperclass();
+ }
+ }
+
+ Class<?> result = callee.getDeclaringClass();
+ if (isRenamedCoreLibrary(result.getName().replace('.', '/'))
+ || emulatedInterfaces.stream().anyMatch(emulated -> emulated.isAssignableFrom(result))) {
+ return result;
+ }
+ // We get here if the declaring class is a supertype of an emulated interface. In that case
+ // use the emulated interface instead (since we don't desugar the supertype). Fail in case
+ // there are multiple possibilities.
+ Iterator<Class<?>> roots =
+ emulatedInterfaces
+ .stream()
+ .filter(
+ emulated -> emulated.isAssignableFrom(clazz) && result.isAssignableFrom(emulated))
+ .iterator();
+ checkState(roots.hasNext()); // must exist
+ Class<?> substitute = roots.next();
+ checkState(!roots.hasNext(), "Ambiguous emulation substitute: %s", callee);
+ return substitute;
+ } else {
+ checkArgument(!itf || opcode != Opcodes.INVOKESPECIAL,
+ "Couldn't resolve interface super call %s.super.%s : %s", owner, name, desc);
+ }
+ return null;
+ }
+
+ /**
+ * Returns the given class if it's a core library class or interface with emulated default
+ * methods. This is equivalent to calling {@link #isEmulatedCoreClassOrInterface} and then
+ * just loading the class (using the target class loader).
+ */
+ public Class<?> getEmulatedCoreClassOrInterface(String internalName) {
+ if (looksGenerated(internalName)) {
+ // Regular desugaring handles generated classes, no emulation is needed
+ return null;
+ }
+ {
+ String unprefixedOwner = rewriter.unprefix(internalName);
+ if (!unprefixedOwner.startsWith("java/util/") || isRenamedCoreLibrary(unprefixedOwner)) {
+ return null;
+ }
+ }
+
+ Class<?> clazz = loadFromInternal(internalName);
+ if (emulatedInterfaces.stream().anyMatch(itf -> itf.isAssignableFrom(clazz))) {
+ return clazz;
+ }
+ return null;
+ }
+
+ public void makeDispatchHelpers(GeneratedClassStore store) {
+ HashMap<Class<?>, ClassVisitor> dispatchHelpers = new HashMap<>();
+ for (Collection<EmulatedMethod> group : emulatedDefaultMethods.asMap().values()) {
+ checkState(!group.isEmpty());
+ Class<?> root = group
+ .stream()
+ .map(EmulatedMethod::owner)
+ .max(DefaultMethodClassFixer.SubtypeComparator.INSTANCE)
+ .get();
+ checkState(group.stream().map(m -> m.owner()).allMatch(o -> root.isAssignableFrom(o)),
+ "Not a single unique method: %s", group);
+ String methodName = group.stream().findAny().get().name();
+
+ ImmutableList<Class<?>> customOverrides = findCustomOverrides(root, methodName);
+
+ for (EmulatedMethod methodDefinition : group) {
+ Class<?> owner = methodDefinition.owner();
+ ClassVisitor dispatchHelper = dispatchHelpers.computeIfAbsent(owner, clazz -> {
+ String className = clazz.getName().replace('.', '/') + "$$Dispatch";
+ ClassVisitor result = store.add(className);
+ result.visit(
+ Opcodes.V1_7,
+ // Must be public so dispatch methods can be called from anywhere
+ Opcodes.ACC_SYNTHETIC | Opcodes.ACC_PUBLIC,
+ className,
+ /*signature=*/ null,
+ "java/lang/Object",
+ EMPTY_LIST);
+ return result;
+ });
+
+ // Types to check for before calling methodDefinition's companion, sub- before super-types
+ ImmutableList<Class<?>> typechecks =
+ concat(group.stream().map(EmulatedMethod::owner), customOverrides.stream())
+ .filter(o -> o != owner && owner.isAssignableFrom(o))
+ .distinct() // should already be but just in case
+ .sorted(DefaultMethodClassFixer.SubtypeComparator.INSTANCE)
+ .collect(ImmutableList.toImmutableList());
+ makeDispatchHelperMethod(dispatchHelper, methodDefinition, typechecks);
+ }
+ }
+ }
+
+ private ImmutableList<Class<?>> findCustomOverrides(Class<?> root, String methodName) {
+ ImmutableList.Builder<Class<?>> customOverrides = ImmutableList.builder();
+ for (ImmutableMap.Entry<String, String> move : memberMoves.entrySet()) {
+ // move.getKey is a string <owner>#<name> which we validated in the constructor.
+ // We need to take the string apart here to compare owner and name separately.
+ if (!methodName.equals(move.getKey().substring(move.getKey().indexOf('#') + 1))) {
+ continue;
+ }
+ Class<?> target =
+ loadFromInternal(
+ rewriter.getPrefix() + move.getKey().substring(0, move.getKey().indexOf('#')));
+ if (!root.isAssignableFrom(target)) {
+ continue;
+ }
+ checkState(!target.isInterface(), "can't move emulated interface method: %s", move);
+ customOverrides.add(target);
+ }
+ return customOverrides.build();
+ }
+
+ private void makeDispatchHelperMethod(
+ ClassVisitor helper, EmulatedMethod method, ImmutableList<Class<?>> typechecks) {
+ checkArgument(method.owner().isInterface());
+ String owner = method.owner().getName().replace('.', '/');
+ Type methodType = Type.getMethodType(method.descriptor());
+ String companionDesc =
+ InterfaceDesugaring.companionDefaultMethodDescriptor(owner, method.descriptor());
+ MethodVisitor dispatchMethod =
+ helper.visitMethod(
+ method.access() | Opcodes.ACC_STATIC,
+ method.name(),
+ companionDesc,
+ /*signature=*/ null, // signature is invalid due to extra "receiver" argument
+ method.exceptions().toArray(EMPTY_LIST));
+
+
+ dispatchMethod.visitCode();
+ {
+ // See if the receiver might come with its own implementation of the method, and call it.
+ // We do this by testing for the interface type created by EmulatedInterfaceRewriter
+ Label fallthrough = new Label();
+ String emulationInterface = renameCoreLibrary(owner);
+ dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0); // load "receiver"
+ dispatchMethod.visitTypeInsn(Opcodes.INSTANCEOF, emulationInterface);
+ dispatchMethod.visitJumpInsn(Opcodes.IFEQ, fallthrough);
+ dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0); // load "receiver"
+ dispatchMethod.visitTypeInsn(Opcodes.CHECKCAST, emulationInterface);
+
+ visitLoadArgs(dispatchMethod, methodType, 1 /* receiver already loaded above */);
+ dispatchMethod.visitMethodInsn(
+ Opcodes.INVOKEINTERFACE,
+ emulationInterface,
+ method.name(),
+ method.descriptor(),
+ /*itf=*/ true);
+ dispatchMethod.visitInsn(methodType.getReturnType().getOpcode(Opcodes.IRETURN));
+
+ dispatchMethod.visitLabel(fallthrough);
+ // Trivial frame for the branch target: same empty stack as before
+ dispatchMethod.visitFrame(Opcodes.F_SAME, 0, EMPTY_FRAME, 0, EMPTY_FRAME);
+ }
+
+ // Next, check for subtypes with specialized implementations and call them
+ for (Class<?> tested : typechecks) {
+ Label fallthrough = new Label();
+ String testedName = tested.getName().replace('.', '/');
+ // In case of a class this must be a member move; for interfaces use the companion.
+ String target =
+ tested.isInterface()
+ ? InterfaceDesugaring.getCompanionClassName(testedName)
+ : checkNotNull(memberMoves.get(rewriter.unprefix(testedName) + '#' + method.name()));
+ dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0); // load "receiver"
+ dispatchMethod.visitTypeInsn(Opcodes.INSTANCEOF, testedName);
+ dispatchMethod.visitJumpInsn(Opcodes.IFEQ, fallthrough);
+ dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0); // load "receiver"
+ dispatchMethod.visitTypeInsn(Opcodes.CHECKCAST, testedName); // make verifier happy
+
+ visitLoadArgs(dispatchMethod, methodType, 1 /* receiver already loaded above */);
+ dispatchMethod.visitMethodInsn(
+ Opcodes.INVOKESTATIC,
+ target,
+ method.name(),
+ InterfaceDesugaring.companionDefaultMethodDescriptor(testedName, method.descriptor()),
+ /*itf=*/ false);
+ dispatchMethod.visitInsn(methodType.getReturnType().getOpcode(Opcodes.IRETURN));
+
+ dispatchMethod.visitLabel(fallthrough);
+ // Trivial frame for the branch target: same empty stack as before
+ dispatchMethod.visitFrame(Opcodes.F_SAME, 0, EMPTY_FRAME, 0, EMPTY_FRAME);
+ }
+
+ // Call static type's default implementation in companion class
+ dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0); // load "receiver"
+ visitLoadArgs(dispatchMethod, methodType, 1 /* receiver already loaded above */);
+ dispatchMethod.visitMethodInsn(
+ Opcodes.INVOKESTATIC,
+ InterfaceDesugaring.getCompanionClassName(owner),
+ method.name(),
+ companionDesc,
+ /*itf=*/ false);
+ dispatchMethod.visitInsn(methodType.getReturnType().getOpcode(Opcodes.IRETURN));
+
+ dispatchMethod.visitMaxs(0, 0);
+ dispatchMethod.visitEnd();
+ }
+
+ private boolean isExcluded(Method method) {
+ String unprefixedOwner =
+ rewriter.unprefix(method.getDeclaringClass().getName().replace('.', '/'));
+ return excludeFromEmulation.contains(unprefixedOwner + "#" + method.getName());
+ }
+
+ private Class<?> loadFromInternal(String internalName) {
+ try {
+ return targetLoader.loadClass(internalName.replace('/', '.'));
+ } catch (ClassNotFoundException e) {
+ throw (NoClassDefFoundError) new NoClassDefFoundError().initCause(e);
+ }
+ }
+
+ private static Method findInterfaceMethod(Class<?> clazz, String name, String desc) {
+ return collectImplementedInterfaces(clazz, new LinkedHashSet<>())
+ .stream()
+ // search more subtypes before supertypes
+ .sorted(DefaultMethodClassFixer.SubtypeComparator.INSTANCE)
+ .map(itf -> findMethod(itf, name, desc))
+ .filter(Objects::nonNull)
+ .findFirst()
+ .orElse((Method) null);
+ }
+
+ private static Method findMethod(Class<?> clazz, String name, String desc) {
+ for (Method m : clazz.getMethods()) {
+ if (m.getName().equals(name) && Type.getMethodDescriptor(m).equals(desc)) {
+ return m;
+ }
+ }
+ return null;
+ }
+
+ private static Set<Class<?>> collectImplementedInterfaces(Class<?> clazz, Set<Class<?>> dest) {
+ if (clazz.isInterface()) {
+ if (!dest.add(clazz)) {
+ return dest;
+ }
+ } else if (clazz.getSuperclass() != null) {
+ collectImplementedInterfaces(clazz.getSuperclass(), dest);
+ }
+
+ for (Class<?> itf : clazz.getInterfaces()) {
+ collectImplementedInterfaces(itf, dest);
+ }
+ return dest;
+ }
+
+ /**
+ * Emits instructions to load a method's parameters as arguments of a method call assumed to have
+ * compatible descriptor, starting at the given local variable slot.
+ */
+ private static void visitLoadArgs(MethodVisitor dispatchMethod, Type neededType, int slot) {
+ for (Type arg : neededType.getArgumentTypes()) {
+ dispatchMethod.visitVarInsn(arg.getOpcode(Opcodes.ILOAD), slot);
+ slot += arg.getSize();
+ }
+ }
+
+ /** Checks whether the given class is (likely) generated by desugar itself. */
+ private static boolean looksGenerated(String owner) {
+ return owner.contains("$$Lambda$") || owner.endsWith("$$CC") || owner.endsWith("$$Dispatch");
+ }
+
+ @AutoValue
+ @Immutable
+ abstract static class EmulatedMethod {
+ public static EmulatedMethod create(
+ int access, Class<?> owner, String name, String desc, @Nullable String[] exceptions) {
+ return new AutoValue_CoreLibrarySupport_EmulatedMethod(access, owner, name, desc,
+ exceptions != null ? ImmutableList.copyOf(exceptions) : ImmutableList.of());
+ }
+
+ abstract int access();
+ abstract Class<?> owner();
+ abstract String name();
+ abstract String descriptor();
+ abstract ImmutableList<String> exceptions();
+ }
+}
diff --git a/java/com/google/devtools/build/android/desugar/CorePackageRenamer.java b/java/com/google/devtools/build/android/desugar/CorePackageRenamer.java
new file mode 100644
index 0000000..5f1dc2e
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/CorePackageRenamer.java
@@ -0,0 +1,45 @@
+// Copyright 2018 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 org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.commons.ClassRemapper;
+import org.objectweb.asm.commons.Remapper;
+
+/**
+ * A visitor that renames packages so configured using {@link CoreLibrarySupport}..
+ */
+class CorePackageRenamer extends ClassRemapper {
+
+ public CorePackageRenamer(ClassVisitor cv, CoreLibrarySupport support) {
+ super(cv, new CorePackageRemapper(support));
+ }
+
+ private static final class CorePackageRemapper extends Remapper {
+ private final CoreLibrarySupport support;
+
+ private CorePackageRemapper(CoreLibrarySupport support) {
+ this.support = support;
+ }
+
+ public boolean isRenamed(String owner) {
+ return support.isRenamedCoreLibrary(owner);
+ }
+
+ @Override
+ public String map(String typeName) {
+ return isRenamed(typeName) ? support.renameCoreLibrary(typeName) : typeName;
+ }
+ }
+}
diff --git a/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java b/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java
index 1aaf0b6..1de48bf 100644
--- a/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java
+++ b/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java
@@ -18,11 +18,15 @@ 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.devtools.build.android.desugar.io.BitFlags;
+import java.lang.reflect.Method;
+import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;
+import javax.annotation.Nullable;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
@@ -44,6 +48,7 @@ public class DefaultMethodClassFixer extends ClassVisitor {
private final ClassReaderFactory bootclasspath;
private final ClassLoader targetLoader;
private final DependencyCollector depsCollector;
+ @Nullable private final CoreLibrarySupport coreLibrarySupport;
private final HashSet<String> instanceMethods = new HashSet<>();
private boolean isInterface;
@@ -57,10 +62,12 @@ public class DefaultMethodClassFixer extends ClassVisitor {
ClassVisitor dest,
ClassReaderFactory classpath,
DependencyCollector depsCollector,
+ @Nullable CoreLibrarySupport coreLibrarySupport,
ClassReaderFactory bootclasspath,
ClassLoader targetLoader) {
super(Opcodes.ASM6, dest);
this.classpath = classpath;
+ this.coreLibrarySupport = coreLibrarySupport;
this.bootclasspath = bootclasspath;
this.targetLoader = targetLoader;
this.depsCollector = depsCollector;
@@ -88,7 +95,9 @@ public class DefaultMethodClassFixer extends ClassVisitor {
@Override
public void visitEnd() {
- if (!isInterface && defaultMethodsDefined(directInterfaces)) {
+ if (!isInterface
+ && (mayNeedInterfaceStubsForEmulatedSuperclass()
+ || defaultMethodsDefined(directInterfaces))) {
// 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();
@@ -190,8 +199,14 @@ public class DefaultMethodClassFixer extends ClassVisitor {
return super.visitMethod(access, name, desc, signature, exceptions);
}
+ private boolean mayNeedInterfaceStubsForEmulatedSuperclass() {
+ return coreLibrarySupport != null
+ && !coreLibrarySupport.isEmulatedCoreClassOrInterface(internalName)
+ && coreLibrarySupport.isEmulatedCoreClassOrInterface(superName);
+ }
+
private void stubMissingDefaultAndBridgeMethods() {
- TreeSet<Class<?>> allInterfaces = new TreeSet<>(InterfaceComparator.INSTANCE);
+ TreeSet<Class<?>> allInterfaces = new TreeSet<>(SubtypeComparator.INSTANCE);
for (String direct : directInterfaces) {
// 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
@@ -203,19 +218,55 @@ public class DefaultMethodClassFixer extends ClassVisitor {
}
Class<?> superclass = loadFromInternal(superName);
+ boolean mayNeedStubsForSuperclass = mayNeedInterfaceStubsForEmulatedSuperclass();
+ if (mayNeedStubsForSuperclass) {
+ // Collect interfaces inherited from emulated superclasses as well, to handle things like
+ // extending AbstractList without explicitly implementing List.
+ for (Class<?> clazz = superclass; clazz != null; clazz = clazz.getSuperclass()) {
+ for (Class<?> itf : superclass.getInterfaces()) {
+ collectInterfaces(itf, allInterfaces);
+ }
+ }
+ }
for (Class<?> interfaceToVisit : allInterfaces) {
// 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 (superclass != null && interfaceToVisit.isAssignableFrom(superclass)) {
- // superclass already implements this interface, so we must skip it. The superclass will
- // be similarly rewritten or comes from the bootclasspath; either way we don't need to and
- // shouldn't stub default methods for this interface.
+ if (!mayNeedStubsForSuperclass && interfaceToVisit.isAssignableFrom(superclass)) {
+ // superclass is also rewritten and already implements this interface, so we _must_ skip it.
continue;
}
- stubMissingDefaultAndBridgeMethods(interfaceToVisit.getName().replace('.', '/'));
+ stubMissingDefaultAndBridgeMethods(
+ interfaceToVisit.getName().replace('.', '/'), 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('/', '.'));
@@ -236,7 +287,8 @@ public class DefaultMethodClassFixer extends ClassVisitor {
}
private void recordInheritedMethods() {
- InstanceMethodRecorder recorder = new InstanceMethodRecorder();
+ InstanceMethodRecorder recorder =
+ new InstanceMethodRecorder(mayNeedInterfaceStubsForEmulatedSuperclass());
String internalName = superName;
while (internalName != null) {
ClassReader bytecode = bootclasspath.readIfKnown(internalName);
@@ -313,26 +365,38 @@ public class DefaultMethodClassFixer extends ClassVisitor {
*/
private boolean defaultMethodsDefined(ImmutableList<String> interfaces) {
for (String implemented : interfaces) {
+ ClassReader bytecode;
if (bootclasspath.isKnown(implemented)) {
- continue;
- }
- ClassReader 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);
+ 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 {
- // 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;
+ 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;
}
@@ -365,30 +429,22 @@ public class DefaultMethodClassFixer extends ClassVisitor {
&& !instanceMethods.contains(name + ":" + desc);
}
- private void stubMissingDefaultAndBridgeMethods(String implemented) {
- if (bootclasspath.isKnown(implemented)) {
- // Default methods on the bootclasspath will be available at runtime, so just ignore them.
- return;
- }
- ClassReader bytecode =
- checkNotNull(
- classpath.readIfKnown(implemented),
- "Couldn't find interface %s implemented by %s",
- implemented,
- internalName);
- bytecode.accept(new DefaultMethodStubber(), ClassReader.SKIP_DEBUG);
- }
-
/**
* 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() {
+ public DefaultMethodStubber(
+ boolean isBootclasspathInterface, boolean mayNeedStubsForSuperclass) {
super(Opcodes.ASM6);
+ this.isBootclasspathInterface = isBootclasspathInterface;
+ this.mayNeedStubsForSuperclass = mayNeedStubsForSuperclass;
}
@Override
@@ -413,8 +469,11 @@ public class DefaultMethodClassFixer extends ClassVisitor {
// definitions conflict, but see stubMissingDefaultMethods() for how we deal with default
// methods redefined in interfaces extending another.
recordIfInstanceMethod(access, name, desc);
- depsCollector.assumeCompanionClass(
- internalName, InterfaceDesugaring.getCompanionClassName(stubbedInterfaceName));
+ if (!isBootclasspathInterface) {
+ // Don't record these dependencies, as we can't check them
+ depsCollector.assumeCompanionClass(
+ internalName, InterfaceDesugaring.getCompanionClassName(stubbedInterfaceName));
+ }
// 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
@@ -424,6 +483,21 @@ public class DefaultMethodClassFixer extends ClassVisitor {
MethodVisitor stubMethod =
DefaultMethodClassFixer.this.visitMethod(access, name, desc, (String) null, exceptions);
+ String receiverName = stubbedInterfaceName;
+ String owner = InterfaceDesugaring.getCompanionClassName(stubbedInterfaceName);
+ 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 = emulatedImplementation.getName().replace('.', '/');
+ owner = checkNotNull(coreLibrarySupport.getMoveTarget(receiverName, name));
+ }
+ }
+
int slot = 0;
stubMethod.visitVarInsn(Opcodes.ALOAD, slot++); // load the receiver
Type neededType = Type.getMethodType(desc);
@@ -433,30 +507,92 @@ public class DefaultMethodClassFixer extends ClassVisitor {
}
stubMethod.visitMethodInsn(
Opcodes.INVOKESTATIC,
- InterfaceDesugaring.getCompanionClassName(stubbedInterfaceName),
+ owner,
name,
- InterfaceDesugaring.companionDefaultMethodDescriptor(stubbedInterfaceName, desc),
- /*itf*/ false);
+ InterfaceDesugaring.companionDefaultMethodDescriptor(receiverName, desc),
+ /*itf=*/ false);
stubMethod.visitInsn(neededType.getReturnType().getOpcode(Opcodes.IRETURN));
stubMethod.visitMaxs(0, 0); // rely on class writer to compute these
stubMethod.visitEnd();
- return null;
+ return null; // don't visit the visited interface's default method
} else if (shouldStubAsBridgeDefaultMethod(access, name, desc)) {
recordIfInstanceMethod(access, name, desc);
- // 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(
- DefaultMethodClassFixer.this.visitMethod(access, name, desc, (String) null, exceptions),
- stubbedInterfaceName,
- bootclasspath,
- depsCollector,
- internalName);
+ 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);
+ for (Type arg : neededType.getArgumentTypes()) {
+ // TODO(b/73586397): insert downcasts if necessary
+ stubMethod.visitVarInsn(arg.getOpcode(Opcodes.ILOAD), slot);
+ slot += arg.getSize();
+ }
+ // Just call the bridged method directly on the visited class using invokevirtual
+ stubMethod.visitMethodInsn(
+ Opcodes.INVOKEVIRTUAL,
+ internalName,
+ name,
+ neededType.getDescriptor(),
+ /*itf=*/ 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; // we don't care about the actual code in these methods
+ 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);
+ Class<?> itf = loadFromInternal(stubbedInterfaceName);
+ checkArgument(itf.isInterface(), "Should be an interface: %s", stubbedInterfaceName);
+ Method result = null;
+ for (Method m : itf.getDeclaredMethods()) {
+ if (m.isBridge()) {
+ continue;
+ }
+ if (!m.getName().equals(name)) {
+ continue;
+ }
+ // For now, only support specialized return types (which don't require casts)
+ // TODO(b/73586397): Make this work for other kinds of bridges in core library interfaces
+ if (Arrays.equals(paramTypes, Type.getArgumentTypes(m))) {
+ checkState(result == null,
+ "Found multiple bridge target %s and %s for descriptor %s", result, m, desc);
+ return result = m;
+ }
+ }
+ return result;
+ }
}
/**
@@ -510,8 +646,13 @@ public class DefaultMethodClassFixer extends ClassVisitor {
private class InstanceMethodRecorder extends ClassVisitor {
- public InstanceMethodRecorder() {
+ private final boolean ignoreEmulatedMethods;
+
+ private String className;
+
+ public InstanceMethodRecorder(boolean ignoreEmulatedMethods) {
super(Opcodes.ASM6);
+ this.ignoreEmulatedMethods = ignoreEmulatedMethods;
}
@Override
@@ -523,12 +664,23 @@ public class DefaultMethodClassFixer extends ClassVisitor {
String superName,
String[] interfaces) {
checkArgument(BitFlags.noneSet(access, Opcodes.ACC_INTERFACE));
+ 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.noneSet(access, Opcodes.ACC_STATIC) // 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;
}
@@ -594,17 +746,17 @@ public class DefaultMethodClassFixer extends ClassVisitor {
}
}
- /** Comparator for interfaces that compares by whether interfaces extend one another. */
- enum InterfaceComparator implements Comparator<Class<?>> {
+ /** 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) {
- checkArgument(o1.isInterface());
- checkArgument(o2.isInterface());
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
}
diff --git a/java/com/google/devtools/build/android/desugar/Desugar.java b/java/com/google/devtools/build/android/desugar/Desugar.java
index 5d3df4a..c176f9c 100644
--- a/java/com/google/devtools/build/android/desugar/Desugar.java
+++ b/java/com/google/devtools/build/android/desugar/Desugar.java
@@ -17,7 +17,6 @@ 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 static com.google.devtools.build.android.desugar.LambdaClassMaker.LAMBDA_METAFACTORY_DUMPER_PROPERTY;
-import static java.nio.charset.StandardCharsets.ISO_8859_1;
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
@@ -28,17 +27,24 @@ import com.google.common.io.ByteStreams;
import com.google.common.io.Closer;
import com.google.devtools.build.android.Converters.ExistingPathConverter;
import com.google.devtools.build.android.Converters.PathConverter;
-import com.google.devtools.build.android.desugar.CoreLibraryRewriter.UnprefixingClassWriter;
+import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter;
+import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter.UnprefixingClassWriter;
+import com.google.devtools.build.android.desugar.io.HeaderClassLoader;
+import com.google.devtools.build.android.desugar.io.IndexedInputs;
+import com.google.devtools.build.android.desugar.io.InputFileProvider;
+import com.google.devtools.build.android.desugar.io.OutputFileProvider;
+import com.google.devtools.build.android.desugar.io.ThrowingClassLoader;
import com.google.devtools.common.options.Option;
import com.google.devtools.common.options.OptionDocumentationCategory;
import com.google.devtools.common.options.OptionEffectTag;
-import com.google.devtools.common.options.Options;
import com.google.devtools.common.options.OptionsBase;
-import com.google.errorprone.annotations.MustBeClosed;
+import com.google.devtools.common.options.OptionsParser;
+import com.google.devtools.common.options.ShellQuotedParamsFilePreProcessor;
import java.io.IOError;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
+import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -195,6 +201,15 @@ class Desugar {
public boolean tolerateMissingDependencies;
@Option(
+ name = "desugar_supported_core_libs",
+ defaultValue = "false",
+ documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+ effectTags = {OptionEffectTag.UNKNOWN},
+ help = "Enable core library desugaring, which requires configuration with related flags."
+ )
+ public boolean desugarCoreLibs;
+
+ @Option(
name = "desugar_interface_method_bodies_if_needed",
defaultValue = "true",
category = "misc",
@@ -248,6 +263,51 @@ class Desugar {
)
public boolean coreLibrary;
+ /** Type prefixes that we'll move to a custom package. */
+ @Option(
+ name = "rewrite_core_library_prefix",
+ defaultValue = "", // ignored
+ allowMultiple = true,
+ documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
+ effectTags = {OptionEffectTag.UNKNOWN},
+ help = "Assume the given java.* prefixes are desugared."
+ )
+ public List<String> rewriteCoreLibraryPrefixes;
+
+ /** Interfaces whose default and static interface methods we'll emulate. */
+ @Option(
+ name = "emulate_core_library_interface",
+ defaultValue = "", // ignored
+ allowMultiple = true,
+ documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
+ effectTags = {OptionEffectTag.UNKNOWN},
+ help = "Assume the given java.* interfaces are emulated."
+ )
+ public List<String> emulateCoreLibraryInterfaces;
+
+ /** Members that we will retarget to the given new owner. */
+ @Option(
+ name = "retarget_core_library_member",
+ defaultValue = "", // ignored
+ allowMultiple = true,
+ documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
+ effectTags = {OptionEffectTag.UNKNOWN},
+ help = "Method invocations to retarget, given as \"class/Name#member->new/class/Name\". "
+ + "The new owner is blindly assumed to exist."
+ )
+ public List<String> retargetCoreLibraryMembers;
+
+ /** Members not to rewrite. */
+ @Option(
+ name = "dont_rewrite_core_library_invocation",
+ defaultValue = "", // ignored
+ allowMultiple = true,
+ documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
+ effectTags = {OptionEffectTag.UNKNOWN},
+ help = "Method invocations not to rewrite, given as \"class/Name#method\"."
+ )
+ public List<String> dontTouchCoreLibraryMembers;
+
/** Set to work around b/62623509 with JaCoCo versions prior to 0.7.9. */
// TODO(kmb): Remove when Android Studio doesn't need it anymore (see b/37116789)
@Option(
@@ -265,7 +325,7 @@ class Desugar {
private final DesugarOptions options;
private final CoreLibraryRewriter rewriter;
private final LambdaClassMaker lambdas;
- private final GeneratedClassStore store;
+ private final GeneratedClassStore store = new GeneratedClassStore();
private final Set<String> visitedExceptionTypes = new HashSet<>();
/** The counter to record the times of try-with-resources desugaring is invoked. */
private final AtomicInteger numOfTryWithResourcesInvoked = new AtomicInteger();
@@ -282,7 +342,6 @@ class Desugar {
this.options = options;
this.rewriter = new CoreLibraryRewriter(options.coreLibrary ? "__desugar__/" : "");
this.lambdas = new LambdaClassMaker(dumpDirectory);
- this.store = new GeneratedClassStore();
this.outputJava7 = options.minSdkVersion < 24;
this.allowDefaultMethods =
options.desugarInterfaceMethodBodiesIfNeeded || options.minSdkVersion >= 24;
@@ -332,8 +391,8 @@ class Desugar {
Files.isDirectory(inputPath) || !Files.isDirectory(outputPath),
"Input jar file requires an output jar file");
- try (OutputFileProvider outputFileProvider = toOutputFileProvider(outputPath);
- InputFileProvider inputFiles = toInputFileProvider(inputPath)) {
+ try (OutputFileProvider outputFileProvider = OutputFileProvider.create(outputPath);
+ InputFileProvider inputFiles = InputFileProvider.open(inputPath)) {
DependencyCollector depsCollector = createDepsCollector();
IndexedInputs indexedInputFiles = new IndexedInputs(ImmutableList.of(inputFiles));
// Prepend classpath with input file itself so LambdaDesugaring can load classes with
@@ -358,6 +417,17 @@ class Desugar {
ImmutableSet.Builder<String> interfaceLambdaMethodCollector = ImmutableSet.builder();
ClassVsInterface interfaceCache = new ClassVsInterface(classpathReader);
+ CoreLibrarySupport coreLibrarySupport =
+ options.desugarCoreLibs
+ ? new CoreLibrarySupport(
+ rewriter,
+ loader,
+ options.rewriteCoreLibraryPrefixes,
+ options.emulateCoreLibraryInterfaces,
+ options.retargetCoreLibraryMembers,
+ options.dontTouchCoreLibraryMembers)
+ : null;
+
desugarClassesInInput(
inputFiles,
outputFileProvider,
@@ -365,6 +435,7 @@ class Desugar {
classpathReader,
depsCollector,
bootclasspathReader,
+ coreLibrarySupport,
interfaceCache,
interfaceLambdaMethodCollector);
@@ -374,11 +445,14 @@ class Desugar {
classpathReader,
depsCollector,
bootclasspathReader,
+ coreLibrarySupport,
interfaceCache,
interfaceLambdaMethodCollector.build(),
bridgeMethodReader);
- desugarAndWriteGeneratedClasses(outputFileProvider, bootclasspathReader);
+ desugarAndWriteGeneratedClasses(
+ outputFileProvider, loader, bootclasspathReader, coreLibrarySupport);
+
copyThrowableExtensionClass(outputFileProvider);
byte[] depsInfo = depsCollector.toByteArray();
@@ -394,7 +468,7 @@ class Desugar {
}
/**
- * Returns a dependency collector for use with a single input Jar. If
+ * Returns a dependency collector for use with a single input Jar. If
* {@link DesugarOptions#emitDependencyMetadata} is set, this method instantiates the collector
* reflectively to allow compiling and using the desugar tool without this mechanism.
*/
@@ -445,19 +519,20 @@ class Desugar {
@Nullable ClassReaderFactory classpathReader,
DependencyCollector depsCollector,
ClassReaderFactory bootclasspathReader,
+ @Nullable CoreLibrarySupport coreLibrarySupport,
ClassVsInterface interfaceCache,
ImmutableSet.Builder<String> interfaceLambdaMethodCollector)
throws IOException {
- for (String filename : inputFiles) {
- if (OutputFileProvider.DESUGAR_DEPS_FILENAME.equals(filename)) {
+ for (String inputFilename : inputFiles) {
+ if (OutputFileProvider.DESUGAR_DEPS_FILENAME.equals(inputFilename)) {
// TODO(kmb): rule out that this happens or merge input file with what's in depsCollector
continue; // skip as we're writing a new file like this at the end or don't want it
}
- try (InputStream content = inputFiles.getInputStream(filename)) {
+ try (InputStream content = inputFiles.getInputStream(inputFilename)) {
// We can write classes uncompressed since they need to be converted to .dex format
// for Android anyways. Resources are written as they were in the input jar to avoid
// any danger of accidentally uncompressed resources ending up in an .apk.
- if (filename.endsWith(".class")) {
+ if (inputFilename.endsWith(".class")) {
ClassReader reader = rewriter.reader(content);
UnprefixingClassWriter writer = rewriter.writer(ClassWriter.COMPUTE_MAXS);
ClassVisitor visitor =
@@ -466,19 +541,24 @@ class Desugar {
classpathReader,
depsCollector,
bootclasspathReader,
+ coreLibrarySupport,
interfaceCache,
interfaceLambdaMethodCollector,
writer,
reader);
if (writer == visitor) {
// Just copy the input if there are no rewritings
- outputFileProvider.write(filename, reader.b);
+ outputFileProvider.write(inputFilename, reader.b);
} else {
reader.accept(visitor, 0);
+ String filename = writer.getClassName() + ".class";
+ checkState(
+ (options.coreLibrary && coreLibrarySupport != null)
+ || filename.equals(inputFilename));
outputFileProvider.write(filename, writer.toByteArray());
}
} else {
- outputFileProvider.copyFrom(filename, inputFiles);
+ outputFileProvider.copyFrom(inputFilename, inputFiles);
}
}
}
@@ -494,6 +574,7 @@ class Desugar {
@Nullable ClassReaderFactory classpathReader,
DependencyCollector depsCollector,
ClassReaderFactory bootclasspathReader,
+ @Nullable CoreLibrarySupport coreLibrarySupport,
ClassVsInterface interfaceCache,
ImmutableSet<String> interfaceLambdaMethods,
@Nullable ClassReaderFactory bridgeMethodReader)
@@ -525,6 +606,7 @@ class Desugar {
classpathReader,
depsCollector,
bootclasspathReader,
+ coreLibrarySupport,
interfaceCache,
interfaceLambdaMethods,
bridgeMethodReader,
@@ -532,17 +614,26 @@ class Desugar {
writer,
reader);
reader.accept(visitor, 0);
- String filename =
- rewriter.unprefix(lambdaClass.getValue().desiredInternalName()) + ".class";
- outputFileProvider.write(filename, writer.toByteArray());
+ checkState(
+ (options.coreLibrary && coreLibrarySupport != null)
+ || rewriter
+ .unprefix(lambdaClass.getValue().desiredInternalName())
+ .equals(writer.getClassName()));
+ outputFileProvider.write(writer.getClassName() + ".class", writer.toByteArray());
}
}
}
private void desugarAndWriteGeneratedClasses(
- OutputFileProvider outputFileProvider, ClassReaderFactory bootclasspathReader)
+ OutputFileProvider outputFileProvider,
+ ClassLoader loader,
+ ClassReaderFactory bootclasspathReader,
+ @Nullable CoreLibrarySupport coreLibrarySupport)
throws IOException {
// Write out any classes we generated along the way
+ if (coreLibrarySupport != null) {
+ coreLibrarySupport.makeDispatchHelpers(store);
+ }
ImmutableMap<String, ClassNode> generatedClasses = store.drain();
checkState(
generatedClasses.isEmpty() || (allowDefaultMethods && outputJava7),
@@ -552,11 +643,39 @@ class Desugar {
UnprefixingClassWriter writer = rewriter.writer(ClassWriter.COMPUTE_MAXS);
// checkState above implies that we want Java 7 .class files, so send through that visitor.
// Don't need a ClassReaderFactory b/c static interface methods should've been moved.
- ClassVisitor visitor =
- new Java7Compatibility(writer, (ClassReaderFactory) null, bootclasspathReader);
+ ClassVisitor visitor = writer;
+ if (coreLibrarySupport != null) {
+ visitor = new EmulatedInterfaceRewriter(visitor, coreLibrarySupport);
+ visitor = new CorePackageRenamer(visitor, coreLibrarySupport);
+ visitor = new CoreLibraryInvocationRewriter(visitor, coreLibrarySupport);
+ }
+
+ if (!allowTryWithResources) {
+ CloseResourceMethodScanner closeResourceMethodScanner = new CloseResourceMethodScanner();
+ generated.getValue().accept(closeResourceMethodScanner);
+ visitor =
+ new TryWithResourcesRewriter(
+ visitor,
+ loader,
+ visitedExceptionTypes,
+ numOfTryWithResourcesInvoked,
+ closeResourceMethodScanner.hasCloseResourceMethod());
+ }
+ if (!allowCallsToObjectsNonNull) {
+ // Not sure whether there will be implicit null check emitted by javac, so we rerun
+ // the inliner again
+ visitor = new ObjectsRequireNonNullMethodRewriter(visitor, rewriter);
+ }
+ if (!allowCallsToLongCompare) {
+ visitor = new LongCompareMethodRewriter(visitor, rewriter);
+ }
+
+ visitor = new Java7Compatibility(visitor, (ClassReaderFactory) null, bootclasspathReader);
generated.getValue().accept(visitor);
- String filename = rewriter.unprefix(generated.getKey()) + ".class";
- outputFileProvider.write(filename, writer.toByteArray());
+ checkState(
+ (options.coreLibrary && coreLibrarySupport != null)
+ || rewriter.unprefix(generated.getKey()).equals(writer.getClassName()));
+ outputFileProvider.write(writer.getClassName() + ".class", writer.toByteArray());
}
}
@@ -569,6 +688,7 @@ class Desugar {
@Nullable ClassReaderFactory classpathReader,
DependencyCollector depsCollector,
ClassReaderFactory bootclasspathReader,
+ @Nullable CoreLibrarySupport coreLibrarySupport,
ClassVsInterface interfaceCache,
ImmutableSet<String> interfaceLambdaMethods,
@Nullable ClassReaderFactory bridgeMethodReader,
@@ -576,6 +696,13 @@ class Desugar {
UnprefixingClassWriter writer,
ClassReader input) {
ClassVisitor visitor = checkNotNull(writer);
+
+ if (coreLibrarySupport != null) {
+ visitor = new EmulatedInterfaceRewriter(visitor, coreLibrarySupport);
+ visitor = new CorePackageRenamer(visitor, coreLibrarySupport);
+ visitor = new CoreLibraryInvocationRewriter(visitor, coreLibrarySupport);
+ }
+
if (!allowTryWithResources) {
CloseResourceMethodScanner closeResourceMethodScanner = new CloseResourceMethodScanner();
input.accept(closeResourceMethodScanner, ClassReader.SKIP_DEBUG);
@@ -590,10 +717,10 @@ class Desugar {
if (!allowCallsToObjectsNonNull) {
// Not sure whether there will be implicit null check emitted by javac, so we rerun
// the inliner again
- visitor = new ObjectsRequireNonNullMethodRewriter(visitor);
+ visitor = new ObjectsRequireNonNullMethodRewriter(visitor, rewriter);
}
if (!allowCallsToLongCompare) {
- visitor = new LongCompareMethodRewriter(visitor);
+ visitor = new LongCompareMethodRewriter(visitor, rewriter);
}
if (outputJava7) {
// null ClassReaderFactory b/c we don't expect to need it for lambda classes
@@ -601,17 +728,25 @@ class Desugar {
if (options.desugarInterfaceMethodBodiesIfNeeded) {
visitor =
new DefaultMethodClassFixer(
- visitor, classpathReader, depsCollector, bootclasspathReader, loader);
+ visitor,
+ classpathReader,
+ depsCollector,
+ coreLibrarySupport,
+ bootclasspathReader,
+ loader);
visitor =
new InterfaceDesugaring(
visitor,
interfaceCache,
depsCollector,
+ coreLibrarySupport,
bootclasspathReader,
+ loader,
store,
options.legacyJacocoFix);
}
}
+
visitor =
new LambdaClassFixer(
visitor,
@@ -639,11 +774,19 @@ class Desugar {
@Nullable ClassReaderFactory classpathReader,
DependencyCollector depsCollector,
ClassReaderFactory bootclasspathReader,
+ @Nullable CoreLibrarySupport coreLibrarySupport,
ClassVsInterface interfaceCache,
ImmutableSet.Builder<String> interfaceLambdaMethodCollector,
UnprefixingClassWriter writer,
ClassReader input) {
ClassVisitor visitor = checkNotNull(writer);
+
+ if (coreLibrarySupport != null) {
+ visitor = new EmulatedInterfaceRewriter(visitor, coreLibrarySupport);
+ visitor = new CorePackageRenamer(visitor, coreLibrarySupport);
+ visitor = new CoreLibraryInvocationRewriter(visitor, coreLibrarySupport);
+ }
+
if (!allowTryWithResources) {
CloseResourceMethodScanner closeResourceMethodScanner = new CloseResourceMethodScanner();
input.accept(closeResourceMethodScanner, ClassReader.SKIP_DEBUG);
@@ -656,10 +799,10 @@ class Desugar {
closeResourceMethodScanner.hasCloseResourceMethod());
}
if (!allowCallsToObjectsNonNull) {
- visitor = new ObjectsRequireNonNullMethodRewriter(visitor);
+ visitor = new ObjectsRequireNonNullMethodRewriter(visitor, rewriter);
}
if (!allowCallsToLongCompare) {
- visitor = new LongCompareMethodRewriter(visitor);
+ visitor = new LongCompareMethodRewriter(visitor, rewriter);
}
if (!options.onlyDesugarJavac9ForLint) {
if (outputJava7) {
@@ -667,17 +810,25 @@ class Desugar {
if (options.desugarInterfaceMethodBodiesIfNeeded) {
visitor =
new DefaultMethodClassFixer(
- visitor, classpathReader, depsCollector, bootclasspathReader, loader);
+ visitor,
+ classpathReader,
+ depsCollector,
+ coreLibrarySupport,
+ bootclasspathReader,
+ loader);
visitor =
new InterfaceDesugaring(
visitor,
interfaceCache,
depsCollector,
+ coreLibrarySupport,
bootclasspathReader,
+ loader,
store,
options.legacyJacocoFix);
}
}
+
// LambdaDesugaring is relatively expensive, so check first whether we need it. Additionally,
// we need to collect lambda methods referenced by invokedynamic instructions up-front anyway.
// TODO(kmb): Scan constant pool instead of visiting the class to find bootstrap methods etc.
@@ -730,7 +881,7 @@ class Desugar {
} catch (ReflectiveOperationException e) {
// We do not want to crash Desugar, if we cannot load or access these classes or fields.
// We aim to provide better diagnostics. If we cannot, just let it go.
- e.printStackTrace();
+ e.printStackTrace(System.err); // To silence error-prone's complaint.
}
}
@@ -760,13 +911,12 @@ class Desugar {
return dumpDirectory;
}
- private static DesugarOptions parseCommandLineOptions(String[] args) throws IOException {
- if (args.length == 1 && args[0].startsWith("@")) {
- args = Files.readAllLines(Paths.get(args[0].substring(1)), ISO_8859_1).toArray(new String[0]);
- }
- DesugarOptions options =
- Options.parseAndExitUponError(DesugarOptions.class, /*allowResidue=*/ false, args)
- .getOptions();
+ private static DesugarOptions parseCommandLineOptions(String[] args) {
+ OptionsParser parser = OptionsParser.newOptionsParser(DesugarOptions.class);
+ parser.setAllowResidue(false);
+ parser.enableParamsFileSupport(new ShellQuotedParamsFilePreProcessor(FileSystems.getDefault()));
+ parser.parseAndExitUponError(args);
+ DesugarOptions options = parser.getOptions(DesugarOptions.class);
checkArgument(!options.inputJars.isEmpty(), "--input is required");
checkArgument(
@@ -780,6 +930,10 @@ class Desugar {
for (Path path : options.bootclasspath) {
checkArgument(!Files.isDirectory(path), "Bootclasspath entry must be a jar file: %s", path);
}
+ checkArgument(!options.desugarCoreLibs
+ || !options.rewriteCoreLibraryPrefixes.isEmpty()
+ || !options.emulateCoreLibraryInterfaces.isEmpty(),
+ "--desugar_supported_core_libs requires specifying renamed and/or emulated core libraries");
return options;
}
@@ -793,19 +947,6 @@ class Desugar {
return ioPairListbuilder.build();
}
- @VisibleForTesting
- static class ThrowingClassLoader extends ClassLoader {
- @Override
- protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
- if (name.startsWith("java.")) {
- // Use system class loader for java. classes, since ClassLoader.defineClass gets
- // grumpy when those don't come from the standard place.
- return super.loadClass(name, resolve);
- }
- throw new ClassNotFoundException();
- }
- }
-
private static void deleteTreeOnExit(final Path directory) {
Thread shutdownHook =
new Thread() {
@@ -844,26 +985,6 @@ class Desugar {
}
}
- /** Transform a Path to an {@link OutputFileProvider} */
- @MustBeClosed
- private static OutputFileProvider toOutputFileProvider(Path path) throws IOException {
- if (Files.isDirectory(path)) {
- return new DirectoryOutputFileProvider(path);
- } else {
- return new ZipOutputFileProvider(path);
- }
- }
-
- /** Transform a Path to an InputFileProvider that needs to be closed by the caller. */
- @MustBeClosed
- private static InputFileProvider toInputFileProvider(Path path) throws IOException {
- if (Files.isDirectory(path)) {
- return new DirectoryInputFileProvider(path);
- } else {
- return new ZipInputFileProvider(path);
- }
- }
-
/**
* Transform a list of Path to a list of InputFileProvider and register them with the given
* closer.
@@ -874,7 +995,7 @@ class Desugar {
Closer closer, List<Path> paths) throws IOException {
ImmutableList.Builder<InputFileProvider> builder = new ImmutableList.Builder<>();
for (Path path : paths) {
- builder.add(closer.register(toInputFileProvider(path)));
+ builder.add(closer.register(InputFileProvider.open(path)));
}
return builder.build();
}
diff --git a/java/com/google/devtools/build/android/desugar/EmulatedInterfaceRewriter.java b/java/com/google/devtools/build/android/desugar/EmulatedInterfaceRewriter.java
new file mode 100644
index 0000000..355dd97
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/EmulatedInterfaceRewriter.java
@@ -0,0 +1,92 @@
+// Copyright 2018 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 com.google.devtools.build.android.desugar.io.BitFlags;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.Opcodes;
+
+/**
+ * Visitor that renames emulated interfaces and marks classes that extend emulated interfaces to
+ * also implement the renamed interfaces. {@link DefaultMethodClassFixer} makes sure the requisite
+ * methods are present in all classes implementing the renamed interface. Doing this helps with
+ * dynamic dispatch on emulated interfaces.
+ */
+public class EmulatedInterfaceRewriter extends ClassVisitor {
+
+ private static final String[] EMPTY_ARRAY = new String[0];
+
+ private final CoreLibrarySupport support;
+
+ public EmulatedInterfaceRewriter(ClassVisitor dest, CoreLibrarySupport support) {
+ super(Opcodes.ASM6, dest);
+ this.support = support;
+ }
+
+ @Override
+ public void visit(
+ int version,
+ int access,
+ String name,
+ String signature,
+ String superName,
+ String[] interfaces) {
+ boolean emulated = support.isEmulatedCoreClassOrInterface(name);
+ {
+ // 1. see if we should implement any additional interfaces.
+ // Use LinkedHashSet to dedupe but maintain deterministic order
+ LinkedHashSet<String> newInterfaces = new LinkedHashSet<>();
+ if (interfaces != null && interfaces.length > 0) {
+ // Make classes implementing emulated interfaces also implement the renamed interfaces we
+ // create below. This includes making the renamed interfaces extends each other as needed.
+ Collections.addAll(newInterfaces, interfaces);
+ for (String itf : interfaces) {
+ if (support.isEmulatedCoreClassOrInterface(itf)) {
+ newInterfaces.add(support.renameCoreLibrary(itf));
+ }
+ }
+ }
+ if (!emulated) {
+ // For an immediate subclass of an emulated class, also fill in any interfaces implemented
+ // by superclasses, similar to the additional default method stubbing performed in
+ // DefaultMethodClassFixer in this situation.
+ Class<?> superclass = support.getEmulatedCoreClassOrInterface(superName);
+ while (superclass != null) {
+ for (Class<?> implemented : superclass.getInterfaces()) {
+ String itf = implemented.getName().replace('.', '/');
+ if (support.isEmulatedCoreClassOrInterface(itf)) {
+ newInterfaces.add(support.renameCoreLibrary(itf));
+ }
+ }
+ superclass = superclass.getSuperclass();
+ }
+ }
+ // Update implemented interfaces and signature if we did anything above
+ if (interfaces == null
+ ? !newInterfaces.isEmpty()
+ : interfaces.length != newInterfaces.size()) {
+ interfaces = newInterfaces.toArray(EMPTY_ARRAY);
+ signature = null; // additional interfaces invalidate any signature
+ }
+ }
+
+ // 2. see if we need to rename this interface itself
+ if (BitFlags.isInterface(access) && emulated) {
+ name = support.renameCoreLibrary(name);
+ }
+ super.visit(version, access, name, signature, superName, interfaces);
+ }
+}
diff --git a/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java b/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java
index b8b3ead..e9e3199 100644
--- a/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java
+++ b/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java
@@ -17,6 +17,9 @@ 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.devtools.build.android.desugar.io.BitFlags;
+import com.google.devtools.build.android.desugar.io.FieldInfo;
+import java.lang.reflect.Method;
import javax.annotation.Nullable;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassVisitor;
@@ -46,7 +49,9 @@ class InterfaceDesugaring extends ClassVisitor {
private final ClassVsInterface interfaceCache;
private final DependencyCollector depsCollector;
+ private final CoreLibrarySupport coreLibrarySupport;
private final ClassReaderFactory bootclasspath;
+ private final ClassLoader targetLoader;
private final GeneratedClassStore store;
private final boolean legacyJaCoCo;
@@ -61,13 +66,17 @@ class InterfaceDesugaring extends ClassVisitor {
ClassVisitor dest,
ClassVsInterface interfaceCache,
DependencyCollector depsCollector,
+ @Nullable CoreLibrarySupport coreLibrarySupport,
ClassReaderFactory bootclasspath,
+ ClassLoader targetLoader,
GeneratedClassStore store,
boolean legacyJaCoCo) {
super(Opcodes.ASM6, dest);
this.interfaceCache = interfaceCache;
this.depsCollector = depsCollector;
+ this.coreLibrarySupport = coreLibrarySupport;
this.bootclasspath = bootclasspath;
+ this.targetLoader = targetLoader;
this.store = store;
this.legacyJaCoCo = legacyJaCoCo;
}
@@ -210,6 +219,10 @@ class InterfaceDesugaring extends ClassVisitor {
internalName,
desc);
++numberOfDefaultMethods;
+ if (coreLibrarySupport != null) {
+ coreLibrarySupport.registerIfEmulatedCoreInterface(
+ access, internalName, name, desc, exceptions);
+ }
abstractDest =
super.visitMethod(access | Opcodes.ACC_ABSTRACT, name, desc, signature, exceptions);
}
@@ -234,7 +247,12 @@ class InterfaceDesugaring extends ClassVisitor {
}
return result != null
? new InterfaceInvocationRewriter(
- result, isInterface() ? internalName : null, bootclasspath, depsCollector, codeOwner)
+ result,
+ isInterface() ? internalName : null,
+ bootclasspath,
+ targetLoader,
+ depsCollector,
+ codeOwner)
: null;
}
@@ -265,8 +283,7 @@ class InterfaceDesugaring extends ClassVisitor {
return "<clinit>".equals(methodName);
}
- private static String normalizeInterfaceMethodName(
- String name, boolean isLambda, boolean isStatic) {
+ static String normalizeInterfaceMethodName(String name, boolean isLambda, boolean isStatic) {
if (isLambda) {
// Rename lambda method to reflect the new owner. Not doing so confuses LambdaDesugaring
// if it's run over this class again. LambdaDesugaring has already renamed the method from
@@ -355,6 +372,7 @@ class InterfaceDesugaring extends ClassVisitor {
@Nullable private final String interfaceName;
private final ClassReaderFactory bootclasspath;
+ private final ClassLoader targetLoader;
private final DependencyCollector depsCollector;
/** Internal name that'll be used to record any dependencies on interface methods. */
private final String declaringClass;
@@ -363,11 +381,13 @@ class InterfaceDesugaring extends ClassVisitor {
MethodVisitor dest,
@Nullable String knownInterfaceName,
ClassReaderFactory bootclasspath,
+ ClassLoader targetLoader,
DependencyCollector depsCollector,
String declaringClass) {
super(Opcodes.ASM6, dest);
this.interfaceName = knownInterfaceName;
this.bootclasspath = bootclasspath;
+ this.targetLoader = targetLoader;
this.depsCollector = depsCollector;
this.declaringClass = declaringClass;
}
@@ -410,7 +430,16 @@ class InterfaceDesugaring extends ClassVisitor {
checkArgument(!owner.endsWith(DependencyCollector.INTERFACE_COMPANION_SUFFIX),
"shouldn't consider %s an interface", owner);
if (opcode == Opcodes.INVOKESPECIAL) {
- // Turn Interface.super.m() into Interface$$CC.m(receiver)
+ // Turn Interface.super.m() into DefiningInterface$$CC.m(receiver). Note that owner
+ // always refers to the current type's immediate super-interface, but the default method
+ // may be inherited by that interface, so we have to figure out where the method is
+ // defined and invoke it in the corresponding companion class (b/73355452). Note that
+ // we're always dealing with interfaces here, and all interface methods are public,
+ // so using Class.getMethods should suffice to find inherited methods. Also note this
+ // can only be a default method invocation, no abstract method invocation.
+ owner =
+ findDefaultMethod(owner, name, desc)
+ .getDeclaringClass().getName().replace('.', '/');
opcode = Opcodes.INVOKESTATIC;
desc = companionDefaultMethodDescriptor(owner, desc);
}
@@ -422,6 +451,23 @@ class InterfaceDesugaring extends ClassVisitor {
}
super.visitMethodInsn(opcode, owner, name, desc, itf);
}
+
+ private Method findDefaultMethod(String owner, String name, String desc) {
+ try {
+ Class<?> clazz = targetLoader.loadClass(owner.replace('/', '.'));
+ // otherwise getting public methods with getMethods() below isn't enough
+ checkArgument(clazz.isInterface(), "Not an interface: %s", owner);
+ for (Method m : clazz.getMethods()) {
+ if (m.getName().equals(name) && Type.getMethodDescriptor(m).equals(desc)) {
+ checkState(m.isDefault(), "Found non-default method: %s", m);
+ return m;
+ }
+ }
+ } catch (ClassNotFoundException e) {
+ throw new IllegalStateException("Couldn't load " + owner, e);
+ }
+ throw new IllegalArgumentException("Method not found: " + owner + "." + name + desc);
+ }
}
/**
diff --git a/java/com/google/devtools/build/android/desugar/Java7Compatibility.java b/java/com/google/devtools/build/android/desugar/Java7Compatibility.java
index 37a45dd..2090d5c 100644
--- a/java/com/google/devtools/build/android/desugar/Java7Compatibility.java
+++ b/java/com/google/devtools/build/android/desugar/Java7Compatibility.java
@@ -17,6 +17,7 @@ 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.devtools.build.android.desugar.io.BitFlags;
import javax.annotation.Nullable;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.Attribute;
diff --git a/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java b/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java
index fb05bcb..6b0a921 100644
--- a/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java
+++ b/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java
@@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkState;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.android.desugar.io.BitFlags;
import java.util.HashSet;
import java.util.LinkedHashSet;
import org.objectweb.asm.AnnotationVisitor;
diff --git a/java/com/google/devtools/build/android/desugar/LambdaClassMaker.java b/java/com/google/devtools/build/android/desugar/LambdaClassMaker.java
index 72f039a..94c6bbc 100644
--- a/java/com/google/devtools/build/android/desugar/LambdaClassMaker.java
+++ b/java/com/google/devtools/build/android/desugar/LambdaClassMaker.java
@@ -81,12 +81,12 @@ class LambdaClassMaker {
if (!Files.exists(rootPathPrefix.getParent())) {
return ImmutableList.of();
}
- try (Stream<Path> paths =
- Files.list(rootPathPrefix.getParent())
- .filter(
- path -> path.toString().startsWith(rootPathPrefixStr)
- && !existingPaths.contains(path))) {
- return paths.collect(ImmutableList.toImmutableList());
+ try (Stream<Path> paths = Files.list(rootPathPrefix.getParent())) {
+ return paths
+ .filter(
+ path ->
+ path.toString().startsWith(rootPathPrefixStr) && !existingPaths.contains(path))
+ .collect(ImmutableList.toImmutableList());
}
}
}
diff --git a/java/com/google/devtools/build/android/desugar/LambdaDesugaring.java b/java/com/google/devtools/build/android/desugar/LambdaDesugaring.java
index 5f41347..f9b5316 100644
--- a/java/com/google/devtools/build/android/desugar/LambdaDesugaring.java
+++ b/java/com/google/devtools/build/android/desugar/LambdaDesugaring.java
@@ -21,6 +21,7 @@ import static org.objectweb.asm.Opcodes.ASM6;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.android.desugar.io.BitFlags;
import java.io.IOException;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles.Lookup;
diff --git a/java/com/google/devtools/build/android/desugar/LongCompareMethodRewriter.java b/java/com/google/devtools/build/android/desugar/LongCompareMethodRewriter.java
index f66d862..7f2f355 100644
--- a/java/com/google/devtools/build/android/desugar/LongCompareMethodRewriter.java
+++ b/java/com/google/devtools/build/android/desugar/LongCompareMethodRewriter.java
@@ -17,6 +17,7 @@ import static org.objectweb.asm.Opcodes.ASM6;
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
import static org.objectweb.asm.Opcodes.LCMP;
+import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
@@ -26,8 +27,11 @@ import org.objectweb.asm.MethodVisitor;
*/
public class LongCompareMethodRewriter extends ClassVisitor {
- public LongCompareMethodRewriter(ClassVisitor cv) {
+ private final CoreLibraryRewriter rewriter;
+
+ public LongCompareMethodRewriter(ClassVisitor cv, CoreLibraryRewriter rewriter) {
super(ASM6, cv);
+ this.rewriter = rewriter;
}
@Override
@@ -37,7 +41,7 @@ public class LongCompareMethodRewriter extends ClassVisitor {
return visitor == null ? visitor : new LongCompareMethodVisitor(visitor);
}
- private static class LongCompareMethodVisitor extends MethodVisitor {
+ private class LongCompareMethodVisitor extends MethodVisitor {
public LongCompareMethodVisitor(MethodVisitor visitor) {
super(ASM6, visitor);
@@ -45,14 +49,14 @@ public class LongCompareMethodRewriter extends ClassVisitor {
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
- if (opcode != INVOKESTATIC
- || !owner.equals("java/lang/Long")
- || !name.equals("compare")
- || !desc.equals("(JJ)I")) {
+ if (opcode == INVOKESTATIC
+ && rewriter.unprefix(owner).equals("java/lang/Long")
+ && name.equals("compare")
+ && desc.equals("(JJ)I")) {
+ super.visitInsn(LCMP);
+ } else {
super.visitMethodInsn(opcode, owner, name, desc, itf);
- return;
}
- super.visitInsn(LCMP);
}
}
}
diff --git a/java/com/google/devtools/build/android/desugar/ObjectsRequireNonNullMethodRewriter.java b/java/com/google/devtools/build/android/desugar/ObjectsRequireNonNullMethodRewriter.java
index 86465d6..931459a 100644
--- a/java/com/google/devtools/build/android/desugar/ObjectsRequireNonNullMethodRewriter.java
+++ b/java/com/google/devtools/build/android/desugar/ObjectsRequireNonNullMethodRewriter.java
@@ -19,6 +19,7 @@ import static org.objectweb.asm.Opcodes.INVOKESTATIC;
import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
import static org.objectweb.asm.Opcodes.POP;
+import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
@@ -29,8 +30,11 @@ import org.objectweb.asm.MethodVisitor;
*/
public class ObjectsRequireNonNullMethodRewriter extends ClassVisitor {
- public ObjectsRequireNonNullMethodRewriter(ClassVisitor cv) {
+ private final CoreLibraryRewriter rewriter;
+
+ public ObjectsRequireNonNullMethodRewriter(ClassVisitor cv, CoreLibraryRewriter rewriter) {
super(ASM6, cv);
+ this.rewriter = rewriter;
}
@Override
@@ -40,7 +44,7 @@ public class ObjectsRequireNonNullMethodRewriter extends ClassVisitor {
return visitor == null ? visitor : new ObjectsMethodInlinerMethodVisitor(visitor);
}
- private static class ObjectsMethodInlinerMethodVisitor extends MethodVisitor {
+ private class ObjectsMethodInlinerMethodVisitor extends MethodVisitor {
public ObjectsMethodInlinerMethodVisitor(MethodVisitor mv) {
super(ASM6, mv);
@@ -48,20 +52,19 @@ public class ObjectsRequireNonNullMethodRewriter extends ClassVisitor {
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
- if (opcode != INVOKESTATIC
- || !owner.equals("java/util/Objects")
- || !name.equals("requireNonNull")
- || !desc.equals("(Ljava/lang/Object;)Ljava/lang/Object;")) {
+ if (opcode == INVOKESTATIC
+ && rewriter.unprefix(owner).equals("java/util/Objects")
+ && name.equals("requireNonNull")
+ && desc.equals("(Ljava/lang/Object;)Ljava/lang/Object;")) {
+ // a call to Objects.requireNonNull(Object o)
+ // duplicate the first argument 'o', as this method returns 'o'.
+ super.visitInsn(DUP);
+ super.visitMethodInsn(
+ INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
+ super.visitInsn(POP);
+ } else {
super.visitMethodInsn(opcode, owner, name, desc, itf);
- return;
}
-
- // a call to Objects.requireNonNull(Object o)
- // duplicate the first argument 'o', as this method returns 'o'.
- super.visitInsn(DUP);
- super.visitMethodInsn(
- INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
- super.visitInsn(POP);
}
}
}
diff --git a/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java b/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java
index 8e6d6d5..98eef45 100644
--- a/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java
+++ b/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java
@@ -29,6 +29,7 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.android.desugar.BytecodeTypeInference.InferredType;
+import com.google.devtools.build.android.desugar.io.BitFlags;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Optional;
@@ -165,14 +166,12 @@ public class TryWithResourcesRewriter extends ClassVisitor {
new CloseResourceMethodSpecializer(cv, resourceInternalName, isInterface));
}
} else {
+ // It is possible that all calls to $closeResources(...) are in dead code regions, and the
+ // calls are eliminated, which leaving the method $closeResources() unused. (b/78030676).
+ // In this case, we just discard the method body.
checkState(
- closeResourceMethod == null,
- "The field resourceTypeInternalNames is empty. "
- + "But the class has the $closeResource method.");
- checkState(
- !hasCloseResourceMethod,
- "The class %s has close resource method, but resourceTypeInternalNames is empty.",
- internalName);
+ !hasCloseResourceMethod || closeResourceMethod != null,
+ "There should be $closeResources(...) in the class file.");
}
super.visitEnd();
}
@@ -311,18 +310,20 @@ public class TryWithResourcesRewriter extends ClassVisitor {
InferredType resourceType = typeInference.getTypeOfOperandFromTop(0);
Optional<String> resourceClassInternalName = resourceType.getInternalName();
- checkState(
- resourceClassInternalName.isPresent(),
- "The resource class %s is not a reference type in %s.%s",
- resourceType,
- internalName,
- methodSignature);
- checkState(
- isAssignableFrom(
- "java.lang.AutoCloseable", resourceClassInternalName.get().replace('/', '.')),
- "The resource type should be a subclass of java.lang.AutoCloseable: %s",
- resourceClassInternalName);
-
+ {
+ // Check the resource type.
+ checkState(
+ resourceClassInternalName.isPresent(),
+ "The resource class %s is not a reference type in %s.%s",
+ resourceType,
+ internalName,
+ methodSignature);
+ String resourceClassName = resourceClassInternalName.get().replace('/', '.');
+ checkState(
+ hasCloseMethod(resourceClassName),
+ "The resource class %s should have a close() method.",
+ resourceClassName);
+ }
resourceTypeInternalNames.add(resourceClassInternalName.get());
super.visitMethodInsn(
opcode,
@@ -356,6 +357,26 @@ public class TryWithResourcesRewriter extends ClassVisitor {
return isAssignableFrom("java.lang.Throwable", owner.replace('/', '.'));
}
+ private boolean hasCloseMethod(String resourceClassName) {
+ try {
+ Class<?> klass = classLoader.loadClass(resourceClassName);
+ klass.getMethod("close");
+ return true;
+ } catch (ClassNotFoundException e) {
+ throw new AssertionError(
+ "Failed to load class "
+ + resourceClassName
+ + " when desugaring method "
+ + internalName
+ + "."
+ + methodSignature,
+ e);
+ } catch (NoSuchMethodException e) {
+ // There is no close() method in the class, so return false.
+ return false;
+ }
+ }
+
private boolean isAssignableFrom(String baseClassName, String subClassName) {
try {
Class<?> baseClass = classLoader.loadClass(baseClassName);
diff --git a/java/com/google/devtools/build/android/desugar/BitFlags.java b/java/com/google/devtools/build/android/desugar/io/BitFlags.java
index 8be2288..af6f481 100644
--- a/java/com/google/devtools/build/android/desugar/BitFlags.java
+++ b/java/com/google/devtools/build/android/desugar/io/BitFlags.java
@@ -11,12 +11,12 @@
// 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;
+package com.google.devtools.build.android.desugar.io;
import org.objectweb.asm.Opcodes;
/** Convenience method for working with {@code int} bitwise flags. */
-class BitFlags {
+public class BitFlags {
/**
* Returns {@code true} iff <b>all</b> bits in {@code bitmask} are set in {@code flags}. Trivially
diff --git a/java/com/google/devtools/build/android/desugar/CoreLibraryRewriter.java b/java/com/google/devtools/build/android/desugar/io/CoreLibraryRewriter.java
index 7f1591b..f3c546c 100644
--- a/java/com/google/devtools/build/android/desugar/CoreLibraryRewriter.java
+++ b/java/com/google/devtools/build/android/desugar/io/CoreLibraryRewriter.java
@@ -11,10 +11,11 @@
// 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;
+package com.google.devtools.build.android.desugar.io;
import java.io.IOException;
import java.io.InputStream;
+import javax.annotation.Nullable;
import org.objectweb.asm.Attribute;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
@@ -24,7 +25,7 @@ import org.objectweb.asm.commons.ClassRemapper;
import org.objectweb.asm.commons.Remapper;
/** Utility class to prefix or unprefix class names of core library classes */
-class CoreLibraryRewriter {
+public class CoreLibraryRewriter {
private final String prefix;
public CoreLibraryRewriter(String prefix) {
@@ -85,6 +86,10 @@ class CoreLibraryRewriter {
return false;
}
+ public String getPrefix() {
+ return prefix;
+ }
+
/** Removes prefix from class names */
public String unprefix(String typeName) {
if (prefix.isEmpty() || !typeName.startsWith(prefix)) {
@@ -152,6 +157,8 @@ class CoreLibraryRewriter {
public class UnprefixingClassWriter extends ClassVisitor {
private final ClassWriter writer;
+ private String finalClassName;
+
UnprefixingClassWriter(int flags) {
super(Opcodes.ASM6);
this.writer = new ClassWriter(flags);
@@ -159,7 +166,7 @@ class CoreLibraryRewriter {
if (!prefix.isEmpty()) {
this.cv =
new ClassRemapper(
- this.cv,
+ this.writer,
new Remapper() {
@Override
public String map(String typeName) {
@@ -169,8 +176,26 @@ class CoreLibraryRewriter {
}
}
- byte[] toByteArray() {
+ /** Returns the (unprefixed) name of the class once written. */
+ @Nullable
+ public String getClassName() {
+ return finalClassName;
+ }
+
+ public byte[] toByteArray() {
return writer.toByteArray();
}
+
+ @Override
+ public void visit(
+ int version,
+ int access,
+ String name,
+ String signature,
+ String superName,
+ String[] interfaces) {
+ finalClassName = unprefix(name);
+ super.visit(version, access, name, signature, superName, interfaces);
+ }
}
}
diff --git a/java/com/google/devtools/build/android/desugar/DirectoryInputFileProvider.java b/java/com/google/devtools/build/android/desugar/io/DirectoryInputFileProvider.java
index 1c5abc9..c607b42 100644
--- a/java/com/google/devtools/build/android/desugar/DirectoryInputFileProvider.java
+++ b/java/com/google/devtools/build/android/desugar/io/DirectoryInputFileProvider.java
@@ -11,7 +11,7 @@
// 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;
+package com.google.devtools.build.android.desugar.io;
import java.io.File;
import java.io.FileInputStream;
diff --git a/java/com/google/devtools/build/android/desugar/DirectoryOutputFileProvider.java b/java/com/google/devtools/build/android/desugar/io/DirectoryOutputFileProvider.java
index 782a81e..f8e87cb 100644
--- a/java/com/google/devtools/build/android/desugar/DirectoryOutputFileProvider.java
+++ b/java/com/google/devtools/build/android/desugar/io/DirectoryOutputFileProvider.java
@@ -11,7 +11,7 @@
// 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;
+package com.google.devtools.build.android.desugar.io;
import com.google.common.io.ByteStreams;
import java.io.IOException;
@@ -21,7 +21,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
/** Output provider is a directory. */
-public class DirectoryOutputFileProvider implements OutputFileProvider {
+class DirectoryOutputFileProvider implements OutputFileProvider {
private final Path root;
diff --git a/java/com/google/devtools/build/android/desugar/FieldInfo.java b/java/com/google/devtools/build/android/desugar/io/FieldInfo.java
index c281039..0b4f634 100644
--- a/java/com/google/devtools/build/android/desugar/FieldInfo.java
+++ b/java/com/google/devtools/build/android/desugar/io/FieldInfo.java
@@ -11,7 +11,7 @@
// 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;
+package com.google.devtools.build.android.desugar.io;
import com.google.auto.value.AutoValue;
@@ -19,7 +19,7 @@ import com.google.auto.value.AutoValue;
@AutoValue
public abstract class FieldInfo {
- static FieldInfo create(String owner, String name, String desc) {
+ public static FieldInfo create(String owner, String name, String desc) {
return new AutoValue_FieldInfo(owner, name, desc);
}
diff --git a/java/com/google/devtools/build/android/desugar/HeaderClassLoader.java b/java/com/google/devtools/build/android/desugar/io/HeaderClassLoader.java
index 0a757bf..f70dc0e 100644
--- a/java/com/google/devtools/build/android/desugar/HeaderClassLoader.java
+++ b/java/com/google/devtools/build/android/desugar/io/HeaderClassLoader.java
@@ -11,7 +11,7 @@
// 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;
+package com.google.devtools.build.android.desugar.io;
import com.google.common.collect.ImmutableList;
import java.io.IOError;
@@ -33,7 +33,7 @@ import org.objectweb.asm.Opcodes;
*
* @see java.net.URLClassLoader
*/
-class HeaderClassLoader extends ClassLoader {
+public class HeaderClassLoader extends ClassLoader {
private final IndexedInputs indexedInputs;
private final CoreLibraryRewriter rewriter;
@@ -58,7 +58,8 @@ class HeaderClassLoader extends ClassLoader {
// Have ASM compute maxs so we don't need to figure out how many formal parameters there are
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ImmutableList<FieldInfo> interfaceFieldNames = getFieldsIfReaderIsInterface(reader);
- reader.accept(new CodeStubber(writer, interfaceFieldNames), 0);
+ // TODO(kmb): Consider SKIP_CODE and stubbing everything so class loader doesn't verify code
+ reader.accept(new CodeStubber(writer, interfaceFieldNames), ClassReader.SKIP_DEBUG);
bytecode = writer.toByteArray();
} catch (IOException e) {
throw new IOError(e);
diff --git a/java/com/google/devtools/build/android/desugar/IndexedInputs.java b/java/com/google/devtools/build/android/desugar/io/IndexedInputs.java
index 33c6132..8ce4b62 100644
--- a/java/com/google/devtools/build/android/desugar/IndexedInputs.java
+++ b/java/com/google/devtools/build/android/desugar/io/IndexedInputs.java
@@ -11,7 +11,7 @@
// 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;
+package com.google.devtools.build.android.desugar.io;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
@@ -28,7 +28,7 @@ import javax.annotation.Nullable;
* scanning all inputs over and over for each class to load. An indexed inputs can have a parent
* that is firstly used when a file name is searched.
*/
-class IndexedInputs {
+public class IndexedInputs {
private final ImmutableMap<String, InputFileProvider> inputFiles;
diff --git a/java/com/google/devtools/build/android/desugar/InputFileProvider.java b/java/com/google/devtools/build/android/desugar/io/InputFileProvider.java
index c2b6353..c41d018 100644
--- a/java/com/google/devtools/build/android/desugar/InputFileProvider.java
+++ b/java/com/google/devtools/build/android/desugar/io/InputFileProvider.java
@@ -11,15 +11,18 @@
// 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;
+package com.google.devtools.build.android.desugar.io;
+import com.google.errorprone.annotations.MustBeClosed;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.zip.ZipEntry;
/** Input file provider allows to iterate on relative path filename of a directory or a jar file. */
-interface InputFileProvider extends Closeable, Iterable<String> {
+public interface InputFileProvider extends Closeable, Iterable<String> {
/**
* Return a ZipEntry for {@code filename}. If the provider is a {@link ZipInputFileProvider}, the
@@ -33,4 +36,14 @@ interface InputFileProvider extends Closeable, Iterable<String> {
* responsibility of the caller to close this stream.
*/
InputStream getInputStream(String filename) throws IOException;
+
+ /** Transform a Path to an InputFileProvider that needs to be closed by the caller. */
+ @MustBeClosed
+ public static InputFileProvider open(Path path) throws IOException {
+ if (Files.isDirectory(path)) {
+ return new DirectoryInputFileProvider(path);
+ } else {
+ return new ZipInputFileProvider(path);
+ }
+ }
}
diff --git a/java/com/google/devtools/build/android/desugar/OutputFileProvider.java b/java/com/google/devtools/build/android/desugar/io/OutputFileProvider.java
index 7a590ef..e693786 100644
--- a/java/com/google/devtools/build/android/desugar/OutputFileProvider.java
+++ b/java/com/google/devtools/build/android/desugar/io/OutputFileProvider.java
@@ -11,12 +11,15 @@
// 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;
+package com.google.devtools.build.android.desugar.io;
+import com.google.errorprone.annotations.MustBeClosed;
import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
/** Output file provider allows to write files in directory or jar files. */
-interface OutputFileProvider extends AutoCloseable {
+public interface OutputFileProvider extends AutoCloseable {
/** Filename to use to write out dependency metadata for later consistency checking. */
public static final String DESUGAR_DEPS_FILENAME = "META-INF/desugar_deps";
@@ -29,4 +32,14 @@ interface OutputFileProvider extends AutoCloseable {
/** Write {@code content} in {@code filename} to this output */
void write(String filename, byte[] content) throws IOException;
+
+ /** Transform a Path to an {@link OutputFileProvider} */
+ @MustBeClosed
+ public static OutputFileProvider create(Path path) throws IOException {
+ if (Files.isDirectory(path)) {
+ return new DirectoryOutputFileProvider(path);
+ } else {
+ return new ZipOutputFileProvider(path);
+ }
+ }
}
diff --git a/java/com/google/devtools/build/android/desugar/io/ThrowingClassLoader.java b/java/com/google/devtools/build/android/desugar/io/ThrowingClassLoader.java
new file mode 100644
index 0000000..16f83f2
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/io/ThrowingClassLoader.java
@@ -0,0 +1,27 @@
+// Copyright 2016 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.io;
+
+/** Class loader that throws whenever it can, for use the parent of a class loader hierarchy. */
+public class ThrowingClassLoader extends ClassLoader {
+ @Override
+ protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
+ if (name.startsWith("java.")) {
+ // Use system class loader for java. classes, since ClassLoader.defineClass gets
+ // grumpy when those don't come from the standard place.
+ return super.loadClass(name, resolve);
+ }
+ throw new ClassNotFoundException();
+ }
+} \ No newline at end of file
diff --git a/java/com/google/devtools/build/android/desugar/ZipInputFileProvider.java b/java/com/google/devtools/build/android/desugar/io/ZipInputFileProvider.java
index 307c8b8..9bd7758 100644
--- a/java/com/google/devtools/build/android/desugar/ZipInputFileProvider.java
+++ b/java/com/google/devtools/build/android/desugar/io/ZipInputFileProvider.java
@@ -11,7 +11,7 @@
// 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;
+package com.google.devtools.build.android.desugar.io;
import com.google.common.base.Functions;
import com.google.common.collect.Iterators;
diff --git a/java/com/google/devtools/build/android/desugar/ZipOutputFileProvider.java b/java/com/google/devtools/build/android/desugar/io/ZipOutputFileProvider.java
index 8d6501d..36cb26d 100644
--- a/java/com/google/devtools/build/android/desugar/ZipOutputFileProvider.java
+++ b/java/com/google/devtools/build/android/desugar/io/ZipOutputFileProvider.java
@@ -11,7 +11,7 @@
// 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;
+package com.google.devtools.build.android.desugar.io;
import static com.google.common.base.Preconditions.checkArgument;
@@ -26,7 +26,7 @@ import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/** Output provider is a zip file. */
-public class ZipOutputFileProvider implements OutputFileProvider {
+class ZipOutputFileProvider implements OutputFileProvider {
private final ZipOutputStream out;
diff --git a/java/com/google/devtools/build/android/desugar/scan/KeepReference.java b/java/com/google/devtools/build/android/desugar/scan/KeepReference.java
new file mode 100644
index 0000000..bae3f38
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/scan/KeepReference.java
@@ -0,0 +1,51 @@
+// Copyright 2018 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.scan;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.auto.value.AutoValue;
+import com.google.errorprone.annotations.Immutable;
+
+@AutoValue
+@Immutable
+abstract class KeepReference {
+ public static KeepReference classReference(String internalName) {
+ checkArgument(!internalName.isEmpty());
+ return new AutoValue_KeepReference(internalName, "", "");
+ }
+
+ public static KeepReference memberReference(String internalName, String name, String desc) {
+ checkArgument(!internalName.isEmpty());
+ checkArgument(!name.isEmpty());
+ checkArgument(!desc.isEmpty());
+ return new AutoValue_KeepReference(internalName, name, desc);
+ }
+
+ public final boolean isMemberReference() {
+ return !name().isEmpty();
+ }
+
+ public final boolean isMethodReference() {
+ return desc().startsWith("(");
+ }
+
+ public final boolean isFieldReference() {
+ return isMemberReference() && !isMethodReference();
+ }
+
+ public abstract String internalName();
+ public abstract String name();
+ public abstract String desc();
+}
diff --git a/java/com/google/devtools/build/android/desugar/scan/KeepScanner.java b/java/com/google/devtools/build/android/desugar/scan/KeepScanner.java
new file mode 100644
index 0000000..4924f7c
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/scan/KeepScanner.java
@@ -0,0 +1,303 @@
+// Copyright 2018 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.scan;
+
+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 static java.nio.file.StandardOpenOption.CREATE;
+import static java.util.Comparator.comparing;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.io.ByteStreams;
+import com.google.common.io.Closer;
+import com.google.devtools.build.android.Converters.ExistingPathConverter;
+import com.google.devtools.build.android.Converters.PathConverter;
+import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter;
+import com.google.devtools.build.android.desugar.io.HeaderClassLoader;
+import com.google.devtools.build.android.desugar.io.IndexedInputs;
+import com.google.devtools.build.android.desugar.io.InputFileProvider;
+import com.google.devtools.build.android.desugar.io.ThrowingClassLoader;
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionDocumentationCategory;
+import com.google.devtools.common.options.OptionEffectTag;
+import com.google.devtools.common.options.OptionsBase;
+import com.google.devtools.common.options.OptionsParser;
+import com.google.devtools.common.options.ShellQuotedParamsFilePreProcessor;
+import java.io.IOError;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.lang.reflect.Method;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.Type;
+
+class KeepScanner {
+
+ public static class KeepScannerOptions extends OptionsBase {
+ @Option(
+ name = "input",
+ defaultValue = "null",
+ documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+ effectTags = OptionEffectTag.UNKNOWN,
+ converter = ExistingPathConverter.class,
+ abbrev = 'i',
+ help = "Input Jar with classes to scan."
+ )
+ public Path inputJars;
+
+ @Option(
+ name = "classpath_entry",
+ allowMultiple = true,
+ defaultValue = "",
+ documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+ effectTags = {OptionEffectTag.UNKNOWN},
+ converter = ExistingPathConverter.class,
+ help =
+ "Ordered classpath (Jar or directory) to resolve symbols in the --input Jar, like "
+ + "javac's -cp flag."
+ )
+ public List<Path> classpath;
+
+ @Option(
+ name = "bootclasspath_entry",
+ allowMultiple = true,
+ defaultValue = "",
+ documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+ effectTags = {OptionEffectTag.UNKNOWN},
+ converter = ExistingPathConverter.class,
+ help =
+ "Bootclasspath that was used to compile the --input Jar with, like javac's "
+ + "-bootclasspath flag (required)."
+ )
+ public List<Path> bootclasspath;
+
+ @Option(
+ name = "keep_file",
+ defaultValue = "null",
+ documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+ effectTags = OptionEffectTag.UNKNOWN,
+ converter = PathConverter.class,
+ help = "Where to write keep rules to."
+ )
+ public Path keepDest;
+
+ @Option(
+ name = "prefix",
+ defaultValue = "j$/",
+ documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+ effectTags = OptionEffectTag.UNKNOWN,
+ help = "type to scan for."
+ )
+ public String prefix;
+ }
+
+ public static void main(String... args) throws Exception {
+ OptionsParser parser = OptionsParser.newOptionsParser(KeepScannerOptions.class);
+ parser.setAllowResidue(false);
+ parser.enableParamsFileSupport(new ShellQuotedParamsFilePreProcessor(FileSystems.getDefault()));
+ parser.parseAndExitUponError(args);
+ KeepScannerOptions options = parser.getOptions(KeepScannerOptions.class);
+
+ Map<String, ImmutableSet<KeepReference>> seeds;
+ try (Closer closer = Closer.create()) {
+ // TODO(kmb): Try to share more of this code with Desugar binary
+ IndexedInputs classpath =
+ new IndexedInputs(toRegisteredInputFileProvider(closer, options.classpath));
+ IndexedInputs bootclasspath =
+ new IndexedInputs(toRegisteredInputFileProvider(closer, options.bootclasspath));
+
+ // Construct classloader from classpath. Since we're assuming the prefix we're looking for
+ // isn't part of the input itself we shouldn't need to include the input in the classloader.
+ CoreLibraryRewriter noopRewriter = new CoreLibraryRewriter("");
+ ClassLoader classloader =
+ new HeaderClassLoader(classpath, noopRewriter,
+ new HeaderClassLoader(bootclasspath, noopRewriter,
+ new ThrowingClassLoader()));
+ seeds = scan(checkNotNull(options.inputJars), options.prefix, classloader);
+ }
+
+ try (PrintStream out =
+ new PrintStream(
+ Files.newOutputStream(options.keepDest, CREATE), /*autoFlush=*/ false, "UTF-8")) {
+ writeKeepDirectives(out, seeds);
+ }
+ }
+
+ /**
+ * Writes a -keep rule for each class listing any members to keep. We sort classes and members
+ * so the output is deterministic.
+ */
+ private static void writeKeepDirectives(
+ PrintStream out, Map<String, ImmutableSet<KeepReference>> seeds) {
+ seeds
+ .entrySet()
+ .stream()
+ .sorted(comparing(Map.Entry::getKey))
+ .forEachOrdered(
+ type -> {
+ out.printf("-keep class %s {%n", type.getKey().replace('/', '.'));
+ type.getValue()
+ .stream()
+ .filter(KeepReference::isMemberReference)
+ .sorted(comparing(KeepReference::name).thenComparing(KeepReference::desc))
+ .map(ref -> toKeepDescriptor(ref))
+ .distinct() // drop duplicates due to method descriptors with different returns
+ .forEachOrdered(line -> out.append(" ").append(line).append(";").println());
+ out.printf("}%n");
+ });
+ }
+
+ /** Scans for and returns references with owners matching the given prefix grouped by owner. */
+ private static Map<String, ImmutableSet<KeepReference>> scan(
+ Path jarFile, String prefix, ClassLoader classpath) throws IOException {
+ // We read the Jar sequentially since ZipFile uses locks anyway but then allow scanning each
+ // class in parallel.
+ try (ZipFile zip = new ZipFile(jarFile.toFile())) {
+ return zip.stream()
+ .filter(entry -> entry.getName().endsWith(".class"))
+ .map(entry -> readFully(zip, entry))
+ .parallel()
+ .flatMap(
+ content -> PrefixReferenceScanner.scan(new ClassReader(content), prefix).stream())
+ .distinct() // so we don't process the same reference multiple times next
+ .map(ref -> nearestDeclaration(ref, classpath))
+ .collect(
+ Collectors.groupingByConcurrent(
+ KeepReference::internalName, ImmutableSet.toImmutableSet()));
+ }
+ }
+
+ private static byte[] readFully(ZipFile zip, ZipEntry entry) {
+ byte[] result = new byte[(int) entry.getSize()];
+ try (InputStream content = zip.getInputStream(entry)) {
+ ByteStreams.readFully(content, result);
+ return result;
+ } catch (IOException e) {
+ throw new IOError(e);
+ }
+ }
+
+ /**
+ * Find the nearest definition of the given reference in the class hierarchy and return the
+ * modified reference. This is needed b/c bytecode sometimes refers to a method or field using
+ * an owner type that inherits the method or field instead of defining the member itself.
+ * In that case we need to find and keep the inherited definition.
+ */
+ private static KeepReference nearestDeclaration(KeepReference ref, ClassLoader classpath) {
+ if (!ref.isMemberReference() || "<init>".equals(ref.name())) {
+ return ref; // class and constructor references don't need any further work
+ }
+
+ Class<?> clazz;
+ try {
+ clazz = classpath.loadClass(ref.internalName().replace('/', '.'));
+ } catch (ClassNotFoundException e) {
+ throw (NoClassDefFoundError) new NoClassDefFoundError("Couldn't load " + ref).initCause(e);
+ }
+
+ Class<?> owner = findDeclaringClass(clazz, ref);
+ if (owner == clazz) {
+ return ref;
+ }
+ String parent = checkNotNull(owner, "Can't resolve: %s", ref).getName().replace('.', '/');
+ return KeepReference.memberReference(parent, ref.name(), ref.desc());
+ }
+
+ private static Class<?> findDeclaringClass(Class<?> clazz, KeepReference ref) {
+ if (ref.isFieldReference()) {
+ try {
+ return clazz.getField(ref.name()).getDeclaringClass();
+ } catch (NoSuchFieldException e) {
+ // field must be non-public, so search class hierarchy
+ do {
+ try {
+ return clazz.getDeclaredField(ref.name()).getDeclaringClass();
+ } catch (NoSuchFieldException ignored) {
+ // fall through for clarity
+ }
+ clazz = clazz.getSuperclass();
+ } while (clazz != null);
+ }
+ } else {
+ checkState(ref.isMethodReference());
+ Type descriptor = Type.getMethodType(ref.desc());
+ for (Method m : clazz.getMethods()) {
+ if (m.getName().equals(ref.name()) && Type.getType(m).equals(descriptor)) {
+ return m.getDeclaringClass();
+ }
+ }
+ do {
+ // Method must be non-public, so search class hierarchy
+ for (Method m : clazz.getDeclaredMethods()) {
+ if (m.getName().equals(ref.name()) && Type.getType(m).equals(descriptor)) {
+ return m.getDeclaringClass();
+ }
+ }
+ clazz = clazz.getSuperclass();
+ } while (clazz != null);
+ }
+ return null;
+ }
+
+ private static CharSequence toKeepDescriptor(KeepReference member) {
+ StringBuilder result = new StringBuilder();
+ if (member.isMethodReference()) {
+ if (!"<init>".equals(member.name())) {
+ result.append("*** ");
+ }
+ result.append(member.name()).append("(");
+ // Ignore return type as it's unique in the source language
+ boolean first = true;
+ for (Type param : Type.getMethodType(member.desc()).getArgumentTypes()) {
+ if (first) {
+ first = false;
+ } else {
+ result.append(", ");
+ }
+ result.append(param.getClassName());
+ }
+ result.append(")");
+ } else {
+ checkArgument(member.isFieldReference());
+ result.append("*** ").append(member.name()); // field names are unique so ignore descriptor
+ }
+ return result;
+ }
+
+ /**
+ * Transform a list of Path to a list of InputFileProvider and register them with the given
+ * closer.
+ */
+ @SuppressWarnings("MustBeClosedChecker")
+ private static ImmutableList<InputFileProvider> toRegisteredInputFileProvider(
+ Closer closer, List<Path> paths) throws IOException {
+ ImmutableList.Builder<InputFileProvider> builder = new ImmutableList.Builder<>();
+ for (Path path : paths) {
+ builder.add(closer.register(InputFileProvider.open(path)));
+ }
+ return builder.build();
+ }
+
+ private KeepScanner() {}
+}
diff --git a/java/com/google/devtools/build/android/desugar/scan/PrefixReferenceScanner.java b/java/com/google/devtools/build/android/desugar/scan/PrefixReferenceScanner.java
new file mode 100644
index 0000000..b899ccc
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/scan/PrefixReferenceScanner.java
@@ -0,0 +1,405 @@
+// Copyright 2018 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.scan;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.collect.ImmutableSet;
+import javax.annotation.Nullable;
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.Handle;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+import org.objectweb.asm.TypePath;
+
+/** {@link ClassVisitor} that records references to classes starting with a given prefix. */
+class PrefixReferenceScanner extends ClassVisitor {
+
+ /**
+ * Returns references with the given prefix in the given class.
+ *
+ * @param prefix an internal name prefix, typically a package such as {@code com/google/}
+ */
+ public static ImmutableSet<KeepReference> scan(ClassReader reader, String prefix) {
+ PrefixReferenceScanner scanner = new PrefixReferenceScanner(prefix);
+ // Frames irrelevant for Android so skip them. Don't skip debug info in case the class we're
+ // visiting has local variable tables (typically it doesn't anyways).
+ reader.accept(scanner, ClassReader.SKIP_FRAMES);
+ return scanner.roots.build();
+ }
+
+ private final ImmutableSet.Builder<KeepReference> roots = ImmutableSet.builder();
+ private final PrefixReferenceMethodVisitor mv = new PrefixReferenceMethodVisitor();
+ private final PrefixReferenceFieldVisitor fv = new PrefixReferenceFieldVisitor();
+ private final PrefixReferenceAnnotationVisitor av = new PrefixReferenceAnnotationVisitor();
+
+ private final String prefix;
+
+ public PrefixReferenceScanner(String prefix) {
+ super(Opcodes.ASM6);
+ this.prefix = prefix;
+ }
+
+ @Override
+ public void visit(
+ int version,
+ int access,
+ String name,
+ String signature,
+ String superName,
+ String[] interfaces) {
+ checkArgument(!name.startsWith(prefix));
+ if (superName != null) {
+ classReference(superName);
+ }
+ classReferences(interfaces);
+ super.visit(version, access, name, signature, superName, interfaces);
+ }
+
+ @Override
+ public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+ typeReference(desc);
+ return av;
+ }
+
+ @Override
+ public void visitOuterClass(String owner, String name, String desc) {
+ classReference(owner);
+ if (desc != null) {
+ typeReference(Type.getMethodType(desc));
+ }
+ }
+
+ @Override
+ public AnnotationVisitor visitTypeAnnotation(
+ int typeRef, TypePath typePath, String desc, boolean visible) {
+ typeReference(desc);
+ return av;
+ }
+
+ @Override
+ public void visitInnerClass(String name, String outerName, String innerName, int access) {
+ classReference(name);
+ if (outerName != null) {
+ classReference(outerName);
+ }
+ }
+
+ @Override
+ public FieldVisitor visitField(
+ int access, String name, String desc, String signature, Object value) {
+ typeReference(desc);
+ return fv;
+ }
+
+ @Override
+ public MethodVisitor visitMethod(
+ int access, String name, String desc, String signature, String[] exceptions) {
+ typeReference(Type.getMethodType(desc));
+ classReferences(exceptions);
+ return mv;
+ }
+
+ private void classReferences(@Nullable String[] internalNames) {
+ if (internalNames != null) {
+ for (String itf : internalNames) {
+ classReference(itf);
+ }
+ }
+ }
+
+ // The following methods are package-private so they don't incur bridge methods when called from
+ // inner classes below.
+
+ void classReference(String internalName) {
+ checkArgument(internalName.charAt(0) != '[' && internalName.charAt(0) != '(', internalName);
+ checkArgument(!internalName.endsWith(";"), internalName);
+ if (internalName.startsWith(prefix)) {
+ roots.add(KeepReference.classReference(internalName));
+ }
+ }
+
+ void objectReference(String internalName) {
+ // don't call this for method types, convert to Type instead
+ checkArgument(internalName.charAt(0) != '(', internalName);
+ if (internalName.charAt(0) == '[') {
+ typeReference(internalName);
+ } else {
+ classReference(internalName);
+ }
+ }
+
+ void typeReference(String typeDesc) {
+ // don't call this for method types, convert to Type instead
+ checkArgument(typeDesc.charAt(0) != '(', typeDesc);
+
+ int lpos = typeDesc.lastIndexOf('[') + 1;
+ if (typeDesc.charAt(lpos) == 'L') {
+ checkArgument(typeDesc.endsWith(";"), typeDesc);
+ classReference(typeDesc.substring(lpos, typeDesc.length() - 1));
+ } else {
+ // else primitive or primitive array
+ checkArgument(typeDesc.length() == lpos + 1, typeDesc);
+ switch (typeDesc.charAt(lpos)) {
+ case 'B':
+ case 'C':
+ case 'S':
+ case 'I':
+ case 'J':
+ case 'D':
+ case 'F':
+ case 'Z':
+ break;
+ default:
+ throw new AssertionError("Unexpected type descriptor: " + typeDesc);
+ }
+ }
+ }
+
+ void typeReference(Type type) {
+ switch (type.getSort()) {
+ case Type.ARRAY:
+ typeReference(type.getElementType());
+ break;
+ case Type.OBJECT:
+ classReference(type.getInternalName());
+ break;
+
+ case Type.METHOD:
+ for (Type param : type.getArgumentTypes()) {
+ typeReference(param);
+ }
+ typeReference(type.getReturnType());
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ void fieldReference(String owner, String name, String desc) {
+ objectReference(owner);
+ typeReference(desc);
+ if (owner.startsWith(prefix)) {
+ roots.add(KeepReference.memberReference(owner, name, desc));
+ }
+ }
+
+ void methodReference(String owner, String name, String desc) {
+ checkArgument(desc.charAt(0) == '(', desc);
+ objectReference(owner);
+ typeReference(Type.getMethodType(desc));
+ if (owner.startsWith(prefix)) {
+ roots.add(KeepReference.memberReference(owner, name, desc));
+ }
+ }
+
+ void handleReference(Handle handle) {
+ switch (handle.getTag()) {
+ case Opcodes.H_GETFIELD:
+ case Opcodes.H_GETSTATIC:
+ case Opcodes.H_PUTFIELD:
+ case Opcodes.H_PUTSTATIC:
+ fieldReference(handle.getOwner(), handle.getName(), handle.getDesc());
+ break;
+
+ default:
+ methodReference(handle.getOwner(), handle.getName(), handle.getDesc());
+ break;
+ }
+ }
+
+ private class PrefixReferenceMethodVisitor extends MethodVisitor {
+
+ public PrefixReferenceMethodVisitor() {
+ super(Opcodes.ASM6);
+ }
+
+ @Override
+ public AnnotationVisitor visitAnnotationDefault() {
+ return av;
+ }
+
+ @Override
+ public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+ typeReference(desc);
+ return av;
+ }
+
+ @Override
+ public AnnotationVisitor visitTypeAnnotation(
+ int typeRef, TypePath typePath, String desc, boolean visible) {
+ typeReference(desc);
+ return av;
+ }
+
+ @Override
+ public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) {
+ typeReference(desc);
+ return av;
+ }
+
+ @Override
+ public void visitTypeInsn(int opcode, String type) {
+ objectReference(type);
+ }
+
+ @Override
+ public void visitFieldInsn(int opcode, String owner, String name, String desc) {
+ fieldReference(owner, name, desc);
+ }
+
+ @Override
+ @SuppressWarnings("deprecation") // Implementing deprecated method to be sure
+ public void visitMethodInsn(int opcode, String owner, String name, String desc) {
+ visitMethodInsn(opcode, owner, name, desc, opcode == Opcodes.INVOKEINTERFACE);
+ }
+
+ @Override
+ public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
+ methodReference(owner, name, desc);
+ }
+
+ @Override
+ public void visitInvokeDynamicInsn(String name, String desc, Handle bsm, Object... bsmArgs) {
+ typeReference(Type.getMethodType(desc));
+ handleReference(bsm);
+ for (Object bsmArg : bsmArgs) {
+ visitConstant(bsmArg);
+ }
+ }
+
+ @Override
+ public void visitLdcInsn(Object cst) {
+ visitConstant(cst);
+ }
+
+ private void visitConstant(Object cst) {
+ if (cst instanceof Type) {
+ typeReference((Type) cst);
+ } else if (cst instanceof Handle) {
+ handleReference((Handle) cst);
+ } else {
+ // Check for other expected types as javadoc recommends
+ checkArgument(
+ cst instanceof String
+ || cst instanceof Integer
+ || cst instanceof Long
+ || cst instanceof Float
+ || cst instanceof Double,
+ "Unexpected constant: ", cst);
+ }
+ }
+
+ @Override
+ public void visitMultiANewArrayInsn(String desc, int dims) {
+ typeReference(desc);
+ }
+
+ @Override
+ public AnnotationVisitor visitInsnAnnotation(
+ int typeRef, TypePath typePath, String desc, boolean visible) {
+ typeReference(desc);
+ return av;
+ }
+
+ @Override
+ public void visitTryCatchBlock(Label start, Label end, Label handler, String type) {
+ if (type != null) {
+ classReference(type);
+ }
+ }
+
+ @Override
+ public AnnotationVisitor visitTryCatchAnnotation(
+ int typeRef, TypePath typePath, String desc, boolean visible) {
+ typeReference(desc);
+ return av;
+ }
+
+ @Override
+ public void visitLocalVariable(
+ String name, String desc, String signature, Label start, Label end, int index) {
+ typeReference(desc);
+ }
+
+ @Override
+ public AnnotationVisitor visitLocalVariableAnnotation(
+ int typeRef,
+ TypePath typePath,
+ Label[] start,
+ Label[] end,
+ int[] index,
+ String desc,
+ boolean visible) {
+ typeReference(desc);
+ return av;
+ }
+ }
+
+ private class PrefixReferenceFieldVisitor extends FieldVisitor {
+
+ public PrefixReferenceFieldVisitor() {
+ super(Opcodes.ASM6);
+ }
+
+ @Override
+ public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+ typeReference(desc);
+ return av;
+ }
+
+ @Override
+ public AnnotationVisitor visitTypeAnnotation(
+ int typeRef, TypePath typePath, String desc, boolean visible) {
+ typeReference(desc);
+ return av;
+ }
+ }
+
+ private class PrefixReferenceAnnotationVisitor extends AnnotationVisitor {
+
+ public PrefixReferenceAnnotationVisitor() {
+ super(Opcodes.ASM6);
+ }
+
+ @Override
+ public void visit(String name, Object value) {
+ if (value instanceof Type) {
+ typeReference((Type) value);
+ }
+ }
+
+ @Override
+ public void visitEnum(String name, String desc, String value) {
+ fieldReference(desc.substring(1, desc.length() - 1), value, desc);
+ }
+
+ @Override
+ public AnnotationVisitor visitAnnotation(String name, String desc) {
+ typeReference(desc);
+ return av;
+ }
+
+ @Override
+ public AnnotationVisitor visitArray(String name) {
+ return av;
+ }
+ }
+}