// 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.devtools.build.android.desugar.io.BitFlags; import javax.annotation.Nullable; 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 tries to 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. *

The class version will 52 iff static interface method from the bootclasspath is invoked. * This is mostly ensure that the generated bytecode is valid. */ public class Java7Compatibility extends ClassVisitor { @Nullable private final ClassReaderFactory factory; @Nullable private final ClassReaderFactory bootclasspathReader; private boolean isInterface; private String internalName; private int access; private String signature; private String superName; private String[] interfaces; public Java7Compatibility( ClassVisitor cv, ClassReaderFactory factory, ClassReaderFactory bootclasspathReader) { super(Opcodes.ASM6, cv); this.factory = factory; this.bootclasspathReader = bootclasspathReader; } @Override public void visit( int version, int access, String name, String signature, String superName, String[] interfaces) { internalName = name; this.access = access; this.signature = signature; this.superName = superName; this.interfaces = interfaces; 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; } checkArgument(!isInterface || BitFlags.isSet(access, Opcodes.ACC_ABSTRACT) || "".equals(name), "Interface %s defines non-abstract method %s%s, which is not supported", internalName, name, desc); MethodVisitor result = new UpdateBytecodeVersionIfNecessary( super.visitMethod(access, name, desc, signature, exceptions)); return (isInterface && "".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); } } /** This will rewrite class version to 52 if it sees invokestatic on an interface. */ private class UpdateBytecodeVersionIfNecessary extends MethodVisitor { boolean updated = false; public UpdateBytecodeVersionIfNecessary(MethodVisitor methodVisitor) { super(Opcodes.ASM6, methodVisitor); } @Override public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { if (itf && opcode == Opcodes.INVOKESTATIC) { checkNotNull(bootclasspathReader); checkState( bootclasspathReader.isKnown(owner), "%s contains invocation of static interface method that is " + "not in the bootclasspath. Owner: %s, name: %s, desc: %s.", Java7Compatibility.this.internalName, owner, name, desc); if (!updated) { Java7Compatibility.this.cv.visit( Opcodes.V1_8, Java7Compatibility.this.access, Java7Compatibility.this.internalName, Java7Compatibility.this.signature, Java7Compatibility.this.superName, Java7Compatibility.this.interfaces); updated = true; } } super.visitMethodInsn(opcode, owner, name, desc, itf); } } private class InlineJacocoInit extends MethodVisitor { public InlineJacocoInit(MethodVisitor dest) { super(Opcodes.ASM6, 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)) { checkNotNull(factory); 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.ASM6); 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.ASM6, (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; } } }