| // 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); |
| } |
| } |
| } |
| } |