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