Home | History | Annotate | Download | only in desugar
      1 // Copyright 2017 The Bazel Authors. All rights reserved.
      2 //
      3 // Licensed under the Apache License, Version 2.0 (the "License");
      4 // you may not use this file except in compliance with the License.
      5 // You may obtain a copy of the License at
      6 //
      7 //    http://www.apache.org/licenses/LICENSE-2.0
      8 //
      9 // Unless required by applicable law or agreed to in writing, software
     10 // distributed under the License is distributed on an "AS IS" BASIS,
     11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 // See the License for the specific language governing permissions and
     13 // limitations under the License.
     14 package com.google.devtools.build.android.desugar;
     15 
     16 import static com.google.common.base.Preconditions.checkNotNull;
     17 import static com.google.common.base.Preconditions.checkState;
     18 import static org.objectweb.asm.Opcodes.ACC_STATIC;
     19 import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC;
     20 import static org.objectweb.asm.Opcodes.ASM6;
     21 import static org.objectweb.asm.Opcodes.INVOKEINTERFACE;
     22 import static org.objectweb.asm.Opcodes.INVOKESTATIC;
     23 import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
     24 
     25 import com.google.common.base.Function;
     26 import com.google.common.base.Preconditions;
     27 import com.google.common.collect.FluentIterable;
     28 import com.google.common.collect.ImmutableMap;
     29 import com.google.common.collect.ImmutableMultimap;
     30 import com.google.common.collect.ImmutableSet;
     31 import com.google.devtools.build.android.desugar.BytecodeTypeInference.InferredType;
     32 import java.util.Collections;
     33 import java.util.LinkedHashSet;
     34 import java.util.Optional;
     35 import java.util.Set;
     36 import java.util.concurrent.atomic.AtomicInteger;
     37 import javax.annotation.Nullable;
     38 import org.objectweb.asm.ClassVisitor;
     39 import org.objectweb.asm.Label;
     40 import org.objectweb.asm.MethodVisitor;
     41 import org.objectweb.asm.commons.ClassRemapper;
     42 import org.objectweb.asm.commons.Remapper;
     43 import org.objectweb.asm.tree.MethodNode;
     44 
     45 /**
     46  * Desugar try-with-resources. This class visitor intercepts calls to the following methods, and
     47  * redirect them to ThrowableExtension.
     48  * <li>{@code Throwable.addSuppressed(Throwable)}
     49  * <li>{@code Throwable.getSuppressed()}
     50  * <li>{@code Throwable.printStackTrace()}
     51  * <li>{@code Throwable.printStackTrace(PrintStream)}
     52  * <li>{@code Throwable.printStackTrace(PringWriter)}
     53  */
     54 public class TryWithResourcesRewriter extends ClassVisitor {
     55 
     56   private static final String RUNTIME_PACKAGE_INTERNAL_NAME =
     57       "com/google/devtools/build/android/desugar/runtime";
     58 
     59   static final String THROWABLE_EXTENSION_INTERNAL_NAME =
     60       RUNTIME_PACKAGE_INTERNAL_NAME + '/' + "ThrowableExtension";
     61 
     62   /** The extension classes for java.lang.Throwable. */
     63   static final ImmutableSet<String> THROWABLE_EXT_CLASS_INTERNAL_NAMES =
     64       ImmutableSet.of(
     65           THROWABLE_EXTENSION_INTERNAL_NAME,
     66           THROWABLE_EXTENSION_INTERNAL_NAME + "$AbstractDesugaringStrategy",
     67           THROWABLE_EXTENSION_INTERNAL_NAME + "$ConcurrentWeakIdentityHashMap",
     68           THROWABLE_EXTENSION_INTERNAL_NAME + "$ConcurrentWeakIdentityHashMap$WeakKey",
     69           THROWABLE_EXTENSION_INTERNAL_NAME + "$MimicDesugaringStrategy",
     70           THROWABLE_EXTENSION_INTERNAL_NAME + "$NullDesugaringStrategy",
     71           THROWABLE_EXTENSION_INTERNAL_NAME + "$ReuseDesugaringStrategy");
     72 
     73   /** The extension classes for java.lang.Throwable. All the names end with ".class" */
     74   static final ImmutableSet<String> THROWABLE_EXT_CLASS_INTERNAL_NAMES_WITH_CLASS_EXT =
     75       FluentIterable.from(THROWABLE_EXT_CLASS_INTERNAL_NAMES)
     76           .transform(
     77               new Function<String, String>() {
     78                 @Override
     79                 public String apply(String s) {
     80                   return s + ".class";
     81                 }
     82               })
     83           .toSet();
     84 
     85   static final ImmutableMultimap<String, String> TARGET_METHODS =
     86       ImmutableMultimap.<String, String>builder()
     87           .put("addSuppressed", "(Ljava/lang/Throwable;)V")
     88           .put("getSuppressed", "()[Ljava/lang/Throwable;")
     89           .put("printStackTrace", "()V")
     90           .put("printStackTrace", "(Ljava/io/PrintStream;)V")
     91           .put("printStackTrace", "(Ljava/io/PrintWriter;)V")
     92           .build();
     93 
     94   static final ImmutableMap<String, String> METHOD_DESC_MAP =
     95       ImmutableMap.<String, String>builder()
     96           .put("(Ljava/lang/Throwable;)V", "(Ljava/lang/Throwable;Ljava/lang/Throwable;)V")
     97           .put("()[Ljava/lang/Throwable;", "(Ljava/lang/Throwable;)[Ljava/lang/Throwable;")
     98           .put("()V", "(Ljava/lang/Throwable;)V")
     99           .put("(Ljava/io/PrintStream;)V", "(Ljava/lang/Throwable;Ljava/io/PrintStream;)V")
    100           .put("(Ljava/io/PrintWriter;)V", "(Ljava/lang/Throwable;Ljava/io/PrintWriter;)V")
    101           .build();
    102 
    103   static final String CLOSE_RESOURCE_METHOD_NAME = "$closeResource";
    104   static final String CLOSE_RESOURCE_METHOD_DESC =
    105       "(Ljava/lang/Throwable;Ljava/lang/AutoCloseable;)V";
    106 
    107   private final ClassLoader classLoader;
    108   private final Set<String> visitedExceptionTypes;
    109   private final AtomicInteger numOfTryWithResourcesInvoked;
    110   /** Stores the internal class names of resources that need to be closed. */
    111   private final LinkedHashSet<String> resourceTypeInternalNames = new LinkedHashSet<>();
    112 
    113   private final boolean hasCloseResourceMethod;
    114 
    115   private String internalName;
    116   /**
    117    * Indicate whether the current class being desugared should be ignored. If the current class is
    118    * one of the runtime extension classes, then it should be ignored.
    119    */
    120   private boolean shouldCurrentClassBeIgnored;
    121   /**
    122    * A method node for $closeResource(Throwable, AutoCloseable). At then end, we specialize this
    123    * method node.
    124    */
    125   @Nullable private MethodNode closeResourceMethod;
    126 
    127   public TryWithResourcesRewriter(
    128       ClassVisitor classVisitor,
    129       ClassLoader classLoader,
    130       Set<String> visitedExceptionTypes,
    131       AtomicInteger numOfTryWithResourcesInvoked,
    132       boolean hasCloseResourceMethod) {
    133     super(ASM6, classVisitor);
    134     this.classLoader = classLoader;
    135     this.visitedExceptionTypes = visitedExceptionTypes;
    136     this.numOfTryWithResourcesInvoked = numOfTryWithResourcesInvoked;
    137     this.hasCloseResourceMethod = hasCloseResourceMethod;
    138   }
    139 
    140   @Override
    141   public void visit(
    142       int version,
    143       int access,
    144       String name,
    145       String signature,
    146       String superName,
    147       String[] interfaces) {
    148     super.visit(version, access, name, signature, superName, interfaces);
    149     internalName = name;
    150     shouldCurrentClassBeIgnored = THROWABLE_EXT_CLASS_INTERNAL_NAMES.contains(name);
    151     Preconditions.checkState(
    152         !shouldCurrentClassBeIgnored || !hasCloseResourceMethod,
    153         "The current class which will be ignored "
    154             + "contains $closeResource(Throwable, AutoCloseable).");
    155   }
    156 
    157   @Override
    158   public void visitEnd() {
    159     if (!resourceTypeInternalNames.isEmpty()) {
    160       checkNotNull(closeResourceMethod);
    161       for (String resourceInternalName : resourceTypeInternalNames) {
    162         boolean isInterface = isInterface(resourceInternalName.replace('/', '.'));
    163         // We use "this" to desugar the body of the close resource method.
    164         closeResourceMethod.accept(
    165             new CloseResourceMethodSpecializer(cv, resourceInternalName, isInterface));
    166       }
    167     } else {
    168       checkState(
    169           closeResourceMethod == null,
    170           "The field resourceTypeInternalNames is empty. "
    171               + "But the class has the $closeResource method.");
    172       checkState(
    173           !hasCloseResourceMethod,
    174           "The class %s has close resource method, but resourceTypeInternalNames is empty.",
    175           internalName);
    176     }
    177     super.visitEnd();
    178   }
    179 
    180   @Override
    181   public MethodVisitor visitMethod(
    182       int access, String name, String desc, String signature, String[] exceptions) {
    183     if (exceptions != null && exceptions.length > 0) {
    184       // collect exception types.
    185       Collections.addAll(visitedExceptionTypes, exceptions);
    186     }
    187     if (isSyntheticCloseResourceMethod(access, name, desc)) {
    188       checkState(closeResourceMethod == null, "The TWR rewriter has been used.");
    189       closeResourceMethod = new MethodNode(ASM6, access, name, desc, signature, exceptions);
    190       // Run the TWR desugar pass over the $closeResource(Throwable, AutoCloseable) first, for
    191       // example, to rewrite calls to AutoCloseable.close()..
    192       TryWithResourceVisitor twrVisitor =
    193           new TryWithResourceVisitor(
    194               internalName, name + desc, closeResourceMethod, classLoader, null);
    195       return twrVisitor;
    196     }
    197 
    198     MethodVisitor visitor = super.cv.visitMethod(access, name, desc, signature, exceptions);
    199     if (visitor == null || shouldCurrentClassBeIgnored) {
    200       return visitor;
    201     }
    202 
    203     BytecodeTypeInference inference = null;
    204     if (hasCloseResourceMethod) {
    205       /*
    206        * BytecodeTypeInference will run after the TryWithResourceVisitor, because when we are
    207        * processing a bytecode instruction, we need to know the types in the operand stack, which
    208        * are inferred after the previous instruction.
    209        */
    210       inference = new BytecodeTypeInference(access, internalName, name, desc);
    211       inference.setDelegateMethodVisitor(visitor);
    212       visitor = inference;
    213     }
    214 
    215     TryWithResourceVisitor twrVisitor =
    216         new TryWithResourceVisitor(internalName, name + desc, visitor, classLoader, inference);
    217     return twrVisitor;
    218   }
    219 
    220   public static boolean isSyntheticCloseResourceMethod(int access, String name, String desc) {
    221     return BitFlags.isSet(access, ACC_SYNTHETIC | ACC_STATIC)
    222         && CLOSE_RESOURCE_METHOD_NAME.equals(name)
    223         && CLOSE_RESOURCE_METHOD_DESC.equals(desc);
    224   }
    225 
    226   private boolean isInterface(String className) {
    227     try {
    228       Class<?> klass = classLoader.loadClass(className);
    229       return klass.isInterface();
    230     } catch (ClassNotFoundException e) {
    231       throw new AssertionError("Failed to load class when desugaring class " + internalName);
    232     }
    233   }
    234 
    235   public static boolean isCallToSyntheticCloseResource(
    236       String currentClassInternalName, int opcode, String owner, String name, String desc) {
    237     if (opcode != INVOKESTATIC) {
    238       return false;
    239     }
    240     if (!currentClassInternalName.equals(owner)) {
    241       return false;
    242     }
    243     if (!CLOSE_RESOURCE_METHOD_NAME.equals(name)) {
    244       return false;
    245     }
    246     if (!CLOSE_RESOURCE_METHOD_DESC.equals(desc)) {
    247       return false;
    248     }
    249     return true;
    250   }
    251 
    252   private class TryWithResourceVisitor extends MethodVisitor {
    253 
    254     private final ClassLoader classLoader;
    255     /** For debugging purpose. Enrich exception information. */
    256     private final String internalName;
    257 
    258     private final String methodSignature;
    259     @Nullable private final BytecodeTypeInference typeInference;
    260 
    261     public TryWithResourceVisitor(
    262         String internalName,
    263         String methodSignature,
    264         MethodVisitor methodVisitor,
    265         ClassLoader classLoader,
    266         @Nullable BytecodeTypeInference typeInference) {
    267       super(ASM6, methodVisitor);
    268       this.classLoader = classLoader;
    269       this.internalName = internalName;
    270       this.methodSignature = methodSignature;
    271       this.typeInference = typeInference;
    272     }
    273 
    274     @Override
    275     public void visitTryCatchBlock(Label start, Label end, Label handler, String type) {
    276       if (type != null) {
    277         visitedExceptionTypes.add(type); // type in a try-catch block must extend Throwable.
    278       }
    279       super.visitTryCatchBlock(start, end, handler, type);
    280     }
    281 
    282     @Override
    283     public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
    284       if (isCallToSyntheticCloseResource(internalName, opcode, owner, name, desc)) {
    285         checkNotNull(
    286             typeInference,
    287             "This method %s.%s has a call to $closeResource(Throwable, AutoCloseable) method, "
    288                 + "but the type inference is null.",
    289             internalName,
    290             methodSignature);
    291         {
    292           // Check the exception type.
    293           InferredType exceptionClass = typeInference.getTypeOfOperandFromTop(1);
    294           if (!exceptionClass.isNull()) {
    295             Optional<String> exceptionClassInternalName = exceptionClass.getInternalName();
    296             checkState(
    297                 exceptionClassInternalName.isPresent(),
    298                 "The exception %s is not a reference type in %s.%s",
    299                 exceptionClass,
    300                 internalName,
    301                 methodSignature);
    302             checkState(
    303                 isAssignableFrom(
    304                     "java.lang.Throwable", exceptionClassInternalName.get().replace('/', '.')),
    305                 "The exception type %s in %s.%s should be a subclass of java.lang.Throwable.",
    306                 exceptionClassInternalName,
    307                 internalName,
    308                 methodSignature);
    309           }
    310         }
    311 
    312         InferredType resourceType = typeInference.getTypeOfOperandFromTop(0);
    313         Optional<String> resourceClassInternalName = resourceType.getInternalName();
    314         checkState(
    315             resourceClassInternalName.isPresent(),
    316             "The resource class %s is not a reference type in %s.%s",
    317             resourceType,
    318             internalName,
    319             methodSignature);
    320         checkState(
    321             isAssignableFrom(
    322                 "java.lang.AutoCloseable", resourceClassInternalName.get().replace('/', '.')),
    323             "The resource type should be a subclass of java.lang.AutoCloseable: %s",
    324             resourceClassInternalName);
    325 
    326         resourceTypeInternalNames.add(resourceClassInternalName.get());
    327         super.visitMethodInsn(
    328             opcode,
    329             owner,
    330             "$closeResource",
    331             "(Ljava/lang/Throwable;L" + resourceClassInternalName.get() + ";)V",
    332             itf);
    333         return;
    334       }
    335 
    336       if (!isMethodCallTargeted(opcode, owner, name, desc)) {
    337         super.visitMethodInsn(opcode, owner, name, desc, itf);
    338         return;
    339       }
    340       numOfTryWithResourcesInvoked.incrementAndGet();
    341       visitedExceptionTypes.add(checkNotNull(owner)); // owner extends Throwable.
    342       super.visitMethodInsn(
    343           INVOKESTATIC, THROWABLE_EXTENSION_INTERNAL_NAME, name, METHOD_DESC_MAP.get(desc), false);
    344     }
    345 
    346     private boolean isMethodCallTargeted(int opcode, String owner, String name, String desc) {
    347       if (opcode != INVOKEVIRTUAL) {
    348         return false;
    349       }
    350       if (!TARGET_METHODS.containsEntry(name, desc)) {
    351         return false;
    352       }
    353       if (visitedExceptionTypes.contains(owner)) {
    354         return true; // The owner is an exception that has been visited before.
    355       }
    356       return isAssignableFrom("java.lang.Throwable", owner.replace('/', '.'));
    357     }
    358 
    359     private boolean isAssignableFrom(String baseClassName, String subClassName) {
    360       try {
    361         Class<?> baseClass = classLoader.loadClass(baseClassName);
    362         Class<?> subClass = classLoader.loadClass(subClassName);
    363         return baseClass.isAssignableFrom(subClass);
    364       } catch (ClassNotFoundException e) {
    365         throw new AssertionError(
    366             "Failed to load class when desugaring method "
    367                 + internalName
    368                 + "."
    369                 + methodSignature
    370                 + " when checking the assignable relation for class "
    371                 + baseClassName
    372                 + " and "
    373                 + subClassName,
    374             e);
    375       }
    376     }
    377   }
    378 
    379   /**
    380    * A class to specialize the method $closeResource(Throwable, AutoCloseable), which does
    381    *
    382    * <ul>
    383    *   <li>Rename AutoCloseable to the given concrete resource type.
    384    *   <li>Adjust the invoke instruction that calls AutoCloseable.close()
    385    * </ul>
    386    */
    387   private static class CloseResourceMethodSpecializer extends ClassRemapper {
    388 
    389     private final boolean isResourceAnInterface;
    390     private final String targetResourceInternalName;
    391 
    392     public CloseResourceMethodSpecializer(
    393         ClassVisitor cv, String targetResourceInternalName, boolean isResourceAnInterface) {
    394       super(
    395           cv,
    396           new Remapper() {
    397             @Override
    398             public String map(String typeName) {
    399               if (typeName.equals("java/lang/AutoCloseable")) {
    400                 return targetResourceInternalName;
    401               } else {
    402                 return typeName;
    403               }
    404             }
    405           });
    406       this.targetResourceInternalName = targetResourceInternalName;
    407       this.isResourceAnInterface = isResourceAnInterface;
    408     }
    409 
    410     @Override
    411     public MethodVisitor visitMethod(
    412         int access, String name, String desc, String signature, String[] exceptions) {
    413       MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
    414       return new MethodVisitor(ASM6, mv) {
    415         @Override
    416         public void visitMethodInsn(
    417             int opcode, String owner, String name, String desc, boolean itf) {
    418           if (opcode == INVOKEINTERFACE
    419               && owner.endsWith("java/lang/AutoCloseable")
    420               && name.equals("close")
    421               && desc.equals("()V")
    422               && itf) {
    423             opcode = isResourceAnInterface ? INVOKEINTERFACE : INVOKEVIRTUAL;
    424             owner = targetResourceInternalName;
    425             itf = isResourceAnInterface;
    426           }
    427           super.visitMethodInsn(opcode, owner, name, desc, itf);
    428         }
    429       };
    430     }
    431   }
    432 }
    433