aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
authorGravatar kmb <kmb@google.com>2017-04-20 20:35:04 +0200
committerGravatar Vladimir Moskva <vladmos@google.com>2017-04-24 16:49:35 +0200
commitebd299084e0e4e37fef104619361c8e3c5ef8557 (patch)
tree7fd9b4e0e57702c313f96c1b8d368e667f74dd76 /src
parentd73a88bb87db1aba2f91920c74dbc7f0409438bb (diff)
Default and static interface desugaring
RELNOTES: n/a PiperOrigin-RevId: 153735445
Diffstat (limited to 'src')
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java10
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java225
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/desugar/Desugar.java148
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/desugar/GeneratedClassStore.java49
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java312
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/desugar/Java7Compatibility.java2
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java15
7 files changed, 716 insertions, 45 deletions
diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java b/src/tools/android/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java
index 2b44d7641a..bae5251d0b 100644
--- a/src/tools/android/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java
+++ b/src/tools/android/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java
@@ -28,7 +28,7 @@ class ClassReaderFactory {
}
/**
- * Returns a reader for the given/internal/Class$Name if the class is defined in the wrapped Jar
+ * Returns a reader for the given/internal/Class$Name if the class is defined in the wrapped input
* and {@code null} otherwise. For simplicity this method turns checked into runtime exceptions
* under the assumption that all classes have already been read once when this method is called.
*/
@@ -50,4 +50,12 @@ class ClassReaderFactory {
return null;
}
+
+ /**
+ * Returns {@code true} if the given given/internal/Class$Name is defined in the wrapped input.
+ */
+ public boolean isKnown(String internalClassName) {
+ String filename = rewriter.unprefix(internalClassName) + ".class";
+ return indexedInputs.getInputFileProvider(filename) != null;
+ }
}
diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java b/src/tools/android/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java
new file mode 100644
index 0000000000..a9e86a1575
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java
@@ -0,0 +1,225 @@
+// Copyright 2017 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.collect.ImmutableList;
+import java.util.HashSet;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+/**
+ * Fixer of classes that extend interfaces with default methods to declare any missing methods
+ * explicitly and call the corresponding companion method generated by {@link InterfaceDesugaring}.
+ */
+public class DefaultMethodClassFixer extends ClassVisitor {
+
+ private final ClassReaderFactory classpath;
+ private final ClassReaderFactory bootclasspath;
+ private final HashSet<String> instanceMethods = new HashSet<>();
+ private final HashSet<String> seenInterfaces = new HashSet<>();
+
+ private boolean isInterface;
+ private ImmutableList<String> interfaces;
+ private String superName;
+
+ public DefaultMethodClassFixer(ClassVisitor dest, ClassReaderFactory classpath,
+ ClassReaderFactory bootclasspath) {
+ super(Opcodes.ASM5, dest);
+ this.classpath = classpath;
+ this.bootclasspath = bootclasspath;
+ }
+
+ @Override
+ public void visit(
+ int version,
+ int access,
+ String name,
+ String signature,
+ String superName,
+ String[] interfaces) {
+ checkState(this.interfaces == null);
+ isInterface = BitFlags.isSet(access, Opcodes.ACC_INTERFACE);
+ checkArgument(superName != null || "java/lang/Object".equals(name), // ASM promises this
+ "Type without superclass: %s", name);
+ this.interfaces = ImmutableList.copyOf(interfaces);
+ this.superName = superName;
+ super.visit(version, access, name, signature, superName, interfaces);
+ }
+
+ @Override
+ public void visitEnd() {
+ if (!isInterface && !interfaces.isEmpty()) {
+ // Inherited methods take precedence over default methods, so visit all superclasses and
+ // figure out what methods they declare before stubbing in any missing default methods.
+ recordInheritedMethods();
+ stubMissingDefaultMethods(interfaces);
+ }
+ super.visitEnd();
+ }
+
+ @Override
+ public MethodVisitor visitMethod(
+ int access, String name, String desc, String signature, String[] exceptions) {
+ // Keep track of instance methods implemented in this class for later.
+ if (!isInterface && !interfaces.isEmpty()) {
+ recordIfInstanceMethod(access, name, desc);
+ }
+ return super.visitMethod(access, name, desc, signature, exceptions);
+ }
+
+ private void recordInheritedMethods() {
+ InstanceMethodRecorder recorder = new InstanceMethodRecorder();
+ String internalName = superName;
+ while (internalName != null) {
+ ClassReader bytecode = bootclasspath.readIfKnown(internalName);
+ if (bytecode == null) {
+ bytecode = checkNotNull(classpath.readIfKnown(internalName), "Not found: %s", internalName);
+ }
+ bytecode.accept(recorder, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG);
+ internalName = bytecode.getSuperName();
+ }
+ }
+
+ private void recordIfInstanceMethod(int access, String name, String desc) {
+ if (BitFlags.noneSet(access, Opcodes.ACC_STATIC)) {
+ // Record all declared instance methods, including abstract, bridge, and native methods, as
+ // they all take precedence over default methods.
+ instanceMethods.add(name + ":" + desc);
+ }
+ }
+
+ 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);
+ }
+ }
+ }
+
+ /**
+ * 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 {
+
+ @SuppressWarnings("hiding") private ImmutableList<String> interfaces;
+ private String interfaceName;
+
+ public DefaultMethodStubber() {
+ super(Opcodes.ASM5);
+ }
+
+ @Override
+ public void visit(
+ int version,
+ int access,
+ String name,
+ String signature,
+ String superName,
+ String[] interfaces) {
+ checkArgument(BitFlags.isSet(access, Opcodes.ACC_INTERFACE));
+ checkState(this.interfaces == null);
+ this.interfaces = ImmutableList.copyOf(interfaces);
+ 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.add(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.
+ // Signatures can be wrong, e.g., when type variables are introduced, instantiated, or
+ // refined in the class we're processing, so drop them.
+ MethodVisitor stubMethod =
+ DefaultMethodClassFixer.this.visitMethod(access, name, desc, (String) null, exceptions);
+
+ int slot = 0;
+ stubMethod.visitVarInsn(Opcodes.ALOAD, slot++); // load the receiver
+ Type neededType = Type.getMethodType(desc);
+ for (Type arg : neededType.getArgumentTypes()) {
+ stubMethod.visitVarInsn(arg.getOpcode(Opcodes.ILOAD), slot);
+ slot += arg.getSize();
+ }
+ stubMethod.visitMethodInsn(
+ Opcodes.INVOKESTATIC,
+ interfaceName + InterfaceDesugaring.COMPANION_SUFFIX,
+ name,
+ InterfaceDesugaring.companionDefaultMethodDescriptor(interfaceName, 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; // we don't care about the actual code in these methods
+ }
+ }
+
+ private class InstanceMethodRecorder extends ClassVisitor {
+
+ public InstanceMethodRecorder() {
+ super(Opcodes.ASM5);
+ }
+
+ @Override
+ public void visit(
+ int version,
+ int access,
+ String name,
+ String signature,
+ 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);
+ }
+
+ @Override
+ public MethodVisitor visitMethod(
+ int access, String name, String desc, String signature, String[] exceptions) {
+ recordIfInstanceMethod(access, name, desc);
+ return null;
+ }
+ }
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/Desugar.java b/src/tools/android/java/com/google/devtools/build/android/desugar/Desugar.java
index 797cbc41be..5e24b3560f 100644
--- a/src/tools/android/java/com/google/devtools/build/android/desugar/Desugar.java
+++ b/src/tools/android/java/com/google/devtools/build/android/desugar/Desugar.java
@@ -14,6 +14,7 @@
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.nio.charset.StandardCharsets.ISO_8859_1;
@@ -42,9 +43,11 @@ import java.nio.file.attribute.BasicFileAttributes;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import javax.annotation.Nullable;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.tree.ClassNode;
/**
* Command-line tool to desugar Java 8 constructs that dx doesn't know what to do with, in
@@ -146,6 +149,16 @@ class Desugar {
public int minSdkVersion;
@Option(
+ name = "desugar_interface_method_bodies_if_needed",
+ defaultValue = "false",
+ category = "misc",
+ help = "Rewrites default and static methods in interfaces if --min_sdk_version < 24. This "
+ + "only works correctly if subclasses of rewritten interfaces as well as uses of static "
+ + "interface methods are run through this tool as well."
+ )
+ public boolean desugarInterfaceMethodBodiesIfNeeded;
+
+ @Option(
name = "copy_bridges_from_classpath",
defaultValue = "false",
category = "misc",
@@ -167,8 +180,11 @@ class Desugar {
private final Path dumpDirectory;
private final CoreLibraryRewriter rewriter;
private final LambdaClassMaker lambdas;
+ private final GeneratedClassStore store;
+ private final boolean outputJava7;
private final boolean allowDefaultMethods;
private final boolean allowCallsToObjectsNonNull;
+
/** An instance of Desugar is expected to be used ONLY ONCE */
private boolean used;
@@ -177,7 +193,10 @@ class Desugar {
this.dumpDirectory = dumpDirectory;
this.rewriter = new CoreLibraryRewriter(options.coreLibrary ? "__desugar__/" : "");
this.lambdas = new LambdaClassMaker(dumpDirectory);
- this.allowDefaultMethods = options.minSdkVersion >= 24;
+ this.store = new GeneratedClassStore();
+ this.outputJava7 = options.minSdkVersion < 24;
+ this.allowDefaultMethods =
+ options.desugarInterfaceMethodBodiesIfNeeded || options.minSdkVersion >= 24;
this.allowCallsToObjectsNonNull = options.minSdkVersion >= 19;
this.used = false;
}
@@ -187,28 +206,30 @@ class Desugar {
this.used = true;
try (Closer closer = Closer.create()) {
- IndexedInputs indexedClasspath =
- new IndexedInputs(toRegisteredInputFileProvider(closer, options.classpath));
+ IndexedInputs indexedBootclasspath =
+ new IndexedInputs(toRegisteredInputFileProvider(closer, options.bootclasspath));
// Use a classloader that as much as possible uses the provided bootclasspath instead of
// the tool's system classloader. Unfortunately we can't do that for java. classes.
ClassLoader bootclassloader =
options.bootclasspath.isEmpty()
? new ThrowingClassLoader()
: new HeaderClassLoader(
- new IndexedInputs(toRegisteredInputFileProvider(closer, options.bootclasspath)),
+ indexedBootclasspath,
rewriter,
new ThrowingClassLoader());
+ IndexedInputs indexedClasspath =
+ new IndexedInputs(toRegisteredInputFileProvider(closer, options.classpath));
// Process each input separately
for (InputOutputPair inputOutputPair : toInputOutputPairs(options)) {
- desugarOneInput(inputOutputPair, indexedClasspath, bootclassloader);
+ desugarOneInput(inputOutputPair, indexedClasspath, bootclassloader,
+ new ClassReaderFactory(indexedBootclasspath, rewriter));
}
}
}
- private void desugarOneInput(
- InputOutputPair inputOutputPair, IndexedInputs indexedClasspath, ClassLoader bootclassloader)
- throws Exception {
+ private void desugarOneInput(InputOutputPair inputOutputPair, IndexedInputs indexedClasspath,
+ ClassLoader bootclassloader, ClassReaderFactory bootclasspathReader) throws Exception {
Path inputPath = inputOutputPair.getInput();
Path outputPath = inputOutputPair.getOutput();
checkArgument(
@@ -227,24 +248,32 @@ class Desugar {
ClassLoader loader =
new HeaderClassLoader(indexedClasspathAndInputFiles, rewriter, bootclassloader);
- ClassReaderFactory readerFactory =
- new ClassReaderFactory(
- (options.copyBridgesFromClasspath && !allowDefaultMethods)
- ? indexedClasspathAndInputFiles
- : indexedInputFiles,
- rewriter);
+ ClassReaderFactory classpathReader = null;
+ ClassReaderFactory bridgeMethodReader = null;
+ if (outputJava7) {
+ classpathReader = new ClassReaderFactory(indexedClasspathAndInputFiles, rewriter);
+ if (options.copyBridgesFromClasspath) {
+ bridgeMethodReader = classpathReader;
+ } else {
+ bridgeMethodReader = new ClassReaderFactory(indexedInputFiles, rewriter);
+ }
+ }
ImmutableSet.Builder<String> interfaceLambdaMethodCollector = ImmutableSet.builder();
- desugarClassesInInput(
- inputFiles, outputFileProvider, loader, readerFactory, interfaceLambdaMethodCollector);
+ desugarClassesInInput(inputFiles, outputFileProvider, loader, classpathReader,
+ bootclasspathReader, interfaceLambdaMethodCollector);
+
+ desugarAndWriteDumpedLambdaClassesToOutput(outputFileProvider, loader, classpathReader,
+ bootclasspathReader, interfaceLambdaMethodCollector.build(), bridgeMethodReader);
- desugarAndWriteDumpedLambdaClassesToOutput(
- outputFileProvider, loader, readerFactory, interfaceLambdaMethodCollector);
+ desugarAndWriteGeneratedClasses(outputFileProvider);
}
- ImmutableMap<Path, LambdaInfo> leftBehind = lambdas.drain();
- checkState(leftBehind.isEmpty(), "Didn't process %s", leftBehind);
+ ImmutableMap<Path, LambdaInfo> lambdasLeftBehind = lambdas.drain();
+ checkState(lambdasLeftBehind.isEmpty(), "Didn't process %s", lambdasLeftBehind);
+ ImmutableMap<String, ClassNode> generatedLeftBehind = store.drain();
+ checkState(generatedLeftBehind.isEmpty(), "Didn't process %s", generatedLeftBehind.keySet());
}
/** Desugar the classes that are in the inputs specified in the command line arguments. */
@@ -252,7 +281,8 @@ class Desugar {
InputFileProvider inputFiles,
OutputFileProvider outputFileProvider,
ClassLoader loader,
- ClassReaderFactory readerFactory,
+ @Nullable ClassReaderFactory classpathReader,
+ ClassReaderFactory bootclasspathReader,
Builder<String> interfaceLambdaMethodCollector)
throws IOException {
for (String filename : inputFiles) {
@@ -262,11 +292,14 @@ class Desugar {
// any danger of accidentally uncompressed resources ending up in an .apk.
if (filename.endsWith(".class")) {
ClassReader reader = rewriter.reader(content);
- UnprefixingClassWriter writer =
- rewriter.writer(ClassWriter.COMPUTE_MAXS /*for bridge methods*/);
+ UnprefixingClassWriter writer = rewriter.writer(ClassWriter.COMPUTE_MAXS);
ClassVisitor visitor =
createClassVisitorsForClassesInInputs(
- loader, readerFactory, interfaceLambdaMethodCollector, writer);
+ loader,
+ classpathReader,
+ bootclasspathReader,
+ interfaceLambdaMethodCollector,
+ writer);
reader.accept(visitor, 0);
outputFileProvider.write(filename, writer.toByteArray());
@@ -284,10 +317,11 @@ class Desugar {
private void desugarAndWriteDumpedLambdaClassesToOutput(
OutputFileProvider outputFileProvider,
ClassLoader loader,
- ClassReaderFactory readerFactory,
- Builder<String> interfaceLambdaMethodCollector)
+ @Nullable ClassReaderFactory classpathReader,
+ ClassReaderFactory bootclasspathReader,
+ ImmutableSet<String> interfaceLambdaMethods,
+ @Nullable ClassReaderFactory bridgeMethodReader)
throws IOException {
- ImmutableSet<String> interfaceLambdaMethods = interfaceLambdaMethodCollector.build();
checkState(
!allowDefaultMethods || interfaceLambdaMethods.isEmpty(),
"Desugaring with default methods enabled moved interface lambdas");
@@ -307,7 +341,13 @@ class Desugar {
rewriter.writer(ClassWriter.COMPUTE_MAXS /*for invoking bridges*/);
ClassVisitor visitor =
createClassVisitorsForDumpedLambdaClasses(
- loader, readerFactory, interfaceLambdaMethods, lambdaClass.getValue(), writer);
+ loader,
+ classpathReader,
+ bootclasspathReader,
+ interfaceLambdaMethods,
+ bridgeMethodReader,
+ lambdaClass.getValue(),
+ writer);
reader.accept(visitor, 0);
String filename =
rewriter.unprefix(lambdaClass.getValue().desiredInternalName()) + ".class";
@@ -316,26 +356,53 @@ class Desugar {
}
}
+ private void desugarAndWriteGeneratedClasses(OutputFileProvider outputFileProvider)
+ throws IOException {
+ // Write out any classes we generated along the way
+ ImmutableMap<String, ClassNode> generatedClasses = store.drain();
+ checkState(generatedClasses.isEmpty() || (allowDefaultMethods && outputJava7),
+ "Didn't expect generated classes but got %s", generatedClasses.keySet());
+ for (Map.Entry<String, ClassNode> generated : generatedClasses.entrySet()) {
+ 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);
+ generated.getValue().accept(visitor);
+ String filename = rewriter.unprefix(generated.getKey()) + ".class";
+ outputFileProvider.write(filename, writer.toByteArray());
+ }
+ }
+
/**
* Create the class visitors for the lambda classes that are generated on the fly. If no new class
* visitors are not generated, then the passed-in {@code writer} will be returned.
*/
private ClassVisitor createClassVisitorsForDumpedLambdaClasses(
ClassLoader loader,
- ClassReaderFactory readerFactory,
+ @Nullable ClassReaderFactory classpathReader,
+ ClassReaderFactory bootclasspathReader,
ImmutableSet<String> interfaceLambdaMethods,
+ @Nullable ClassReaderFactory bridgeMethodReader,
LambdaInfo lambdaClass,
UnprefixingClassWriter writer) {
- ClassVisitor visitor = writer;
+ ClassVisitor visitor = checkNotNull(writer);
- if (!allowDefaultMethods) {
+ if (outputJava7) {
// null ClassReaderFactory b/c we don't expect to need it for lambda classes
visitor = new Java7Compatibility(visitor, (ClassReaderFactory) null);
+ if (options.desugarInterfaceMethodBodiesIfNeeded) {
+ visitor = new DefaultMethodClassFixer(visitor, classpathReader, bootclasspathReader);
+ visitor = new InterfaceDesugaring(visitor, bootclasspathReader, store);
+ }
}
-
visitor =
new LambdaClassFixer(
- visitor, lambdaClass, readerFactory, interfaceLambdaMethods, allowDefaultMethods);
+ visitor,
+ lambdaClass,
+ bridgeMethodReader,
+ interfaceLambdaMethods,
+ allowDefaultMethods,
+ outputJava7);
// Send lambda classes through desugaring to make sure there's no invokedynamic
// instructions in generated lambda classes (checkState below will fail)
visitor = new LambdaDesugaring(visitor, loader, lambdas, null, allowDefaultMethods);
@@ -357,17 +424,20 @@ class Desugar {
*/
private ClassVisitor createClassVisitorsForClassesInInputs(
ClassLoader loader,
- ClassReaderFactory readerFactory,
+ @Nullable ClassReaderFactory classpathReader,
+ ClassReaderFactory bootclasspathReader,
Builder<String> interfaceLambdaMethodCollector,
UnprefixingClassWriter writer) {
- checkArgument(writer != null, "The class writer cannot be null");
- ClassVisitor visitor = writer;
+ ClassVisitor visitor = checkNotNull(writer);
if (!options.onlyDesugarJavac9ForLint) {
- if (!allowDefaultMethods) {
- visitor = new Java7Compatibility(visitor, readerFactory);
+ if (outputJava7) {
+ visitor = new Java7Compatibility(visitor, classpathReader);
+ if (options.desugarInterfaceMethodBodiesIfNeeded) {
+ visitor = new DefaultMethodClassFixer(visitor, classpathReader, bootclasspathReader);
+ visitor = new InterfaceDesugaring(visitor, bootclasspathReader, store);
+ }
}
-
visitor =
new LambdaDesugaring(
visitor, loader, lambdas, interfaceLambdaMethodCollector, allowDefaultMethods);
diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/GeneratedClassStore.java b/src/tools/android/java/com/google/devtools/build/android/desugar/GeneratedClassStore.java
new file mode 100644
index 0000000000..bf376a45ba
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/desugar/GeneratedClassStore.java
@@ -0,0 +1,49 @@
+// Copyright 2017 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.tree.ClassNode;
+
+/**
+ * Simple wrapper around a map that holds generated classes so they can be processed later.
+ */
+class GeneratedClassStore {
+
+ /** Map from internal names to generated classes with deterministic iteration order. */
+ private final Map<String, ClassNode> classes = new LinkedHashMap<>();
+
+ /**
+ * Adds a class for the given internal name. It's the caller's responsibility to {@link
+ * ClassVisitor#visit} the returned object to initialize the desired class, and to avoid
+ * confusion, this method throws if the class had already been present.
+ */
+ public ClassVisitor add(String internalClassName) {
+ ClassNode result = new ClassNode();
+ checkState(
+ classes.put(internalClassName, result) == null, "Already present: %s", internalClassName);
+ return result;
+ }
+
+ public ImmutableMap<String, ClassNode> drain() {
+ ImmutableMap<String, ClassNode> result = ImmutableMap.copyOf(classes);
+ classes.clear();
+ return result;
+ }
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java b/src/tools/android/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java
new file mode 100644
index 0000000000..b2cf540646
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java
@@ -0,0 +1,312 @@
+// Copyright 2017 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+
+import javax.annotation.Nullable;
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+import org.objectweb.asm.TypePath;
+
+/**
+ * Visitor that moves methods with bodies from interfaces into a companion class and rewrites
+ * call sites accordingly (which is only needed for static interface methods). Default methods
+ * are kept as abstract methods with all their annotations.
+ *
+ * <p>Any necessary companion classes will be added to the given {@link GeneratedClassStore}. It's
+ * the caller's responsibility to write those out.
+ *
+ * <p>Relies on {@link DefaultMethodClassFixer} to stub in method bodies for moved default methods.
+ * Assumes that lambdas are already desugared. Ignores bridge methods, which are handled specially.
+ */
+class InterfaceDesugaring extends ClassVisitor {
+
+ static final String COMPANION_SUFFIX = "$$CC";
+
+ private final ClassReaderFactory bootclasspath;
+ private final GeneratedClassStore store;
+
+ private String internalName;
+ private int bytecodeVersion;
+ private int accessFlags;
+ @Nullable private ClassVisitor companion;
+
+ public InterfaceDesugaring(ClassVisitor dest, ClassReaderFactory bootclasspath,
+ GeneratedClassStore store) {
+ super(Opcodes.ASM5, dest);
+ this.bootclasspath = bootclasspath;
+ this.store = store;
+ }
+
+ @Override
+ public void visit(
+ int version,
+ int access,
+ String name,
+ String signature,
+ String superName,
+ String[] interfaces) {
+ companion = null;
+ internalName = name;
+ bytecodeVersion = version;
+ accessFlags = access;
+ super.visit(version, access, name, signature, superName, interfaces);
+ }
+
+ @Override
+ public void visitEnd() {
+ if (companion != null) {
+ companion.visitEnd();
+ }
+ super.visitEnd();
+ }
+
+ @Override
+ public MethodVisitor visitMethod(
+ int access, String name, String desc, String signature, String[] exceptions) {
+ MethodVisitor result;
+ if (BitFlags.isSet(accessFlags, Opcodes.ACC_INTERFACE)
+ && BitFlags.noneSet(access, Opcodes.ACC_ABSTRACT | Opcodes.ACC_BRIDGE)
+ && !"<clinit>".equals(name)) {
+ checkArgument(BitFlags.noneSet(access, Opcodes.ACC_NATIVE), "Forbidden per JLS ch 9.4");
+
+ boolean isLambdaBody =
+ name.startsWith("lambda$") && BitFlags.isSet(access, Opcodes.ACC_SYNTHETIC);
+ if (isLambdaBody) {
+ access &= ~Opcodes.ACC_PUBLIC; // undo visibility change from LambdaDesugaring
+ // Rename lambda method to reflect the new owner. Not doing so confuses LambdaDesugaring
+ // if it's run over this class again.
+ name += COMPANION_SUFFIX;
+ }
+ if (BitFlags.isSet(access, Opcodes.ACC_STATIC)) {
+ // Completely move static interface methods, which requires rewriting call sites
+ result =
+ companion()
+ .visitMethod(access & ~Opcodes.ACC_PRIVATE, name, desc, signature, exceptions);
+ } else {
+ MethodVisitor abstractDest;
+ if (isLambdaBody) {
+ // Completely move lambda bodies, which requires rewriting call sites
+ access &= ~Opcodes.ACC_PRIVATE;
+ abstractDest = null;
+ } else {
+ // Make default methods abstract but move their implementation into a static method with
+ // corresponding signature. Doesn't require callsite rewriting but implementing classes
+ // may need to implement default methods explicitly.
+ checkArgument(BitFlags.noneSet(access, Opcodes.ACC_PRIVATE),
+ "Unexpected private interface method %s.%s : %s", name, internalName, desc);
+ abstractDest = super.visitMethod(
+ access | Opcodes.ACC_ABSTRACT, name, desc, signature, exceptions);
+ }
+
+ // TODO(b/37110951): adjust signature with explicit receiver type, which may be generic
+ MethodVisitor codeDest =
+ companion()
+ .visitMethod(
+ access | Opcodes.ACC_STATIC,
+ name,
+ companionDefaultMethodDescriptor(internalName, desc),
+ (String) null, // drop signature, since given one doesn't include the new param
+ exceptions);
+
+ result = abstractDest != null ? new MultiplexAnnotations(codeDest, abstractDest) : codeDest;
+ }
+ } else {
+ result = super.visitMethod(access, name, desc, signature, exceptions);
+ }
+ return result != null ? new InterfaceInvocationRewriter(result) : null;
+ }
+
+ /**
+ * Returns the descriptor of a static method for an instance method with the given receiver and
+ * description, simply by pre-pending the given descriptor's parameter list with the given
+ * receiver type.
+ */
+ static String companionDefaultMethodDescriptor(String interfaceName, String desc) {
+ Type type = Type.getMethodType(desc);
+ Type[] companionArgs = new Type[type.getArgumentTypes().length + 1];
+ companionArgs[0] = Type.getObjectType(interfaceName);
+ System.arraycopy(type.getArgumentTypes(), 0, companionArgs, 1, type.getArgumentTypes().length);
+ return Type.getMethodDescriptor(type.getReturnType(), companionArgs);
+ }
+
+ private ClassVisitor companion() {
+ if (companion == null) {
+ checkState(BitFlags.isSet(accessFlags, Opcodes.ACC_INTERFACE));
+ String companionName = internalName + COMPANION_SUFFIX;
+
+ companion = store.add(companionName);
+ companion.visit(
+ bytecodeVersion,
+ (accessFlags | Opcodes.ACC_SYNTHETIC) & ~Opcodes.ACC_INTERFACE,
+ companionName,
+ (String) null, // signature
+ "java/lang/Object",
+ new String[0]);
+ }
+ return companion;
+ }
+
+ /**
+ * Rewriter for calls to static interface methods and super calls to default methods, unless
+ * they're part of the bootclasspath, as well as all lambda body methods. Keeps calls to
+ * interface methods declared in the bootclasspath as-is (but note that these would presumably
+ * fail on devices without those methods).
+ */
+ private class InterfaceInvocationRewriter extends MethodVisitor {
+
+ public InterfaceInvocationRewriter(MethodVisitor dest) {
+ super(Opcodes.ASM5, dest);
+ }
+
+ @Override
+ public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
+ // Assume that any static interface methods on the classpath are moved
+ if (itf) {
+ if (name.startsWith("lambda$")) {
+ // Redirect lambda invocations to completely remove all lambda methods from interfaces.
+ checkArgument(!owner.endsWith(COMPANION_SUFFIX),
+ "%s shouldn't consider %s an interface", internalName, owner);
+ checkArgument(!bootclasspath.isKnown(owner)); // must be in current input
+ if (opcode == Opcodes.INVOKEINTERFACE) {
+ opcode = Opcodes.INVOKESTATIC;
+ desc = companionDefaultMethodDescriptor(owner, desc);
+ } else {
+ checkArgument(opcode == Opcodes.INVOKESTATIC,
+ "Unexpected opcode %s to invoke %s.%s", opcode, owner, name);
+ }
+ // Reflect that InterfaceDesugaring moves and renames the lambda body method
+ owner += COMPANION_SUFFIX;
+ name += COMPANION_SUFFIX;
+ checkState(name.equals(LambdaDesugaring.uniqueInPackage(owner, name)),
+ "Unexpected lambda body method name %s for %s", name, owner);
+ itf = false;
+ } else if ((opcode == Opcodes.INVOKESTATIC || opcode == Opcodes.INVOKESPECIAL)
+ && !bootclasspath.isKnown(owner)) {
+ checkArgument(!owner.endsWith(COMPANION_SUFFIX),
+ "%s shouldn't consider %s an interface", internalName, owner);
+ if (opcode == Opcodes.INVOKESPECIAL) {
+ // Turn Interface.super.m() into Interface$$CC.m(receiver)
+ opcode = Opcodes.INVOKESTATIC;
+ desc = companionDefaultMethodDescriptor(owner, desc);
+ }
+ owner += COMPANION_SUFFIX;
+ itf = false;
+ }
+ }
+ super.visitMethodInsn(opcode, owner, name, desc, itf);
+ }
+ }
+
+ /**
+ * Method visitor that behaves like a passthrough but additionally duplicates all annotations
+ * into a second given {@link MethodVisitor}.
+ */
+ private static class MultiplexAnnotations extends MethodVisitor {
+
+ private final MethodVisitor annotationOnlyDest;
+
+ public MultiplexAnnotations(@Nullable MethodVisitor dest, MethodVisitor annotationOnlyDest) {
+ super(Opcodes.ASM5, dest);
+ this.annotationOnlyDest = annotationOnlyDest;
+ }
+
+ @Override
+ public void visitParameter(String name, int access) {
+ super.visitParameter(name, access);
+ annotationOnlyDest.visitParameter(name, access);
+ }
+
+ @Override
+ public AnnotationVisitor visitTypeAnnotation(
+ int typeRef, TypePath typePath, String desc, boolean visible) {
+ AnnotationVisitor dest = super.visitTypeAnnotation(typeRef, typePath, desc, visible);
+ AnnotationVisitor annoDest =
+ annotationOnlyDest.visitTypeAnnotation(typeRef, typePath, desc, visible);
+ return new MultiplexAnnotationVisitor(dest, annoDest);
+ }
+
+ @Override
+ public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) {
+ AnnotationVisitor dest = super.visitParameterAnnotation(parameter, desc, visible);
+ AnnotationVisitor annoDest =
+ annotationOnlyDest.visitParameterAnnotation(parameter, desc, visible);
+ return new MultiplexAnnotationVisitor(dest, annoDest);
+ }
+ }
+
+ /**
+ * Annotation visitor that recursively passes the visited annotations to any number of given
+ * {@link AnnotationVisitor}s.
+ */
+ private static class MultiplexAnnotationVisitor extends AnnotationVisitor {
+
+ private final AnnotationVisitor[] moreDestinations;
+
+ public MultiplexAnnotationVisitor(@Nullable AnnotationVisitor dest,
+ AnnotationVisitor... moreDestinations) {
+ super(Opcodes.ASM5, dest);
+ this.moreDestinations = moreDestinations;
+ }
+
+ @Override
+ public void visit(String name, Object value) {
+ super.visit(name, value);
+ for (AnnotationVisitor dest : moreDestinations) {
+ dest.visit(name, value);
+ }
+ }
+
+ @Override
+ public void visitEnum(String name, String desc, String value) {
+ super.visitEnum(name, desc, value);
+ for (AnnotationVisitor dest : moreDestinations) {
+ dest.visitEnum(name, desc, value);
+ }
+ }
+
+ @Override
+ public AnnotationVisitor visitAnnotation(String name, String desc) {
+ AnnotationVisitor[] subVisitors = new AnnotationVisitor[moreDestinations.length];
+ AnnotationVisitor dest = super.visitAnnotation(name, desc);
+ for (int i = 0; i < subVisitors.length; ++i) {
+ subVisitors[i] = moreDestinations[i].visitAnnotation(name, desc);
+ }
+ return new MultiplexAnnotationVisitor(dest, subVisitors);
+ }
+
+ @Override
+ public AnnotationVisitor visitArray(String name) {
+ AnnotationVisitor[] subVisitors = new AnnotationVisitor[moreDestinations.length];
+ AnnotationVisitor dest = super.visitArray(name);
+ for (int i = 0; i < subVisitors.length; ++i) {
+ subVisitors[i] = moreDestinations[i].visitArray(name);
+ }
+ return new MultiplexAnnotationVisitor(dest, subVisitors);
+ }
+
+ @Override
+ public void visitEnd() {
+ super.visitEnd();
+ for (AnnotationVisitor dest : moreDestinations) {
+ dest.visitEnd();
+ }
+ }
+ }
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/Java7Compatibility.java b/src/tools/android/java/com/google/devtools/build/android/desugar/Java7Compatibility.java
index cc3fe14ff2..55f82b3032 100644
--- a/src/tools/android/java/com/google/devtools/build/android/desugar/Java7Compatibility.java
+++ b/src/tools/android/java/com/google/devtools/build/android/desugar/Java7Compatibility.java
@@ -27,7 +27,7 @@ import org.objectweb.asm.Opcodes;
import org.objectweb.asm.TypePath;
/**
- * Visitor that ensures bytecode version <= 51 (Java 7) and that throws if it default or static
+ * Visitor that ensures bytecode version <= 51 (Java 7) and that throws if it sees default or static
* interface methods (i.e., non-abstract interface methods), which don't exist in Java 7.
*/
public class Java7Compatibility extends ClassVisitor {
diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java b/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java
index d4726d346a..5e50fc87f6 100644
--- a/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java
+++ b/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java
@@ -49,6 +49,7 @@ class LambdaClassFixer extends ClassVisitor {
private final ClassReaderFactory factory;
private final ImmutableSet<String> interfaceLambdaMethods;
private final boolean allowDefaultMethods;
+ private final boolean copyBridgeMethods;
private final HashSet<String> implementedMethods = new HashSet<>();
private final LinkedHashSet<String> methodsToMoveIn = new LinkedHashSet<>();
@@ -62,12 +63,16 @@ class LambdaClassFixer extends ClassVisitor {
private String signature;
public LambdaClassFixer(ClassVisitor dest, LambdaInfo lambdaInfo, ClassReaderFactory factory,
- ImmutableSet<String> interfaceLambdaMethods, boolean allowDefaultMethods) {
+ ImmutableSet<String> interfaceLambdaMethods, boolean allowDefaultMethods,
+ boolean copyBridgeMethods) {
super(Opcodes.ASM5, dest);
+ checkArgument(!allowDefaultMethods || interfaceLambdaMethods.isEmpty());
+ checkArgument(allowDefaultMethods || copyBridgeMethods);
this.lambdaInfo = lambdaInfo;
this.factory = factory;
this.interfaceLambdaMethods = interfaceLambdaMethods;
this.allowDefaultMethods = allowDefaultMethods;
+ this.copyBridgeMethods = copyBridgeMethods;
}
@Override
@@ -180,7 +185,7 @@ class LambdaClassFixer extends ClassVisitor {
}
copyRewrittenLambdaMethods();
- if (!allowDefaultMethods) {
+ if (copyBridgeMethods) {
copyBridgeMethods(interfaces);
}
super.visitEnd();
@@ -327,8 +332,9 @@ class LambdaClassFixer extends ClassVisitor {
// Only copy bridge methods--hand-written default methods are not supported--and only if
// we haven't seen the method already.
if (implementedMethods.add(name + ":" + desc)) {
- return new AvoidJacocoInit(
- LambdaClassFixer.super.visitMethod(access, name, desc, signature, exceptions));
+ MethodVisitor result =
+ LambdaClassFixer.super.visitMethod(access, name, desc, signature, exceptions);
+ return allowDefaultMethods ? result : new AvoidJacocoInit(result);
}
}
return null;
@@ -348,6 +354,7 @@ class LambdaClassFixer extends ClassVisitor {
public CopyOneMethod(String methodName) {
// No delegate visitor; instead we'll add methods to the outer class's delegate where needed
super(Opcodes.ASM5);
+ checkState(!allowDefaultMethods, "Couldn't copy interface lambda bodies");
this.methodName = methodName;
}