aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java
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/android/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java
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/android/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java')
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java409
1 files changed, 409 insertions, 0 deletions
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);
+ }
+ }
+}