blob: 2688892031ddd67fc02dafc3e398f432aa2c3b70 [file] [log] [blame]
// 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.
*
* <p>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.ASM7, 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);
// ASM uses the high 16 bits for the minor version:
// https://asm.ow2.io/javadoc/org/objectweb/asm/ClassVisitor.html#visit-int-int-java.lang.String-java.lang.String-java.lang.String-java.lang.String:A-
// See https://github.com/bazelbuild/bazel/issues/6299 for an example of a class file with a
// non-zero minor version.
int major = version & 0xffff;
if (major > Opcodes.V1_7) {
version = Opcodes.V1_7;
}
super.visit(version, 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) || "<clinit>".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 && "<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);
}
}
/** 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.ASM7, 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.ASM7, 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.ASM7);
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.ASM7, (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;
}
}
}