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