aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/tools
diff options
context:
space:
mode:
authorGravatar Adam Michael <ajmichael@google.com>2017-02-10 02:43:34 +0000
committerGravatar Kristina Chodorow <kchodorow@google.com>2017-02-10 15:35:54 +0000
commit29aa0eb17c85a96e3edae8362ba93fddeed4c1e0 (patch)
tree8be36b954b22f0babb90ed51a955427e0fe4f7bb /src/tools
parent6bb3f875b780578abff192cf6444dbdfaf85061f (diff)
Open source java 8 desugarer.
Fixes https://github.com/bazelbuild/bazel/issues/2222. RELNOTES: Support for Java 8 lambdas, method references, type annotations and repeated annotations in Android builds with --experimental_desugar_for_android. -- PiperOrigin-RevId: 147109786 MOS_MIGRATED_REVID=147109786
Diffstat (limited to 'src/tools')
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/BUILD2
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/desugar/BUILD38
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/desugar/BUILD.tools7
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/desugar/BitFlags.java39
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java50
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/desugar/Desugar.java232
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/desugar/HeaderClassLoader.java156
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/desugar/Java7Compatibility.java239
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java409
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/desugar/LambdaClassMaker.java95
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/desugar/LambdaDesugaring.java431
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/desugar/LambdaInfo.java30
12 files changed, 1728 insertions, 0 deletions
diff --git a/src/tools/android/java/com/google/devtools/build/android/BUILD b/src/tools/android/java/com/google/devtools/build/android/BUILD
index 34cf96cc8a..56027340ca 100644
--- a/src/tools/android/java/com/google/devtools/build/android/BUILD
+++ b/src/tools/android/java/com/google/devtools/build/android/BUILD
@@ -7,6 +7,7 @@ filegroup(
srcs = [
"BUILD.tools",
"classes_deploy.jar",
+ "//src/tools/android/java/com/google/devtools/build/android/desugar:embedded_tools",
"//src/tools/android/java/com/google/devtools/build/android/proto:srcs",
],
)
@@ -50,6 +51,7 @@ java_library(
filegroup(
name = "srcs",
srcs = glob(["**"]) + [
+ "//src/tools/android/java/com/google/devtools/build/android/desugar:srcs",
"//src/tools/android/java/com/google/devtools/build/android/dexer:srcs",
"//src/tools/android/java/com/google/devtools/build/android/ideinfo:srcs",
"//src/tools/android/java/com/google/devtools/build/android/idlclass:srcs",
diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/BUILD b/src/tools/android/java/com/google/devtools/build/android/desugar/BUILD
new file mode 100644
index 0000000000..cc2dcead72
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/desugar/BUILD
@@ -0,0 +1,38 @@
+# Description:
+# Tool for desugaring Java constructs not supported by Android tools or devices.
+
+filegroup(
+ name = "embedded_tools",
+ srcs = [
+ "BUILD.tools",
+ "desugar_bin_deploy.jar",
+ ],
+ visibility = ["//src/tools/android/java/com/google/devtools/build/android:__pkg__"],
+)
+
+java_library(
+ name = "desugar",
+ srcs = glob(["*.java"]),
+ deps = [
+ "//src/main/java/com/google/devtools/common/options",
+ "//src/main/protobuf:worker_protocol_java_proto",
+ "//src/tools/android/java/com/google/devtools/build/android:android_builder_lib",
+ "//third_party:asm",
+ "//third_party:asm-tree",
+ "//third_party:auto_value",
+ "//third_party:guava",
+ "//third_party:jsr305",
+ ],
+)
+
+java_binary(
+ name = "desugar_bin",
+ main_class = "does.not.exist",
+ runtime_deps = [":desugar"],
+)
+
+filegroup(
+ name = "srcs",
+ srcs = glob(["**"]),
+ visibility = ["//src/tools/android/java/com/google/devtools/build/android:__pkg__"],
+)
diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/BUILD.tools b/src/tools/android/java/com/google/devtools/build/android/desugar/BUILD.tools
new file mode 100644
index 0000000000..29e860205c
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/desugar/BUILD.tools
@@ -0,0 +1,7 @@
+package(default_visibility = ["//visibility:public"])
+
+java_binary(
+ name = "Desugar",
+ main_class = "com.google.devtools.build.android.desugar.Desugar",
+ runtime_deps = [":desugar_bin_deploy.jar"],
+) \ No newline at end of file
diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/BitFlags.java b/src/tools/android/java/com/google/devtools/build/android/desugar/BitFlags.java
new file mode 100644
index 0000000000..8542719c87
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/desugar/BitFlags.java
@@ -0,0 +1,39 @@
+// 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;
+
+/**
+ * Convenience method for working with {@code int} bitwise flags.
+ */
+class BitFlags {
+
+ /**
+ * Returns {@code true} iff <b>all</b> bits in {@code bitmask} are set in {@code flags}.
+ * Trivially returns {@code true} if {@code bitmask} is 0.
+ */
+ public static boolean isSet(int flags, int bitmask) {
+ return (flags & bitmask) == bitmask;
+ }
+
+ /**
+ * Returns {@code true} iff <b>none</b> of the bits in {@code bitmask} are set in {@code flags}.
+ * Trivially returns {@code true} if {@code bitmask} is 0.
+ */
+ public static boolean noneSet(int flags, int bitmask) {
+ return (flags & bitmask) == 0;
+ }
+
+ // Static methods only
+ private BitFlags() {}
+}
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
new file mode 100644
index 0000000000..d324723853
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java
@@ -0,0 +1,50 @@
+// 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;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import javax.annotation.Nullable;
+import org.objectweb.asm.ClassReader;
+
+class ClassReaderFactory {
+ private final ZipFile jar;
+
+ public ClassReaderFactory(ZipFile jar) {
+ this.jar = jar;
+ }
+
+ /**
+ * Returns a reader for the given/internal/Class$Name if the class is defined in the wrapped Jar
+ * and {@code null} otherwise. For simplicity this method turns checked into runtime excpetions
+ * under the assumption that all classes have already been read once when this method is called.
+ */
+ @Nullable
+ public ClassReader readIfKnown(String internalClassName) {
+ ZipEntry entry = jar.getEntry(internalClassName + ".class");
+ if (entry == null) {
+ return null;
+ }
+ try (InputStream bytecode = jar.getInputStream(entry)) {
+ // ClassReader doesn't take ownership and instead eagerly reads the stream's contents
+ return new ClassReader(bytecode);
+ } catch (IOException e) {
+ // We should've already read through all files in the Jar once at this point, so we don't
+ // expect failures reading some files a second time.
+ throw new IllegalStateException("Couldn't load " + internalClassName, e);
+ }
+ }
+}
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
new file mode 100644
index 0000000000..9f927b5024
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/desugar/Desugar.java
@@ -0,0 +1,232 @@
+// 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;
+
+import static com.google.common.base.Preconditions.checkState;
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.io.ByteStreams;
+import com.google.devtools.build.android.Converters.ExistingPathConverter;
+import com.google.devtools.build.android.Converters.PathConverter;
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionsBase;
+import com.google.devtools.common.options.OptionsParser;
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+import java.util.zip.CRC32;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipOutputStream;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassWriter;
+
+/**
+ * Command-line tool to desugar Java 8 constructs that dx doesn't know what to do with, in
+ * particular lambdas and method references.
+ */
+class Desugar {
+
+ /**
+ * Commandline options for {@link Desugar}.
+ */
+ public static class Options extends OptionsBase {
+ @Option(name = "input",
+ defaultValue = "null",
+ category = "input",
+ converter = ExistingPathConverter.class,
+ abbrev = 'i',
+ help = "Input Jar with classes to desugar.")
+ public Path inputJar;
+
+ @Option(name = "classpath_entry",
+ allowMultiple = true,
+ defaultValue = "",
+ category = "input",
+ converter = ExistingPathConverter.class,
+ help = "Ordered classpath to resolve symbols in the --input Jar, like javac's -cp flag.")
+ public List<Path> classpath;
+
+ @Option(name = "bootclasspath_entry",
+ allowMultiple = true,
+ defaultValue = "",
+ category = "input",
+ converter = ExistingPathConverter.class,
+ help = "Bootclasspath that was used to compile the --input Jar with, like javac's "
+ + "-bootclasspath flag. If no bootclasspath is explicitly given then the tool's own "
+ + "bootclasspath is used.")
+ public List<Path> bootclasspath;
+
+ @Option(name = "output",
+ defaultValue = "null",
+ category = "output",
+ converter = PathConverter.class,
+ abbrev = 'o',
+ help = "Output Jar to write desugared classes into.")
+ public Path outputJar;
+
+ @Option(name = "verbose",
+ defaultValue = "false",
+ category = "misc",
+ abbrev = 'v',
+ help = "Enables verbose debugging output.")
+ public boolean verbose;
+ }
+
+ public static void main(String[] args) throws Exception {
+ // 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.
+ Path dumpDirectory = Files.createTempDirectory("lambdas");
+ System.setProperty(
+ LambdaClassMaker.LAMBDA_METAFACTORY_DUMPER_PROPERTY, dumpDirectory.toString());
+
+ if (args.length == 1 && args[0].startsWith("@")) {
+ args = Files.readAllLines(Paths.get(args[0].substring(1)), ISO_8859_1).toArray(new String[0]);
+ }
+
+ OptionsParser optionsParser =
+ OptionsParser.newOptionsParser(Options.class);
+ optionsParser.parseAndExitUponError(args);
+ Options options = optionsParser.getOptions(Options.class);
+
+ if (options.verbose) {
+ System.out.printf("Lambda classes will be written under %s%n", dumpDirectory);
+ }
+ ClassLoader loader =
+ createClassLoader(options.bootclasspath, options.inputJar, options.classpath);
+ try (ZipFile in = new ZipFile(options.inputJar.toFile());
+ ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(
+ Files.newOutputStream(options.outputJar)))) {
+ LambdaClassMaker lambdas = new LambdaClassMaker(dumpDirectory);
+ ClassReaderFactory readerFactory = new ClassReaderFactory(in);
+ ImmutableSet.Builder<String> interfaceLambdaMethodCollector = ImmutableSet.builder();
+
+ // Process input Jar, desugaring as we go
+ for (Enumeration<? extends ZipEntry> entries = in.entries(); entries.hasMoreElements(); ) {
+ ZipEntry entry = entries.nextElement();
+ try (InputStream content = in.getInputStream(entry)) {
+ // 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 (entry.getName().endsWith(".class")) {
+ ClassReader reader = new ClassReader(content);
+ ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS /*for bridge methods*/);
+ reader.accept(
+ new LambdaDesugaring(
+ new Java7Compatibility(writer, readerFactory),
+ loader,
+ lambdas,
+ interfaceLambdaMethodCollector),
+ 0);
+ writeStoredEntry(out, entry.getName(), writer.toByteArray());
+ } else {
+ // TODO(bazel-team): Avoid de- and re-compressing resource files
+ ZipEntry destEntry = new ZipEntry(entry);
+ destEntry.setCompressedSize(-1);
+ out.putNextEntry(destEntry);
+ ByteStreams.copy(content, out);
+ out.closeEntry();
+ }
+ }
+ }
+
+ // Write out the lambda classes we generated along the way
+ ImmutableSet<String> interfaceLambdaMethods = interfaceLambdaMethodCollector.build();
+ for (Map.Entry<Path, LambdaInfo> lambdaClass : lambdas.drain().entrySet()) {
+ try (InputStream bytecode =
+ Files.newInputStream(dumpDirectory.resolve(lambdaClass.getKey()))) {
+ ClassReader reader = new ClassReader(bytecode);
+ ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS /*for invoking bridges*/);
+ LambdaClassFixer lambdaFixer =
+ new LambdaClassFixer(
+ // null ClassReaderFactory b/c we don't expect to need it for lambda classes
+ new Java7Compatibility(writer, (ClassReaderFactory) null),
+ lambdaClass.getValue(),
+ readerFactory,
+ interfaceLambdaMethods);
+ // Send lambda classes through desugaring to make sure there's no invokedynamic
+ // instructions in generated lambda classes (checkState below will fail)
+ reader.accept(new LambdaDesugaring(lambdaFixer, loader, lambdas, null), 0);
+ writeStoredEntry(out, lambdaFixer.getInternalName() + ".class", writer.toByteArray());
+ }
+ }
+
+ Map<Path, LambdaInfo> leftBehind = lambdas.drain();
+ checkState(leftBehind.isEmpty(), "Didn't process %s", leftBehind);
+ }
+ // Use input's timestamp for output file so the output file is stable.
+ Files.setLastModifiedTime(options.outputJar, Files.getLastModifiedTime(options.inputJar));
+ }
+
+ private static void writeStoredEntry(ZipOutputStream out, String filename, byte[] content)
+ throws IOException {
+ // Need to pre-compute checksum for STORED (uncompressed) entries)
+ CRC32 checksum = new CRC32();
+ checksum.update(content);
+
+ ZipEntry result = new ZipEntry(filename);
+ result.setTime(0L); // Use stable timestamp Jan 1 1980
+ result.setCrc(checksum.getValue());
+ result.setSize(content.length);
+ result.setCompressedSize(content.length);
+ // Write uncompressed, since this is just an intermediary artifact that we will convert to .dex
+ result.setMethod(ZipEntry.STORED);
+
+ out.putNextEntry(result);
+ out.write(content);
+ out.closeEntry();
+ }
+
+ private static ClassLoader createClassLoader(List<Path> bootclasspath, Path inputJar,
+ List<Path> classpath) throws IOException {
+ // Prepend classpath with input jar itself so LambdaDesugaring can load classes with lambdas.
+ // Note that inputJar and classpath need to be in the same classloader because we typically get
+ // the header Jar for inputJar on the classpath and having the header Jar in a parent loader
+ // means the header version is preferred over the real thing.
+ classpath = ImmutableList.<Path>builder().add(inputJar).addAll(classpath).build();
+ if (bootclasspath.isEmpty()) {
+ // TODO(b/31547323): Require bootclasspath once Bazel always provides it. Using the tool's
+ // bootclasspath as a fallback is iffy at best and produces wrong results at worst.
+ return HeaderClassLoader.fromClassPath(classpath);
+ }
+ // 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.
+ return HeaderClassLoader.fromClassPath(classpath,
+ HeaderClassLoader.fromClassPath(bootclasspath,
+ new 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();
+ }
+ }));
+ }
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/HeaderClassLoader.java b/src/tools/android/java/com/google/devtools/build/android/desugar/HeaderClassLoader.java
new file mode 100644
index 0000000000..8d9f7ea1e0
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/desugar/HeaderClassLoader.java
@@ -0,0 +1,156 @@
+// 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;
+
+import java.io.IOError;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Path;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.zip.ZipEntry;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+/**
+ * Class loader that can "load" classes from header Jars. This class loader stubs in missing code
+ * attributes on the fly to make {@link ClassLoader#defineClass} happy. Classes loaded are unusable
+ * other than to resolve method references, so this class loader should only be used to process
+ * or inspect classes, not to execute their code. Also note that the resulting classes may be
+ * missing private members, which header Jars may omit.
+ *
+ * @see java.net.URLClassLoader
+ */
+class HeaderClassLoader extends ClassLoader {
+
+ private final Map<String, JarFile> jarfiles;
+
+ /** Creates a classloader from the given classpath with the system classloader as its parent. */
+ public static HeaderClassLoader fromClassPath(List<Path> classpath) throws IOException {
+ return new HeaderClassLoader(indexJars(classpath));
+ }
+
+ /** Creates a classloader from the given classpath with the given parent. */
+ public static HeaderClassLoader fromClassPath(List<Path> classpath, ClassLoader parent)
+ throws IOException {
+ return new HeaderClassLoader(indexJars(classpath), parent);
+ }
+
+ /**
+ * Opens the given list of Jar files and returns an index of all classes in them, to avoid
+ * scanning all Jars over and over for each class in {@link #findClass}.
+ */
+ private static Map<String, JarFile> indexJars(List<Path> classpath) throws IOException {
+ HashMap<String, JarFile> result = new HashMap<>();
+ for (Path jarfile : classpath) {
+ JarFile jar = new JarFile(jarfile.toFile());
+ for (Enumeration<JarEntry> cur = jar.entries(); cur.hasMoreElements(); ) {
+ JarEntry entry = cur.nextElement();
+ if (entry.getName().endsWith(".class") && !result.containsKey(entry.getName())) {
+ result.put(entry.getName(), jar);
+ }
+ }
+ }
+ return result;
+ }
+
+ private HeaderClassLoader(Map<String, JarFile> jarfiles) {
+ super();
+ this.jarfiles = jarfiles;
+ }
+
+ private HeaderClassLoader(Map<String, JarFile> jarfiles, ClassLoader parent) {
+ super(parent);
+ this.jarfiles = jarfiles;
+ }
+
+ @Override
+ protected Class<?> findClass(String name) throws ClassNotFoundException {
+ String filename = name.replace('.', '/') + ".class";
+ JarFile jarfile = jarfiles.get(filename);
+ if (jarfile == null) {
+ throw new ClassNotFoundException();
+ }
+ ZipEntry entry = jarfile.getEntry(filename);
+ byte[] bytecode;
+ try (InputStream content = jarfile.getInputStream(entry)) {
+ ClassReader reader = new ClassReader(content);
+ // 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);
+ reader.accept(new CodeStubber(writer), 0);
+ bytecode = writer.toByteArray();
+ } catch (IOException e) {
+ throw new IOError(e);
+ }
+ return defineClass(name, bytecode, 0, bytecode.length);
+ }
+
+ /** Class visitor that stubs in missing code attributes. */
+ private static class CodeStubber extends ClassVisitor {
+
+ public CodeStubber(ClassVisitor cv) {
+ super(Opcodes.ASM5, cv);
+ }
+
+ @Override
+ public MethodVisitor visitMethod(
+ int access, String name, String desc, String signature, String[] exceptions) {
+ MethodVisitor dest = super.visitMethod(access, name, desc, signature, exceptions);
+ if ((access & (Opcodes.ACC_ABSTRACT | Opcodes.ACC_NATIVE)) != 0) {
+ // No need to stub out abstract or native methods
+ return dest;
+ }
+ return new BodyStubber(dest);
+ }
+
+ }
+
+ /** Method visitor used by {@link CodeStubber} to put code into methods without code. */
+ private static class BodyStubber extends MethodVisitor {
+
+ private static final String EXCEPTION_INTERNAL_NAME = "java/lang/UnsupportedOperationException";
+
+ private boolean hasCode = false;
+
+ public BodyStubber(MethodVisitor mv) {
+ super(Opcodes.ASM5, mv);
+ }
+
+ @Override
+ public void visitCode() {
+ hasCode = true;
+ super.visitCode();
+ }
+
+ @Override
+ public void visitEnd() {
+ if (!hasCode) {
+ super.visitTypeInsn(Opcodes.NEW, EXCEPTION_INTERNAL_NAME);
+ super.visitInsn(Opcodes.DUP);
+ super.visitMethodInsn(
+ Opcodes.INVOKESPECIAL, EXCEPTION_INTERNAL_NAME, "<init>", "()V", /*itf*/ false);
+ super.visitInsn(Opcodes.ATHROW);
+ super.visitMaxs(0, 0); // triggers computation of the actual max's
+ }
+ super.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
new file mode 100644
index 0000000000..cc3fe14ff2
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/desugar/Java7Compatibility.java
@@ -0,0 +1,239 @@
+// 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;
+
+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.AnnotationVisitor;
+import org.objectweb.asm.Attribute;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+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
+ * interface methods (i.e., non-abstract interface methods), which don't exist in Java 7.
+ */
+public class Java7Compatibility extends ClassVisitor {
+
+ private final ClassReaderFactory factory;
+
+ private boolean isInterface;
+ private String internalName;
+
+ public Java7Compatibility(ClassVisitor cv, ClassReaderFactory factory) {
+ super(Opcodes.ASM5, cv);
+ this.factory = factory;
+ }
+
+ @Override
+ public void visit(
+ int version,
+ int access,
+ String name,
+ String signature,
+ String superName,
+ String[] interfaces) {
+ internalName = name;
+ isInterface = BitFlags.isSet(access, Opcodes.ACC_INTERFACE);
+ super.visit(
+ Math.min(version, Opcodes.V1_7),
+ access,
+ name,
+ signature,
+ superName,
+ interfaces);
+ }
+
+ @Override
+ public MethodVisitor visitMethod(
+ int access, String name, String desc, String signature, String[] exceptions) {
+ // Remove bridge default methods in interfaces; javac generates them again for implementing
+ // classes anyways.
+ if (isInterface
+ && (access & (Opcodes.ACC_BRIDGE | Opcodes.ACC_ABSTRACT | Opcodes.ACC_STATIC))
+ == Opcodes.ACC_BRIDGE) {
+ return null;
+ }
+ if (isInterface
+ && "$jacocoInit".equals(name)
+ && BitFlags.isSet(access, Opcodes.ACC_SYNTHETIC | Opcodes.ACC_STATIC)) {
+ // Drop static interface method that Jacoco generates--we'll inline it into the static
+ // initializer instead
+ return null;
+ }
+ // TODO(b/31547323): Avoid stack trace and report errors more user-friendly
+ checkArgument(!isInterface
+ || BitFlags.isSet(access, Opcodes.ACC_ABSTRACT)
+ || "<clinit>".equals(name),
+ "Interface %s defines non-abstract method %s%s, which is not supported",
+ internalName, name, desc);
+ MethodVisitor result = super.visitMethod(access, name, desc, signature, exceptions);
+ return (isInterface && "<clinit>".equals(name)) ? new InlineJacocoInit(result) : result;
+ }
+
+ @Override
+ public void visitInnerClass(String name, String outerName, String innerName, int access) {
+ // Drop MethodHandles$Lookup inner class information--it shouldn't be needed anymore. Proguard
+ // complains about this even though the inner class information is never used.
+ if (!"java/lang/invoke/MethodHandles$Lookup".equals(name)) {
+ super.visitInnerClass(name, outerName, innerName, access);
+ }
+ }
+
+ private class InlineJacocoInit extends MethodVisitor {
+ public InlineJacocoInit(MethodVisitor dest) {
+ super(Opcodes.ASM5, dest);
+ }
+
+ @Override
+ public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
+ if (opcode == Opcodes.INVOKESTATIC
+ && "$jacocoInit".equals(name)
+ && internalName.equals(owner)) {
+ ClassReader bytecode = checkNotNull(factory.readIfKnown(internalName),
+ "Couldn't load interface %s to inline $jacocoInit()", internalName);
+ InlineOneMethod copier = new InlineOneMethod("$jacocoInit", this);
+ bytecode.accept(copier, ClassReader.SKIP_DEBUG /* we're copying generated code anyway */);
+ } else {
+ super.visitMethodInsn(opcode, owner, name, desc, itf);
+ }
+ }
+ }
+
+ private static class InlineOneMethod extends ClassVisitor {
+
+ private final String methodName;
+ private final MethodVisitor dest;
+ private int copied = 0;
+
+ public InlineOneMethod(String methodName, MethodVisitor dest) {
+ super(Opcodes.ASM5);
+ this.methodName = methodName;
+ this.dest = dest;
+ }
+
+ @Override
+ public void visit(
+ int version,
+ int access,
+ String name,
+ String signature,
+ String superName,
+ String[] interfaces) {
+ checkArgument(BitFlags.isSet(access, Opcodes.ACC_INTERFACE));
+ }
+
+ @Override
+ public MethodVisitor visitMethod(
+ int access, String name, String desc, String signature, String[] exceptions) {
+ if (name.equals(methodName)) {
+ checkState(copied == 0, "Found unexpected second method %s with descriptor %s", name, desc);
+ ++copied;
+ return new InlineMethodBody(dest);
+ }
+ return null;
+ }
+ }
+
+ private static class InlineMethodBody extends MethodVisitor {
+ private final MethodVisitor dest;
+
+ public InlineMethodBody(MethodVisitor dest) {
+ // We'll set the destination visitor in visitCode() to reduce the risk of copying anything
+ // we didn't mean to copy
+ super(Opcodes.ASM5, (MethodVisitor) null);
+ this.dest = dest;
+ }
+
+ @Override
+ public void visitParameter(String name, int access) {
+ }
+
+ @Override
+ public AnnotationVisitor visitAnnotationDefault() {
+ throw new IllegalStateException("Don't use to copy annotation attributes");
+ }
+
+ @Override
+ public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+ return null;
+ }
+
+ @Override
+ public AnnotationVisitor visitTypeAnnotation(
+ int typeRef, TypePath typePath, String desc, boolean visible) {
+ return null;
+ }
+
+ @Override
+ public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) {
+ return null;
+ }
+
+ @Override
+ public void visitAttribute(Attribute attr) {
+ mv = null; // don't care about anything but the code attribute
+ }
+
+ @Override
+ public void visitCode() {
+ // Start copying instructions but don't call super.visitCode() since dest is already in the
+ // middle of visiting another method
+ mv = dest;
+ }
+
+ @Override
+ public void visitInsn(int opcode) {
+ switch (opcode) {
+ case Opcodes.IRETURN:
+ case Opcodes.LRETURN:
+ case Opcodes.FRETURN:
+ case Opcodes.DRETURN:
+ case Opcodes.ARETURN:
+ case Opcodes.RETURN:
+ checkState(mv != null, "Encountered a second return it would seem: %s", opcode);
+ mv = null; // Done: we don't expect anything to follow
+ return;
+ default:
+ super.visitInsn(opcode);
+ }
+ }
+
+ @Override
+ public void visitVarInsn(int opcode, int var) {
+ throw new UnsupportedOperationException(
+ "We don't support inlining methods with locals: " + opcode + " " + var);
+ }
+
+ @Override
+ public void visitLocalVariable(
+ String name, String desc, String signature, Label start, Label end, int index) {
+ throw new UnsupportedOperationException(
+ "We don't support inlining methods with locals: " + name + ": " + desc);
+ }
+
+ @Override
+ public void visitMaxs(int maxStack, int maxLocals) {
+ // Drop this, since dest will get more instructions and will need to recompute stack size.
+ // This does indicate the end of visiting bytecode instructions, so defensively reset mv.
+ mv = null;
+ }
+ }
+}
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
new file mode 100644
index 0000000000..b429f76208
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java
@@ -0,0 +1,409 @@
+// 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;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+import org.objectweb.asm.tree.AbstractInsnNode;
+import org.objectweb.asm.tree.MethodNode;
+import org.objectweb.asm.tree.TypeInsnNode;
+
+/**
+ * Visitor intended to fix up lambda classes to match assumptions made in {@link LambdaDesugaring}.
+ * Specifically this includes fixing visibilities and generating any missing factory methods.
+ *
+ * <p>Each instance can only visit one class. This is because the signature of the needed factory
+ * method is passed into the constructor.
+ */
+class LambdaClassFixer extends ClassVisitor {
+
+ /** Magic method name used by {@link java.lang.invoke.LambdaMetafactory} */
+ public static final String FACTORY_METHOD_NAME = "get$Lambda";
+
+ private final LambdaInfo lambdaInfo;
+ private final ClassReaderFactory factory;
+ private final ImmutableSet<String> interfaceLambdaMethods;
+ private final HashSet<String> implementedMethods = new HashSet<>();
+ private final LinkedHashSet<String> methodsToMoveIn = new LinkedHashSet<>();
+
+ private String internalName;
+ private ImmutableList<String> interfaces;
+
+ private boolean hasState;
+ private boolean hasFactory;
+
+ private String desc;
+ private String signature;
+ private String[] exceptions;
+
+
+ public LambdaClassFixer(ClassVisitor dest, LambdaInfo lambdaInfo, ClassReaderFactory factory,
+ ImmutableSet<String> interfaceLambdaMethods) {
+ super(Opcodes.ASM5, dest);
+ this.lambdaInfo = lambdaInfo;
+ this.factory = factory;
+ this.interfaceLambdaMethods = interfaceLambdaMethods;
+ }
+
+ public String getInternalName() {
+ return internalName;
+ }
+
+ @Override
+ public void visit(
+ int version,
+ int access,
+ String name,
+ String signature,
+ String superName,
+ String[] interfaces) {
+ checkArgument(BitFlags.noneSet(access, Opcodes.ACC_INTERFACE), "Not a class: %s", name);
+ checkState(internalName == null, "Already visited %s, can't reuse for %s", internalName, name);
+ internalName = name;
+ hasState = false;
+ hasFactory = false;
+ desc = null;
+ this.signature = null;
+ exceptions = null;
+ this.interfaces = ImmutableList.copyOf(interfaces);
+ super.visit(version, access, name, signature, superName, interfaces);
+ }
+
+ @Override
+ public FieldVisitor visitField(
+ int access, String name, String desc, String signature, Object value) {
+ hasState = true;
+ return super.visitField(access, name, desc, signature, value);
+ }
+
+ @Override
+ public MethodVisitor visitMethod(
+ int access, String name, String desc, String signature, String[] exceptions) {
+ if (name.equals("writeReplace") && BitFlags.noneSet(access, Opcodes.ACC_STATIC)
+ && desc.equals("()Ljava/lang/Object;")) {
+ // Lambda serialization hooks use java/lang/invoke/SerializedLambda, which isn't available on
+ // Android. Since Jack doesn't do anything special for serializable lambdas we just drop these
+ // serialization hooks.
+ // https://docs.oracle.com/javase/8/docs/platform/serialization/spec/output.html#a5324 gives
+ // details on the role and signature of this method.
+ return null;
+ }
+ if (BitFlags.noneSet(access, Opcodes.ACC_ABSTRACT | Opcodes.ACC_STATIC)) {
+ // Keep track of instance methods implemented in this class for later. Since this visitor
+ // is intended for lambda classes, no need to look at the superclass.
+ implementedMethods.add(name + ":" + desc);
+ }
+ if (FACTORY_METHOD_NAME.equals(name)) {
+ hasFactory = true;
+ access &= ~Opcodes.ACC_PRIVATE; // make factory method accessible
+ } else if ("<init>".equals(name)) {
+ this.desc = desc;
+ this.signature = signature;
+ this.exceptions = exceptions;
+ }
+ MethodVisitor methodVisitor =
+ new LambdaClassMethodRewriter(super.visitMethod(access, name, desc, signature, exceptions));
+ if (!lambdaInfo.bridgeMethod().equals(lambdaInfo.methodReference())) {
+ // Skip UseBridgeMethod unless we actually need it
+ methodVisitor =
+ new UseBridgeMethod(methodVisitor, lambdaInfo, access, name, desc, signature, exceptions);
+ }
+ return methodVisitor;
+ }
+
+ @Override
+ public void visitEnd() {
+ checkState(!hasState || hasFactory,
+ "Expected factory method for capturing lambda %s", internalName);
+ if (!hasFactory) {
+ // Fake factory method if LambdaMetafactory didn't generate it
+ checkState(signature == null,
+ "Didn't expect generic constructor signature %s %s", internalName, signature);
+
+ // Since this is a stateless class we populate and use a static singleton field "$instance"
+ String singletonFieldDesc = Type.getObjectType(internalName).getDescriptor();
+ super.visitField(
+ Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL,
+ "$instance",
+ singletonFieldDesc,
+ (String) null,
+ (Object) null)
+ .visitEnd();
+
+ MethodVisitor codeBuilder =
+ super.visitMethod(
+ Opcodes.ACC_STATIC,
+ "<clinit>",
+ "()V",
+ (String) null,
+ new String[0]);
+ codeBuilder.visitTypeInsn(Opcodes.NEW, internalName);
+ codeBuilder.visitInsn(Opcodes.DUP);
+ codeBuilder.visitMethodInsn(Opcodes.INVOKESPECIAL, internalName, "<init>",
+ checkNotNull(desc, "didn't see a constructor for %s", internalName), /*itf*/ false);
+ codeBuilder.visitFieldInsn(Opcodes.PUTSTATIC, internalName, "$instance", singletonFieldDesc);
+ codeBuilder.visitInsn(Opcodes.RETURN);
+ codeBuilder.visitMaxs(2, 0); // two values are pushed onto the stack
+ codeBuilder.visitEnd();
+
+ codeBuilder = // reuse codeBuilder variable to avoid accidental additions to previous method
+ super.visitMethod(
+ Opcodes.ACC_STATIC,
+ FACTORY_METHOD_NAME,
+ lambdaInfo.factoryMethodDesc(),
+ (String) null,
+ exceptions);
+ codeBuilder.visitFieldInsn(Opcodes.GETSTATIC, internalName, "$instance", singletonFieldDesc);
+ codeBuilder.visitInsn(Opcodes.ARETURN);
+ codeBuilder.visitMaxs(1, 0); // one value on the stack
+ }
+
+ copyRewrittenLambdaMethods();
+ copyBridgeMethods(interfaces);
+ super.visitEnd();
+ }
+
+ private void copyRewrittenLambdaMethods() {
+ for (String rewritten : methodsToMoveIn) {
+ String interfaceInternalName = rewritten.substring(0, rewritten.indexOf('#'));
+ String methodName = rewritten.substring(interfaceInternalName.length() + 1);
+ ClassReader bytecode = checkNotNull(factory.readIfKnown(interfaceInternalName),
+ "Couldn't load interface with lambda method %s", rewritten);
+ CopyOneMethod copier = new CopyOneMethod(methodName);
+ // TODO(kmb): Set source file attribute for lambda classes so lambda debug info makes sense
+ bytecode.accept(copier, ClassReader.SKIP_DEBUG);
+ }
+ }
+
+ private void copyBridgeMethods(ImmutableList<String> interfaces) {
+ for (String implemented : interfaces) {
+ ClassReader bytecode = factory.readIfKnown(implemented);
+ if (bytecode != null) {
+ // Don't copy line numbers and local variable tables. They would be misleading or wrong
+ // and other methods in generated lambda classes don't have debug info either.
+ bytecode.accept(new CopyBridgeMethods(), ClassReader.SKIP_DEBUG);
+ } // else the interface is defined in a different Jar, which we can ignore here
+ }
+ }
+
+ /**
+ * Rewriter for methods in generated lambda classes.
+ */
+ private class LambdaClassMethodRewriter extends MethodVisitor {
+ public LambdaClassMethodRewriter(MethodVisitor dest) {
+ super(Opcodes.ASM5, dest);
+ }
+
+ @Override
+ public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
+ String method = owner + "#" + name;
+ // Rewrite invocations of lambda methods in interfaces to anticipate the lambda method being
+ // moved into the lambda class (i.e., the class being visited here).
+ if (interfaceLambdaMethods.contains(method)) {
+ checkArgument(opcode == Opcodes.INVOKESTATIC, "Cannot move instance method %s", method);
+ owner = internalName;
+ itf = false; // owner was interface but is now a class
+ methodsToMoveIn.add(method);
+ }
+ super.visitMethodInsn(opcode, owner, name, desc, itf);
+ }
+
+ @Override
+ public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+ // Drop annotation that's part of the generated lambda class that's not available on Android.
+ // Proguard complains about this otherwise.
+ if ("Ljava/lang/invoke/LambdaForm$Hidden;".equals(desc)) {
+ return null;
+ }
+ return super.visitAnnotation(desc, visible);
+ }
+ }
+
+ /**
+ * Visitor that copies bridge methods from the visited interface into the class visited by the
+ * surrounding {@link LambdaClassFixer}. Descends recursively into interfaces extended by the
+ * visited interface.
+ */
+ private class CopyBridgeMethods extends ClassVisitor {
+
+ @SuppressWarnings("hiding") private ImmutableList<String> interfaces;
+
+ public CopyBridgeMethods() {
+ // No delegate visitor; instead we'll add methods to the outer class's delegate where needed
+ 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);
+ }
+
+ @Override
+ public MethodVisitor visitMethod(
+ int access, String name, String desc, String signature, String[] exceptions) {
+ if ((access & (Opcodes.ACC_BRIDGE | Opcodes.ACC_ABSTRACT | Opcodes.ACC_STATIC))
+ == Opcodes.ACC_BRIDGE) {
+ // 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));
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void visitEnd() {
+ copyBridgeMethods(this.interfaces);
+ }
+ }
+
+ private class CopyOneMethod extends ClassVisitor {
+
+ private final String methodName;
+ private int copied = 0;
+
+ public CopyOneMethod(String methodName) {
+ // No delegate visitor; instead we'll add methods to the outer class's delegate where needed
+ super(Opcodes.ASM5);
+ this.methodName = methodName;
+ }
+
+ @Override
+ public void visit(
+ int version,
+ int access,
+ String name,
+ String signature,
+ String superName,
+ String[] interfaces) {
+ checkArgument(BitFlags.isSet(access, Opcodes.ACC_INTERFACE));
+ }
+
+ @Override
+ public MethodVisitor visitMethod(
+ int access, String name, String desc, String signature, String[] exceptions) {
+ if (name.equals(methodName)) {
+ checkState(copied == 0, "Found unexpected second method %s with descriptor %s", name, desc);
+ ++copied;
+ return new AvoidJacocoInit(
+ LambdaClassFixer.super.visitMethod(access, name, desc, signature, exceptions));
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Method visitor that rewrites {@code $jacocoInit()} calls to equivalent field accesses.
+ *
+ * <p>This class should only be used to visit interface methods and assumes that the code in
+ * {@code $jacocoInit()} is always executed in the interface's static initializer, which is the
+ * case in the absence of hand-written static or default interface methods (which
+ * {@link Java7Compatibility} makes sure of).
+ */
+ private static class AvoidJacocoInit extends MethodVisitor {
+ public AvoidJacocoInit(MethodVisitor dest) {
+ super(Opcodes.ASM5, dest);
+ }
+
+ @Override
+ public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
+ if (opcode == Opcodes.INVOKESTATIC && "$jacocoInit".equals(name)) {
+ // Rewrite $jacocoInit() calls to just read the $jacocoData field
+ super.visitFieldInsn(Opcodes.GETSTATIC, owner, "$jacocoData", "[Z");
+ } else {
+ super.visitMethodInsn(opcode, owner, name, desc, itf);
+ }
+ }
+ }
+
+ private static class UseBridgeMethod extends MethodNode {
+
+ private final MethodVisitor dest;
+ private final LambdaInfo lambdaInfo;
+
+ public UseBridgeMethod(MethodVisitor dest, LambdaInfo lambdaInfo,
+ int access, String name, String desc, String signature, String[] exceptions) {
+ super(Opcodes.ASM5, access, name, desc, signature, exceptions);
+ this.dest = dest;
+ this.lambdaInfo = lambdaInfo;
+ }
+
+ @Override
+ public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
+ if (owner.equals(lambdaInfo.methodReference().getOwner())
+ && name.equals(lambdaInfo.methodReference().getName())
+ && desc.equals(lambdaInfo.methodReference().getDesc())) {
+ if (lambdaInfo.methodReference().getTag() == Opcodes.H_NEWINVOKESPECIAL
+ && lambdaInfo.bridgeMethod().getTag() != Opcodes.H_NEWINVOKESPECIAL) {
+ // We're changing a constructor call to a factory method call, so we unfortunately need
+ // to go find the NEW/DUP pair preceding the constructor call and remove it
+ removeLastAllocation();
+ }
+ super.visitMethodInsn(
+ LambdaDesugaring.invokeOpcode(lambdaInfo.bridgeMethod()),
+ lambdaInfo.bridgeMethod().getOwner(),
+ lambdaInfo.bridgeMethod().getName(),
+ lambdaInfo.bridgeMethod().getDesc(),
+ lambdaInfo.bridgeMethod().isInterface());
+
+ } else {
+ super.visitMethodInsn(opcode, owner, name, desc, itf);
+ }
+ }
+
+ private void removeLastAllocation() {
+ AbstractInsnNode insn = instructions.getLast();
+ while (insn != null && insn.getPrevious() != null) {
+ AbstractInsnNode prev = insn.getPrevious();
+ if (prev.getOpcode() == Opcodes.NEW && insn.getOpcode() == Opcodes.DUP
+ && ((TypeInsnNode) prev).desc.equals(lambdaInfo.methodReference().getOwner())) {
+ instructions.remove(prev);
+ instructions.remove(insn);
+ return;
+ }
+ insn = prev;
+ }
+ throw new IllegalStateException(
+ "Couldn't find allocation to rewrite ::new reference " + lambdaInfo.methodReference());
+ }
+
+ @Override
+ public void visitEnd() {
+ accept(dest);
+ }
+ }
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaClassMaker.java b/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaClassMaker.java
new file mode 100644
index 0000000000..8a2ebea51b
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaClassMaker.java
@@ -0,0 +1,95 @@
+// 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;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterators;
+import java.io.IOException;
+import java.lang.invoke.MethodHandle;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+
+class LambdaClassMaker {
+
+ static final String LAMBDA_METAFACTORY_DUMPER_PROPERTY = "jdk.internal.lambda.dumpProxyClasses";
+
+ private final Path rootDirectory;
+ private final Map<Path, LambdaInfo> generatedClasses = new LinkedHashMap<>();
+
+ public LambdaClassMaker(Path rootDirectory) {
+ checkArgument(Files.isDirectory(rootDirectory));
+ this.rootDirectory = rootDirectory;
+ }
+
+ public String generateLambdaClass(String invokerInternalName, LambdaInfo lambdaInfo,
+ MethodHandle bootstrapMethod, ArrayList<Object> bsmArgs) throws IOException {
+ // Invoking the bootstrap method will dump the generated class
+ try {
+ bootstrapMethod.invokeWithArguments(bsmArgs);
+ } catch (Throwable e) {
+ throw new IllegalStateException("Failed to generate lambda class for class "
+ + invokerInternalName + " using " + bootstrapMethod + " with arguments " + bsmArgs, e);
+ }
+
+ Path generatedClassFile = findOnlyUnprocessed(invokerInternalName + "$$Lambda$");
+ String lambdaClassName = generatedClassFile.toString();
+ checkState(lambdaClassName.endsWith(".class"), "Unexpected filename %s", lambdaClassName);
+ lambdaClassName = lambdaClassName.substring(0, lambdaClassName.length() - ".class".length());
+ generatedClasses.put(generatedClassFile, lambdaInfo);
+ return lambdaClassName;
+ }
+
+ /**
+ * Returns relative paths to .class files generated since the last call to this method together
+ * with a string descriptor of the factory method.
+ */
+ public Map<Path, LambdaInfo> drain() {
+ ImmutableMap<Path, LambdaInfo> result = ImmutableMap.copyOf(generatedClasses);
+ generatedClasses.clear();
+ return result;
+ }
+
+ private Path findOnlyUnprocessed(final String pathPrefix) throws IOException {
+ // TODO(kmb): Investigate making this faster in the case of many lambdas
+ // TODO(bazel-team): This could be much nicer with lambdas
+ try (Stream<Path> results =
+ Files.walk(rootDirectory)
+ .map(
+ new Function<Path, Path>() {
+ @Override
+ public Path apply(Path path) {
+ return rootDirectory.relativize(path);
+ }
+ })
+ .filter(
+ new Predicate<Path>() {
+ @Override
+ public boolean test(Path path) {
+ return path.toString().startsWith(pathPrefix)
+ && !generatedClasses.containsKey(path);
+ }
+ })) {
+ return Iterators.getOnlyElement(results.iterator());
+ }
+ }
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaDesugaring.java b/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaDesugaring.java
new file mode 100644
index 0000000000..2248fa0a70
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaDesugaring.java
@@ -0,0 +1,431 @@
+// 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;
+
+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.lang.invoke.MethodHandles.publicLookup;
+import static org.objectweb.asm.Opcodes.ASM5;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableSet;
+import java.io.IOException;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles.Lookup;
+import java.lang.invoke.MethodType;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Executable;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.Handle;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+/**
+ * Visitor that desugars classes with uses of lambdas into Java 7-looking code. This includes
+ * rewriting lambda-related invokedynamic instructions as well as fixing accessibility of methods
+ * that javac emits for lambda bodies.
+ */
+class LambdaDesugaring extends ClassVisitor {
+
+ private final ClassLoader targetLoader;
+ private final LambdaClassMaker lambdas;
+ private final ImmutableSet.Builder<String> aggregateInterfaceLambdaMethods;
+ private final Map<Handle, MethodReferenceBridgeInfo> bridgeMethods = new HashMap<>();
+
+ private String internalName;
+ private boolean isInterface;
+
+ public LambdaDesugaring(ClassVisitor dest, ClassLoader targetLoader, LambdaClassMaker lambdas,
+ ImmutableSet.Builder<String> aggregateInterfaceLambdaMethods) {
+ super(Opcodes.ASM5, dest);
+ this.targetLoader = targetLoader;
+ this.lambdas = lambdas;
+ this.aggregateInterfaceLambdaMethods = aggregateInterfaceLambdaMethods;
+ }
+
+ @Override
+ public void visit(
+ int version,
+ int access,
+ String name,
+ String signature,
+ String superName,
+ String[] interfaces) {
+ internalName = name;
+ isInterface = BitFlags.isSet(access, Opcodes.ACC_INTERFACE);
+ super.visit(version, access, name, signature, superName, interfaces);
+ }
+
+ @Override
+ public void visitEnd() {
+ for (Map.Entry<Handle, MethodReferenceBridgeInfo> bridge : bridgeMethods.entrySet()) {
+ Handle original = bridge.getKey();
+ Handle neededMethod = bridge.getValue().bridgeMethod();
+ checkState(neededMethod.getTag() == Opcodes.H_INVOKESTATIC
+ || neededMethod.getTag() == Opcodes.H_INVOKEVIRTUAL,
+ "Cannot generate bridge method %s to reach %s", neededMethod, original);
+ checkState(bridge.getValue().referenced() != null,
+ "Need referenced method %s to generate bridge %s", original, neededMethod);
+
+ int access = Opcodes.ACC_BRIDGE | Opcodes.ACC_SYNTHETIC | Opcodes.ACC_FINAL;
+ if (neededMethod.getTag() == Opcodes.H_INVOKESTATIC) {
+ access |= Opcodes.ACC_STATIC;
+ }
+ MethodVisitor bridgeMethod =
+ super.visitMethod(
+ access,
+ neededMethod.getName(),
+ neededMethod.getDesc(),
+ (String) null,
+ toInternalNames(bridge.getValue().referenced().getExceptionTypes()));
+
+ // Bridge is a factory method calling a constructor
+ if (original.getTag() == Opcodes.H_NEWINVOKESPECIAL) {
+ bridgeMethod.visitTypeInsn(Opcodes.NEW, original.getOwner());
+ bridgeMethod.visitInsn(Opcodes.DUP);
+ }
+
+ int slot = 0;
+ if (neededMethod.getTag() != Opcodes.H_INVOKESTATIC) {
+ bridgeMethod.visitVarInsn(Opcodes.ALOAD, slot++);
+ }
+ Type neededType = Type.getMethodType(neededMethod.getDesc());
+ for (Type arg : neededType.getArgumentTypes()) {
+ bridgeMethod.visitVarInsn(arg.getOpcode(Opcodes.ILOAD), slot);
+ slot += arg.getSize();
+ }
+ bridgeMethod.visitMethodInsn(invokeOpcode(original), original.getOwner(), original.getName(),
+ original.getDesc(), original.isInterface());
+ bridgeMethod.visitInsn(neededType.getReturnType().getOpcode(Opcodes.IRETURN));
+
+ bridgeMethod.visitMaxs(0, 0); // rely on class writer to compute these
+ bridgeMethod.visitEnd();
+ }
+ super.visitEnd();
+ }
+
+ @Override
+ public MethodVisitor visitMethod(
+ int access, String name, String desc, String signature, String[] exceptions) {
+ if (name.equals("$deserializeLambda$") && BitFlags.isSet(access, Opcodes.ACC_SYNTHETIC)) {
+ // Android doesn't do anything special for lambda serialization so drop the special
+ // deserialization hook that javac generates. This also makes sure we don't reference
+ // java/lang/invoke/SerializedLambda, which doesn't exist on Android.
+ return null;
+ }
+ if (name.startsWith("lambda$") && BitFlags.isSet(access, Opcodes.ACC_SYNTHETIC)) {
+ if (isInterface && BitFlags.isSet(access, Opcodes.ACC_STATIC)) {
+ // There must be a lambda in the interface (which in the absence of hand-written default or
+ // static interface methods must mean it's in the <clinit> method or inside another lambda).
+ // We'll move this method out of this class, so just record and drop it here.
+ // (Note lambda body methods have unique names, so we don't need to remember desc here.)
+ aggregateInterfaceLambdaMethods.add(internalName + '#' + name);
+ return null;
+ }
+ if (BitFlags.isSet(access, Opcodes.ACC_PRIVATE)) {
+ // Make lambda body method accessible from lambda class
+ access &= ~Opcodes.ACC_PRIVATE;
+ // Method was private so it can be final, which should help VMs perform dispatch.
+ access |= Opcodes.ACC_FINAL;
+ }
+ }
+ MethodVisitor dest = super.visitMethod(access, name, desc, signature, exceptions);
+ return new InvokedynamicRewriter(dest);
+ }
+
+ /**
+ * Makes {@link #visitEnd} generate a bridge method for the given method handle if the
+ * referenced method will be invisible to the generated lambda class.
+ *
+ * @return struct containing either {@code invokedMethod} or {@code invokedMethod} and a handle
+ * representing the bridge method that will be generated for {@code invokedMethod}.
+ */
+ private MethodReferenceBridgeInfo queueUpBridgeMethodIfNeeded(Handle invokedMethod)
+ throws ClassNotFoundException {
+ if (invokedMethod.getName().startsWith("lambda$")) {
+ // We adjust lambda bodies to be visible
+ return MethodReferenceBridgeInfo.noBridge(invokedMethod);
+ }
+
+ // invokedMethod is a method reference if we get here
+ Executable invoked = findTargetMethod(invokedMethod);
+ if (isVisibleToLambdaClass(invoked, invokedMethod.getOwner())) {
+ // Referenced method is visible to the generated class, so nothing to do
+ return MethodReferenceBridgeInfo.noBridge(invokedMethod);
+ }
+
+ // We need a bridge method if we get here
+ checkState(!isInterface,
+ "%s is an interface and shouldn't need bridge to %s", internalName, invokedMethod);
+ checkState(!invokedMethod.isInterface(),
+ "%s's lambda classes can't see interface method: %s", internalName, invokedMethod);
+ MethodReferenceBridgeInfo result = bridgeMethods.get(invokedMethod);
+ if (result != null) {
+ return result; // we're already queued up a bridge method for this method reference
+ }
+
+ String name = "bridge$lambda$" + bridgeMethods.size();
+ Handle bridgeMethod;
+ switch (invokedMethod.getTag()) {
+ case Opcodes.H_INVOKESTATIC:
+ bridgeMethod = new Handle(invokedMethod.getTag(), internalName, name,
+ invokedMethod.getDesc(), /*itf*/ false);
+ break;
+ case Opcodes.H_INVOKEVIRTUAL:
+ case Opcodes.H_INVOKESPECIAL: // we end up calling these using invokevirtual
+ bridgeMethod = new Handle(Opcodes.H_INVOKEVIRTUAL, internalName, name,
+ invokedMethod.getDesc(), /*itf*/ false);
+ break;
+ case Opcodes.H_NEWINVOKESPECIAL: {
+ // Call invisible constructor through generated bridge "factory" method, so we need to
+ // compute the descriptor for the bridge method from the constructor's descriptor
+ String desc =
+ Type.getMethodDescriptor(
+ Type.getObjectType(invokedMethod.getOwner()),
+ Type.getArgumentTypes(invokedMethod.getDesc()));
+ bridgeMethod = new Handle(Opcodes.H_INVOKESTATIC, internalName, name, desc, /*itf*/ false);
+ break;
+ }
+ case Opcodes.H_INVOKEINTERFACE:
+ // Shouldn't get here
+ default:
+ throw new UnsupportedOperationException("Cannot bridge " + invokedMethod);
+ }
+ result = MethodReferenceBridgeInfo.bridge(invokedMethod, invoked, bridgeMethod);
+ MethodReferenceBridgeInfo old = bridgeMethods.put(invokedMethod, result);
+ checkState(old == null, "Already had bridge %s so we don't also want %s", old, result);
+ return result;
+ }
+
+ /**
+ * Checks whether the referenced method would be visible by an unrelated class in the same package
+ * as the currently visited class.
+ */
+ private boolean isVisibleToLambdaClass(Executable invoked, String owner) {
+ int modifiers = invoked.getModifiers();
+ if (Modifier.isPrivate(modifiers)) {
+ return false;
+ }
+ if (Modifier.isPublic(modifiers)) {
+ return true;
+ }
+ // invoked is protected or package-private, either way we need it to be in the same package
+ // because the additional visibility protected gives doesn't help lambda classes, which are in
+ // a different class hierarchy (and typically just extend Object)
+ return packageName(internalName).equals(packageName(owner));
+ }
+
+ private Executable findTargetMethod(Handle invokedMethod) throws ClassNotFoundException {
+ Type descriptor = Type.getMethodType(invokedMethod.getDesc());
+ Class<?> owner = loadFromInternal(invokedMethod.getOwner());
+ if (invokedMethod.getTag() == Opcodes.H_NEWINVOKESPECIAL) {
+ for (Constructor<?> c : owner.getDeclaredConstructors()) {
+ if (Type.getType(c).equals(descriptor)) {
+ return c;
+ }
+ }
+ } else {
+ for (Method m : owner.getDeclaredMethods()) {
+ if (m.getName().equals(invokedMethod.getName())
+ && Type.getType(m).equals(descriptor)) {
+ return m;
+ }
+ }
+ }
+ throw new IllegalArgumentException("Referenced method not found: " + invokedMethod);
+ }
+
+ private Class<?> loadFromInternal(String internalName) throws ClassNotFoundException {
+ return targetLoader.loadClass(internalName.replace('/', '.'));
+ }
+
+ static int invokeOpcode(Handle invokedMethod) {
+ switch (invokedMethod.getTag()) {
+ case Opcodes.H_INVOKESTATIC:
+ return Opcodes.INVOKESTATIC;
+ case Opcodes.H_INVOKEVIRTUAL:
+ return Opcodes.INVOKEVIRTUAL;
+ case Opcodes.H_INVOKESPECIAL:
+ case Opcodes.H_NEWINVOKESPECIAL: // Must be preceded by NEW
+ return Opcodes.INVOKESPECIAL;
+ case Opcodes.H_INVOKEINTERFACE:
+ return Opcodes.INVOKEINTERFACE;
+ default:
+ throw new UnsupportedOperationException("Don't know how to call " + invokedMethod);
+ }
+ }
+
+ private static String[] toInternalNames(Class<?>[] classes) {
+ String[] result = new String[classes.length];
+ for (int i = 0; i < classes.length; ++i) {
+ result[i] = Type.getInternalName(classes[i]);
+ }
+ return result;
+ }
+
+ private static String packageName(String internalClassName) {
+ int lastSlash = internalClassName.lastIndexOf('/');
+ return lastSlash > 0 ? internalClassName.substring(0, lastSlash) : "";
+ }
+
+ /**
+ * Desugaring that replaces invokedynamics for {@link java.lang.invoke.LambdaMetafactory} with
+ * static factory method invocations and triggers a class to be generated for each invokedynamic.
+ */
+ private class InvokedynamicRewriter extends MethodVisitor {
+
+ public InvokedynamicRewriter(MethodVisitor dest) {
+ super(ASM5, dest);
+ }
+
+ @Override
+ public void visitInvokeDynamicInsn(String name, String desc, Handle bsm, Object... bsmArgs) {
+ if (!"java/lang/invoke/LambdaMetafactory".equals(bsm.getOwner())) {
+ // Not an invokedynamic for a lambda expression
+ super.visitInvokeDynamicInsn(name, desc, bsm, bsmArgs);
+ return;
+ }
+
+ try {
+ Lookup lookup = createLookup(internalName);
+ ArrayList<Object> args = new ArrayList<>(bsmArgs.length + 3);
+ args.add(lookup);
+ args.add(name);
+ args.add(MethodType.fromMethodDescriptorString(desc, targetLoader));
+ for (Object bsmArg : bsmArgs) {
+ args.add(toJvmMetatype(lookup, bsmArg));
+ }
+
+ // Both bootstrap methods in LambdaMetafactory expect a MethodHandle as their 5th argument
+ // so we can assume bsmArgs[1] (the 5th arg) to be a Handle.
+ MethodReferenceBridgeInfo bridgeInfo = queueUpBridgeMethodIfNeeded((Handle) bsmArgs[1]);
+
+ // Resolve the bootstrap method in "host configuration" (this tool's default classloader)
+ // since targetLoader may only contain stubs that we can't actually execute.
+ // generateLambdaClass() below will invoke the bootstrap method, so a stub isn't enough,
+ // and ultimately we don't care if the bootstrap method was even on the bootclasspath
+ // when this class was compiled (although it must've been since javac is unhappy otherwise).
+ MethodHandle bsmMethod = toMethodHandle(publicLookup(), bsm, /*target*/ false);
+ String lambdaClassName = lambdas.generateLambdaClass(
+ internalName,
+ LambdaInfo.create(desc, bridgeInfo.methodReference(), bridgeInfo.bridgeMethod()),
+ bsmMethod,
+ args);
+ // Emit invokestatic that calls the factory method generated in the lambda class
+ super.visitMethodInsn(
+ Opcodes.INVOKESTATIC,
+ lambdaClassName,
+ LambdaClassFixer.FACTORY_METHOD_NAME,
+ desc,
+ /*itf*/ false);
+ } catch (IOException | ReflectiveOperationException e) {
+ throw new IllegalStateException("Couldn't desugar invokedynamic for " + internalName + "."
+ + name + " using " + bsm + " with arguments " + Arrays.toString(bsmArgs), e);
+ }
+ }
+
+ private Lookup createLookup(String lookupClass) throws ReflectiveOperationException {
+ Class<?> clazz = loadFromInternal(lookupClass);
+ Constructor<Lookup> constructor = Lookup.class.getDeclaredConstructor(Class.class);
+ constructor.setAccessible(true);
+ return constructor.newInstance(clazz);
+ }
+
+ /**
+ * Produces a {@link MethodHandle} or {@link MethodType} using {@link #targetLoader} for the
+ * given ASM {@link Handle} or {@link Type}. {@code lookup} is only used for resolving
+ * {@link Handle}s.
+ */
+ private Object toJvmMetatype(Lookup lookup, Object asm) throws ReflectiveOperationException {
+ if (asm instanceof Number) {
+ return asm;
+ }
+ if (asm instanceof Type) {
+ Type type = (Type) asm;
+ switch (type.getSort()) {
+ case Type.OBJECT:
+ return loadFromInternal(type.getInternalName());
+ case Type.METHOD:
+ return MethodType.fromMethodDescriptorString(type.getDescriptor(), targetLoader);
+ default:
+ throw new IllegalArgumentException("Cannot convert: " + asm);
+ }
+ }
+ if (asm instanceof Handle) {
+ return toMethodHandle(lookup, (Handle) asm, /*target*/ true);
+ }
+ throw new IllegalArgumentException("Cannot convert: " + asm);
+ }
+
+ /**
+ * Produces a {@link MethodHandle} using either the context or {@link #targetLoader} class
+ * loader, depending on {@code target}.
+ */
+ private MethodHandle toMethodHandle(Lookup lookup, Handle asmHandle, boolean target)
+ throws ReflectiveOperationException {
+ Class<?> owner = loadFromInternal(asmHandle.getOwner());
+ MethodType signature = MethodType.fromMethodDescriptorString(asmHandle.getDesc(),
+ target ? targetLoader : Thread.currentThread().getContextClassLoader());
+ switch (asmHandle.getTag()) {
+ case Opcodes.H_INVOKESTATIC:
+ return lookup.findStatic(owner, asmHandle.getName(), signature);
+ case Opcodes.H_INVOKEVIRTUAL:
+ case Opcodes.H_INVOKESPECIAL: // we end up calling these using invokevirtual
+ case Opcodes.H_INVOKEINTERFACE:
+ return lookup.findVirtual(owner, asmHandle.getName(), signature);
+ case Opcodes.H_NEWINVOKESPECIAL:
+ return lookup.findConstructor(owner, signature);
+ default:
+ throw new UnsupportedOperationException("Cannot resolve " + asmHandle);
+ }
+ }
+ }
+
+ /**
+ * Record of how a lambda class can reach its referenced method through a possibly-different
+ * bridge method.
+ *
+ * <p>In a JVM, lambda classes are allowed to call the referenced methods directly, but we don't
+ * have that luxury when the generated lambda class is evaluated using normal visibility rules.
+ */
+ @AutoValue
+ abstract static class MethodReferenceBridgeInfo {
+ public static MethodReferenceBridgeInfo noBridge(Handle methodReference) {
+ return new AutoValue_LambdaDesugaring_MethodReferenceBridgeInfo(
+ methodReference, (Executable) null, methodReference);
+ }
+ public static MethodReferenceBridgeInfo bridge(
+ Handle methodReference, Executable referenced, Handle bridgeMethod) {
+ checkArgument(!bridgeMethod.equals(methodReference));
+ return new AutoValue_LambdaDesugaring_MethodReferenceBridgeInfo(
+ methodReference, checkNotNull(referenced), bridgeMethod);
+ }
+
+ public abstract Handle methodReference();
+
+ /** Returns {@code null} iff {@link #bridgeMethod} equals {@link #methodReference}. */
+ @Nullable public abstract Executable referenced();
+
+ public abstract Handle bridgeMethod();
+ }
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaInfo.java b/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaInfo.java
new file mode 100644
index 0000000000..e3bb84f891
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaInfo.java
@@ -0,0 +1,30 @@
+// 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;
+
+import com.google.auto.value.AutoValue;
+import org.objectweb.asm.Handle;
+
+@AutoValue
+abstract class LambdaInfo {
+ public static LambdaInfo create(
+ String factoryMethodDesc, Handle methodReference, Handle bridgeMethod) {
+ return new AutoValue_LambdaInfo(
+ factoryMethodDesc, methodReference, bridgeMethod);
+ }
+
+ public abstract String factoryMethodDesc();
+ public abstract Handle methodReference();
+ public abstract Handle bridgeMethod();
+}