diff options
author | android-build-team Robot <android-build-team-robot@google.com> | 2017-05-17 08:01:22 +0000 |
---|---|---|
committer | android-build-team Robot <android-build-team-robot@google.com> | 2017-05-17 08:01:22 +0000 |
commit | eba736429e76922f94ecb5d16737cc7827401d9a (patch) | |
tree | 293694e1293dbc4326919e772e3ffb8722985924 | |
parent | 19ec33977159326b7e3ed2562071a351a32540a6 (diff) | |
parent | 86f6f67af2035e113a2bc4ba4c23709481862acc (diff) | |
download | desugar-eba736429e76922f94ecb5d16737cc7827401d9a.tar.gz |
release-request-655284b0-7f0b-48b5-b01d-27f48a99f2c0-for-git_oc-mr1-release-4017105 snap-temp-L82600000064873863
Change-Id: I0bb134d4f6b032f7bed900b00521f5d07079e5fb
5 files changed, 201 insertions, 66 deletions
diff --git a/copy.bara.sky b/copy.bara.sky index bc83110..0f9df66 100644 --- a/copy.bara.sky +++ b/copy.bara.sky @@ -1,5 +1,5 @@ # Copybara project config for importing desugar tool from bazel -import_url = "rpc://github/bazelbuild/bazel" +import_url = "rpc://github.googlesource.com/bazelbuild/bazel" core.workflow( name = "default", @@ -19,7 +19,7 @@ core.workflow( ], ), destination = git.gerrit_destination( - url = "sso://android-review/platform/external/desugar", + url = "rpc://android-review.googlesource.com/platform/external/desugar", fetch = "upstream-master", push_to_refs_for = "upstream-master", ), diff --git a/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java b/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java index 8ad5dc2..c42062a 100644 --- a/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java +++ b/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java @@ -18,7 +18,10 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import com.google.common.collect.ImmutableList; +import java.util.Comparator; import java.util.HashSet; +import java.util.Set; +import java.util.TreeSet; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.MethodVisitor; @@ -33,18 +36,20 @@ public class DefaultMethodClassFixer extends ClassVisitor { private final ClassReaderFactory classpath; private final ClassReaderFactory bootclasspath; + private final ClassLoader targetLoader; private final HashSet<String> instanceMethods = new HashSet<>(); - private final HashSet<String> seenInterfaces = new HashSet<>(); private boolean isInterface; - private ImmutableList<String> interfaces; + private String internalName; + private ImmutableList<String> directInterfaces; private String superName; public DefaultMethodClassFixer(ClassVisitor dest, ClassReaderFactory classpath, - ClassReaderFactory bootclasspath) { + ClassReaderFactory bootclasspath, ClassLoader targetLoader) { super(Opcodes.ASM5, dest); this.classpath = classpath; this.bootclasspath = bootclasspath; + this.targetLoader = targetLoader; } @Override @@ -55,22 +60,23 @@ public class DefaultMethodClassFixer extends ClassVisitor { String signature, String superName, String[] interfaces) { - checkState(this.interfaces == null); + checkState(this.directInterfaces == null); isInterface = BitFlags.isSet(access, Opcodes.ACC_INTERFACE); + internalName = name; checkArgument(superName != null || "java/lang/Object".equals(name), // ASM promises this "Type without superclass: %s", name); - this.interfaces = ImmutableList.copyOf(interfaces); + this.directInterfaces = ImmutableList.copyOf(interfaces); this.superName = superName; super.visit(version, access, name, signature, superName, interfaces); } @Override public void visitEnd() { - if (!isInterface && defaultMethodsDefined(interfaces)) { + if (!isInterface && 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(); - stubMissingDefaultMethods(interfaces); + stubMissingDefaultMethods(); } super.visitEnd(); } @@ -85,6 +91,51 @@ public class DefaultMethodClassFixer extends ClassVisitor { return super.visitMethod(access, name, desc, signature, exceptions); } + private void stubMissingDefaultMethods() { + TreeSet<Class<?>> allInterfaces = new TreeSet<>(InterfaceComparator.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 + // it's convenient to rely on Class.isAssignableFrom to compute subtype relationships, and + // we'd still have to insist that all transitively implemented interfaces can be loaded. + // We don't load the visited class, however, in case it's a generated lambda class. + Class<?> itf = loadFromInternal(direct); + collectInterfaces(itf, allInterfaces); + } + + Class<?> superclass = loadFromInternal(superName); + 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. + continue; + } + stubMissingDefaultMethods(interfaceToVisit.getName().replace('.', '/')); + } + } + + private Class<?> loadFromInternal(String internalName) { + try { + return targetLoader.loadClass(internalName.replace('/', '.')); + } catch (ClassNotFoundException e) { + throw new IllegalStateException( + "Couldn't load " + internalName + ", is the classpath complete?", e); + } + } + + private void collectInterfaces(Class<?> itf, Set<Class<?>> dest) { + checkArgument(itf.isInterface()); + if (!dest.add(itf)) { + return; + } + for (Class<?> implemented : itf.getInterfaces()) { + collectInterfaces(implemented, dest); + } + } + private void recordInheritedMethods() { InstanceMethodRecorder recorder = new InstanceMethodRecorder(); String internalName = superName; @@ -132,29 +183,36 @@ public class DefaultMethodClassFixer extends ClassVisitor { return false; } - private void stubMissingDefaultMethods(ImmutableList<String> interfaces) { - for (String implemented : interfaces) { - if (!seenInterfaces.add(implemented)) { - // Skip: a superclass already implements this interface, or we've seen it here - continue; - } - ClassReader bytecode = classpath.readIfKnown(implemented); - if (bytecode != null && !bootclasspath.isKnown(implemented)) { - // 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. - bytecode.accept(new DefaultMethodStubber(), ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG); - } + /** + * Returns {@code true} for non-bridge default methods not in {@link #instanceMethods}. + */ + private boolean shouldStub(int access, String name, String desc) { + // Ignore private methods, which technically aren't default methods and can only be called from + // other methods defined in the interface. This also ignores lambda body methods, which is fine + // as we don't want or need to stub those. Also ignore bridge methods as javac adds them to + // concrete classes as needed anyway and we handle them separately for generated lambda classes. + return BitFlags.noneSet(access, + Opcodes.ACC_ABSTRACT | Opcodes.ACC_STATIC | Opcodes.ACC_BRIDGE | Opcodes.ACC_PRIVATE) + && !instanceMethods.contains(name + ":" + desc); + } + + private void stubMissingDefaultMethods(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); + // We can skip code attributes as we just need to find default methods to stub. + bytecode.accept(new DefaultMethodStubber(), ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG); } /** * Visitor for interfaces that produces delegates in the class visited by the outer * {@link DefaultMethodClassFixer} for every default method encountered. */ - public class DefaultMethodStubber extends ClassVisitor { + private class DefaultMethodStubber extends ClassVisitor { - @SuppressWarnings("hiding") private ImmutableList<String> interfaces; private String interfaceName; public DefaultMethodStubber() { @@ -170,25 +228,23 @@ public class DefaultMethodClassFixer extends ClassVisitor { String superName, String[] interfaces) { checkArgument(BitFlags.isSet(access, Opcodes.ACC_INTERFACE)); - checkState(this.interfaces == null); - this.interfaces = ImmutableList.copyOf(interfaces); + checkState(interfaceName == null); interfaceName = name; } @Override - public void visitEnd() { - stubMissingDefaultMethods(this.interfaces); - } - - @Override public MethodVisitor visitMethod( int access, String name, String desc, String signature, String[] exceptions) { - if (BitFlags.noneSet(access, Opcodes.ACC_ABSTRACT | Opcodes.ACC_STATIC | Opcodes.ACC_BRIDGE) - && !instanceMethods.contains(name + ":" + desc)) { + if (shouldStub(access, name, desc)) { + // Remember we stubbed this method in case it's also defined by subsequently visited + // interfaces. javac would force the method to be defined explicitly if there any two + // definitions conflict, but see stubMissingDefaultMethods() for how we deal with default + // methods redefined in interfaces extending another. + recordIfInstanceMethod(access, name, desc); + // Add this method to the class we're desugaring and stub in a body to call the default // implementation in the interface's companion class. ijar omits these methods when setting - // ACC_SYNTHETIC modifier, so don't. Don't do this for bridge methods, which we handle - // separately. + // ACC_SYNTHETIC modifier, so don't. // Signatures can be wrong, e.g., when type variables are introduced, instantiated, or // refined in the class we're processing, so drop them. MethodVisitor stubMethod = @@ -219,7 +275,7 @@ public class DefaultMethodClassFixer extends ClassVisitor { /** * Visitor for interfaces that recursively searches interfaces for default method declarations. */ - public class DefaultMethodFinder extends ClassVisitor { + private class DefaultMethodFinder extends ClassVisitor { @SuppressWarnings("hiding") private ImmutableList<String> interfaces; private boolean found; @@ -255,8 +311,7 @@ public class DefaultMethodClassFixer extends ClassVisitor { @Override public MethodVisitor visitMethod( int access, String name, String desc, String signature, String[] exceptions) { - if (BitFlags.noneSet(access, Opcodes.ACC_ABSTRACT | Opcodes.ACC_STATIC | Opcodes.ACC_BRIDGE) - && !instanceMethods.contains(name + ":" + desc)) { + if (!found && shouldStub(access, name, desc)) { // Found a default method we're not ignoring (instanceMethods at this point contains methods // the top-level visited class implements itself). found = true; @@ -280,13 +335,6 @@ public class DefaultMethodClassFixer extends ClassVisitor { String superName, String[] interfaces) { checkArgument(BitFlags.noneSet(access, Opcodes.ACC_INTERFACE)); - for (String inheritedInterface : interfaces) { - // No point copying default methods that we'll also copy for a superclass. Note we may - // be processing a class in the bootclasspath, in which case the interfaces must also - // be in the bootclasspath and we can skip those as well. Also note this is best-effort, - // since these interfaces may extend other interfaces that we're not recording here. - seenInterfaces.add(inheritedInterface); - } super.visit(version, access, name, signature, superName, interfaces); } @@ -297,4 +345,26 @@ public class DefaultMethodClassFixer extends ClassVisitor { return null; } } + + /** Comparator for interfaces that compares by whether interfaces extend one another. */ + enum InterfaceComparator implements Comparator<Class<?>> { + INSTANCE; + + @Override + public int compare(Class<?> o1, Class<?> o2) { + checkArgument(o1.isInterface()); + checkArgument(o2.isInterface()); + if (o1 == o2) { + return 0; + } + if (o1.isAssignableFrom(o2)) { // o1 is supertype of o2 + return 1; // we want o1 to come after o2 + } + if (o2.isAssignableFrom(o1)) { // o2 is supertype of o1 + return -1; // we want o2 to come after o1 + } + // o1 and o2 aren't comparable so arbitrarily impose lexicographical ordering + return o1.getName().compareTo(o2.getName()); + } + } } diff --git a/java/com/google/devtools/build/android/desugar/Desugar.java b/java/com/google/devtools/build/android/desugar/Desugar.java index c4528ae..b4b4c8e 100644 --- a/java/com/google/devtools/build/android/desugar/Desugar.java +++ b/java/com/google/devtools/build/android/desugar/Desugar.java @@ -42,9 +42,11 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import javax.annotation.Nullable; import org.objectweb.asm.ClassReader; @@ -171,6 +173,16 @@ class Desugar { public boolean desugarTryWithResourcesIfNeeded; @Option( + name = "desugar_try_with_resources_omit_runtime_classes", + defaultValue = "false", + category = "misc", + help = + "Omits the runtime classes necessary to support try-with-resources from the output. " + + "This property has effect only if --desugar_try_with_resources_if_needed is used." + ) + public boolean desugarTryWithResourcesOmitRuntimeClasses; + + @Option( name = "copy_bridges_from_classpath", defaultValue = "false", category = "misc", @@ -193,6 +205,7 @@ class Desugar { private final CoreLibraryRewriter rewriter; private final LambdaClassMaker lambdas; private final GeneratedClassStore store; + 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(); @@ -306,7 +319,9 @@ class Desugar { } private void copyThrowableExtensionClass(OutputFileProvider outputFileProvider) { - if (!outputJava7 || !options.desugarTryWithResourcesIfNeeded) { + if (!outputJava7 + || !options.desugarTryWithResourcesIfNeeded + || options.desugarTryWithResourcesOmitRuntimeClasses) { // try-with-resources statements are okay in the output jar. return; } @@ -441,10 +456,13 @@ class Desugar { // null ClassReaderFactory b/c we don't expect to need it for lambda classes visitor = new Java7Compatibility(visitor, (ClassReaderFactory) null); if (options.desugarTryWithResourcesIfNeeded) { - visitor = new TryWithResourcesRewriter(visitor, loader, numOfTryWithResourcesInvoked); + visitor = + new TryWithResourcesRewriter( + visitor, loader, visitedExceptionTypes, numOfTryWithResourcesInvoked); } if (options.desugarInterfaceMethodBodiesIfNeeded) { - visitor = new DefaultMethodClassFixer(visitor, classpathReader, bootclasspathReader); + visitor = + new DefaultMethodClassFixer(visitor, classpathReader, bootclasspathReader, loader); visitor = new InterfaceDesugaring(visitor, bootclasspathReader, store); } } @@ -487,10 +505,13 @@ class Desugar { if (outputJava7) { visitor = new Java7Compatibility(visitor, classpathReader); if (options.desugarTryWithResourcesIfNeeded) { - visitor = new TryWithResourcesRewriter(visitor, loader, numOfTryWithResourcesInvoked); + visitor = + new TryWithResourcesRewriter( + visitor, loader, visitedExceptionTypes, numOfTryWithResourcesInvoked); } if (options.desugarInterfaceMethodBodiesIfNeeded) { - visitor = new DefaultMethodClassFixer(visitor, classpathReader, bootclasspathReader); + visitor = + new DefaultMethodClassFixer(visitor, classpathReader, bootclasspathReader, loader); visitor = new InterfaceDesugaring(visitor, bootclasspathReader, store); } } @@ -522,15 +543,19 @@ class Desugar { * LambdaClassMaker generates lambda classes for us, but it does so by essentially simulating the * call to LambdaMetafactory that the JVM would make when encountering an invokedynamic. * LambdaMetafactory is in the JDK and its implementation has a property to write out ("dump") - * generated classes, which we take advantage of here. Set property before doing anything else - * since the property is read in the static initializer; if this breaks we can investigate setting - * the property when calling the tool. + * generated classes, which we take advantage of here. This property can be set externally, and in + * that case the specified directory is used as a temporary dir. Otherwise, it will be set here, + * before doing anything else since the property is read in the static initializer. */ private static Path createAndRegisterLambdaDumpDirectory() throws IOException { + String propertyValue = System.getProperty(LambdaClassMaker.LAMBDA_METAFACTORY_DUMPER_PROPERTY); + if (propertyValue != null) { + return Paths.get(propertyValue); + } + Path dumpDirectory = Files.createTempDirectory("lambdas"); System.setProperty( LambdaClassMaker.LAMBDA_METAFACTORY_DUMPER_PROPERTY, dumpDirectory.toString()); - deleteTreeOnExit(dumpDirectory); return dumpDirectory; } diff --git a/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java b/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java index 2429d2f..38255a1 100644 --- a/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java +++ b/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java @@ -13,6 +13,7 @@ // limitations under the License. package com.google.devtools.build.android.desugar; +import static com.google.common.base.Preconditions.checkNotNull; import static org.objectweb.asm.Opcodes.ASM5; import static org.objectweb.asm.Opcodes.INVOKESTATIC; import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL; @@ -22,8 +23,11 @@ import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSet; +import java.util.Collections; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; /** @@ -83,34 +87,69 @@ public class TryWithResourcesRewriter extends ClassVisitor { .build(); private final ClassLoader classLoader; - + private final Set<String> visitedExceptionTypes; private final AtomicInteger numOfTryWithResourcesInvoked; + /** + * Indicate whether the current class being desugared should be ignored. If the current class is + * one of the runtime extension classes, then it should be ignored. + */ + private boolean shouldCurrentClassBeIgnored; public TryWithResourcesRewriter( ClassVisitor classVisitor, ClassLoader classLoader, + Set<String> visitedExceptionTypes, AtomicInteger numOfTryWithResourcesInvoked) { super(ASM5, classVisitor); this.classLoader = classLoader; + this.visitedExceptionTypes = visitedExceptionTypes; this.numOfTryWithResourcesInvoked = numOfTryWithResourcesInvoked; } @Override + public void visit( + int version, + int access, + String name, + String signature, + String superName, + String[] interfaces) { + super.visit(version, access, name, signature, superName, interfaces); + shouldCurrentClassBeIgnored = THROWABLE_EXT_CLASS_INTERNAL_NAMES.contains(name); + } + + @Override public MethodVisitor visitMethod( int access, String name, String desc, String signature, String[] exceptions) { + if (exceptions != null && exceptions.length > 0) { + // collect exception types. + Collections.addAll(visitedExceptionTypes, exceptions); + } MethodVisitor visitor = super.cv.visitMethod(access, name, desc, signature, exceptions); - return visitor == null || THROWABLE_EXT_CLASS_INTERNAL_NAMES.contains(name) + return visitor == null || shouldCurrentClassBeIgnored ? visitor - : new TryWithResourceVisitor(visitor, classLoader); + : new TryWithResourceVisitor(name + desc, visitor, classLoader); } private class TryWithResourceVisitor extends MethodVisitor { private final ClassLoader classLoader; + /** For debugging purpose. Enrich exception information. */ + private final String methodSignature; - public TryWithResourceVisitor(MethodVisitor methodVisitor, ClassLoader classLoader) { + public TryWithResourceVisitor( + String methodSignature, MethodVisitor methodVisitor, ClassLoader classLoader) { super(ASM5, methodVisitor); this.classLoader = classLoader; + this.methodSignature = methodSignature; + } + + @Override + public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { + if (type != null) { + visitedExceptionTypes.add(type); // type in a try-catch block must extend Throwable. + } + super.visitTryCatchBlock(start, end, handler, type); } @Override @@ -120,6 +159,7 @@ public class TryWithResourcesRewriter extends ClassVisitor { return; } numOfTryWithResourcesInvoked.incrementAndGet(); + visitedExceptionTypes.add(checkNotNull(owner)); // owner extends Throwable. super.visitMethodInsn( INVOKESTATIC, THROWABLE_EXTENSION_INTERNAL_NAME, name, METHOD_DESC_MAP.get(desc), false); } @@ -131,15 +171,16 @@ public class TryWithResourcesRewriter extends ClassVisitor { if (!TARGET_METHODS.containsEntry(name, desc)) { return false; } - if (owner.equals("java/lang/Throwable")) { - return true; // early return, for performance. + if (visitedExceptionTypes.contains(owner)) { + return true; // The owner is an exception that has been visited before. } try { Class<?> throwableClass = classLoader.loadClass("java.lang.Throwable"); Class<?> klass = classLoader.loadClass(owner.replace('/', '.')); return throwableClass.isAssignableFrom(klass); } catch (ClassNotFoundException e) { - throw new AssertionError(e); + throw new AssertionError( + "Failed to load class when desugaring method " + methodSignature, e); } } } diff --git a/java/com/google/devtools/common/options/OptionsParser.java b/java/com/google/devtools/common/options/OptionsParser.java index c6bd002..728c490 100644 --- a/java/com/google/devtools/common/options/OptionsParser.java +++ b/java/com/google/devtools/common/options/OptionsParser.java @@ -553,15 +553,14 @@ public class OptionsParser implements OptionsProvider { for (Field optionField : allFields) { Option option = optionField.getAnnotation(Option.class); String category = option.category(); - if (!category.equals(prevCategory)) { - prevCategory = category; + if (!category.equals(prevCategory) + && option.optionUsageRestrictions() == OptionUsageRestrictions.DOCUMENTED) { String description = categoryDescriptions.get(category); if (description == null) { description = "Options category '" + category + "'"; } - if (option.optionUsageRestrictions() == OptionUsageRestrictions.DOCUMENTED) { - desc.append("\n").append(description).append(":\n"); - } + desc.append("\n").append(description).append(":\n"); + prevCategory = category; } if (option.optionUsageRestrictions() == OptionUsageRestrictions.DOCUMENTED) { |