blob: 507b7d012ba917c5b331789038ba1bc0cec067cc [file] [log] [blame]
// Copyright 2017 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.devtools.build.android.desugar;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.objectweb.asm.Opcodes.ASM6;
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
import com.google.common.base.Function;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
/**
* Desugar try-with-resources. This class visitor intercepts calls to the following methods, and
* redirect them to ThrowableExtension.
* <li>{@code Throwable.addSuppressed(Throwable)}
* <li>{@code Throwable.getSuppressed()}
* <li>{@code Throwable.printStackTrace()}
* <li>{@code Throwable.printStackTrace(PrintStream)}
* <li>{@code Throwable.printStackTrace(PringWriter)}
*/
public class TryWithResourcesRewriter extends ClassVisitor {
private static final String RUNTIME_PACKAGE_INTERNAL_NAME =
"com/google/devtools/build/android/desugar/runtime";
static final String THROWABLE_EXTENSION_INTERNAL_NAME =
RUNTIME_PACKAGE_INTERNAL_NAME + '/' + "ThrowableExtension";
/** The extension classes for java.lang.Throwable. */
static final ImmutableSet<String> THROWABLE_EXT_CLASS_INTERNAL_NAMES =
ImmutableSet.of(
THROWABLE_EXTENSION_INTERNAL_NAME,
THROWABLE_EXTENSION_INTERNAL_NAME + "$AbstractDesugaringStrategy",
THROWABLE_EXTENSION_INTERNAL_NAME + "$ConcurrentWeakIdentityHashMap",
THROWABLE_EXTENSION_INTERNAL_NAME + "$ConcurrentWeakIdentityHashMap$WeakKey",
THROWABLE_EXTENSION_INTERNAL_NAME + "$MimicDesugaringStrategy",
THROWABLE_EXTENSION_INTERNAL_NAME + "$NullDesugaringStrategy",
THROWABLE_EXTENSION_INTERNAL_NAME + "$ReuseDesugaringStrategy");
/** The extension classes for java.lang.Throwable. All the names end with ".class" */
static final ImmutableSet<String> THROWABLE_EXT_CLASS_INTERNAL_NAMES_WITH_CLASS_EXT =
FluentIterable.from(THROWABLE_EXT_CLASS_INTERNAL_NAMES)
.transform(
new Function<String, String>() {
@Override
public String apply(String s) {
return s + ".class";
}
})
.toSet();
static final ImmutableMultimap<String, String> TARGET_METHODS =
ImmutableMultimap.<String, String>builder()
.put("addSuppressed", "(Ljava/lang/Throwable;)V")
.put("getSuppressed", "()[Ljava/lang/Throwable;")
.put("printStackTrace", "()V")
.put("printStackTrace", "(Ljava/io/PrintStream;)V")
.put("printStackTrace", "(Ljava/io/PrintWriter;)V")
.build();
static final ImmutableMap<String, String> METHOD_DESC_MAP =
ImmutableMap.<String, String>builder()
.put("(Ljava/lang/Throwable;)V", "(Ljava/lang/Throwable;Ljava/lang/Throwable;)V")
.put("()[Ljava/lang/Throwable;", "(Ljava/lang/Throwable;)[Ljava/lang/Throwable;")
.put("()V", "(Ljava/lang/Throwable;)V")
.put("(Ljava/io/PrintStream;)V", "(Ljava/lang/Throwable;Ljava/io/PrintStream;)V")
.put("(Ljava/io/PrintWriter;)V", "(Ljava/lang/Throwable;Ljava/io/PrintWriter;)V")
.build();
private final ClassLoader classLoader;
private final Set<String> visitedExceptionTypes;
private final AtomicInteger numOfTryWithResourcesInvoked;
private String internalName;
/**
* Indicate whether the current class being desugared should be ignored. If the current class is
* one of the runtime extension classes, then it should be ignored.
*/
private boolean shouldCurrentClassBeIgnored;
public TryWithResourcesRewriter(
ClassVisitor classVisitor,
ClassLoader classLoader,
Set<String> visitedExceptionTypes,
AtomicInteger numOfTryWithResourcesInvoked) {
super(ASM6, classVisitor);
this.classLoader = classLoader;
this.visitedExceptionTypes = visitedExceptionTypes;
this.numOfTryWithResourcesInvoked = numOfTryWithResourcesInvoked;
}
@Override
public void visit(
int version,
int access,
String name,
String signature,
String superName,
String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
internalName = name;
shouldCurrentClassBeIgnored = THROWABLE_EXT_CLASS_INTERNAL_NAMES.contains(name);
}
@Override
public MethodVisitor visitMethod(
int access, String name, String desc, String signature, String[] exceptions) {
if (exceptions != null && exceptions.length > 0) {
// collect exception types.
Collections.addAll(visitedExceptionTypes, exceptions);
}
MethodVisitor visitor = super.cv.visitMethod(access, name, desc, signature, exceptions);
return visitor == null || shouldCurrentClassBeIgnored
? visitor
: new TryWithResourceVisitor(internalName + "." + name + desc, visitor, classLoader);
}
private class TryWithResourceVisitor extends MethodVisitor {
private final ClassLoader classLoader;
/** For debugging purpose. Enrich exception information. */
private final String methodSignature;
public TryWithResourceVisitor(
String methodSignature, MethodVisitor methodVisitor, ClassLoader classLoader) {
super(ASM6, methodVisitor);
this.classLoader = classLoader;
this.methodSignature = methodSignature;
}
@Override
public void visitTryCatchBlock(Label start, Label end, Label handler, String type) {
if (type != null) {
visitedExceptionTypes.add(type); // type in a try-catch block must extend Throwable.
}
super.visitTryCatchBlock(start, end, handler, type);
}
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
if (!isMethodCallTargeted(opcode, owner, name, desc)) {
super.visitMethodInsn(opcode, owner, name, desc, itf);
return;
}
numOfTryWithResourcesInvoked.incrementAndGet();
visitedExceptionTypes.add(checkNotNull(owner)); // owner extends Throwable.
super.visitMethodInsn(
INVOKESTATIC, THROWABLE_EXTENSION_INTERNAL_NAME, name, METHOD_DESC_MAP.get(desc), false);
}
private boolean isMethodCallTargeted(int opcode, String owner, String name, String desc) {
if (opcode != INVOKEVIRTUAL) {
return false;
}
if (!TARGET_METHODS.containsEntry(name, desc)) {
return false;
}
if (visitedExceptionTypes.contains(owner)) {
return true; // The owner is an exception that has been visited before.
}
try {
Class<?> throwableClass = classLoader.loadClass("java.lang.Throwable");
Class<?> klass = classLoader.loadClass(owner.replace('/', '.'));
return throwableClass.isAssignableFrom(klass);
} catch (ClassNotFoundException e) {
throw new AssertionError(
"Failed to load class when desugaring method " + methodSignature, e);
}
}
}
}