1 package org.robolectric.internal.bytecode; 2 3 import static java.lang.invoke.MethodType.methodType; 4 5 import com.google.common.collect.Iterables; 6 import java.lang.invoke.CallSite; 7 import java.lang.invoke.MethodHandle; 8 import java.lang.invoke.MethodHandles; 9 import java.lang.invoke.MethodType; 10 import java.lang.reflect.Modifier; 11 import java.util.List; 12 import java.util.ListIterator; 13 import java.util.Objects; 14 import org.objectweb.asm.ClassReader; 15 import org.objectweb.asm.ClassWriter; 16 import org.objectweb.asm.ConstantDynamic; 17 import org.objectweb.asm.Handle; 18 import org.objectweb.asm.Label; 19 import org.objectweb.asm.MethodVisitor; 20 import org.objectweb.asm.Opcodes; 21 import org.objectweb.asm.Type; 22 import org.objectweb.asm.commons.ClassRemapper; 23 import org.objectweb.asm.commons.GeneratorAdapter; 24 import org.objectweb.asm.commons.JSRInlinerAdapter; 25 import org.objectweb.asm.commons.Method; 26 import org.objectweb.asm.commons.Remapper; 27 import org.objectweb.asm.tree.AbstractInsnNode; 28 import org.objectweb.asm.tree.ClassNode; 29 import org.objectweb.asm.tree.FieldInsnNode; 30 import org.objectweb.asm.tree.FieldNode; 31 import org.objectweb.asm.tree.InsnList; 32 import org.objectweb.asm.tree.InsnNode; 33 import org.objectweb.asm.tree.InvokeDynamicInsnNode; 34 import org.objectweb.asm.tree.JumpInsnNode; 35 import org.objectweb.asm.tree.LabelNode; 36 import org.objectweb.asm.tree.LdcInsnNode; 37 import org.objectweb.asm.tree.MethodInsnNode; 38 import org.objectweb.asm.tree.MethodNode; 39 import org.objectweb.asm.tree.TypeInsnNode; 40 import org.objectweb.asm.tree.VarInsnNode; 41 import org.robolectric.util.PerfStatsCollector; 42 43 /** 44 * Instruments (i.e. modifies the bytecode) of classes to place the scaffolding necessary to use 45 * Robolectric's shadows. 46 */ 47 public class ClassInstrumentor { 48 private static final Handle BOOTSTRAP_INIT; 49 private static final Handle BOOTSTRAP; 50 private static final Handle BOOTSTRAP_STATIC; 51 private static final Handle BOOTSTRAP_INTRINSIC; 52 private static final String ROBO_INIT_METHOD_NAME = "$$robo$init"; 53 protected static final Type OBJECT_TYPE = Type.getType(Object.class); 54 private static final ShadowImpl SHADOW_IMPL = new ShadowImpl(); 55 final Decorator decorator; 56 57 static { 58 String className = Type.getInternalName(InvokeDynamicSupport.class); 59 60 MethodType bootstrap = 61 methodType(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class); 62 63 /* 64 * There is an additional int.class argument to the invokedynamic bootstrap method. This conveys 65 * whether or not the method invocation represents a native method. A one means the original 66 * method was a native method, and a zero means it was not. It should be boolean.class, but 67 * that is nt possible due to https://bugs.java.com/bugdatabase/view_bug?bug_id=JDK-8322510. 68 */ 69 String bootstrapMethod = 70 bootstrap 71 .appendParameterTypes(MethodHandle.class, /* isNative */ int.class) 72 .toMethodDescriptorString(); 73 String bootstrapIntrinsic = 74 bootstrap.appendParameterTypes(String.class).toMethodDescriptorString(); 75 76 BOOTSTRAP_INIT = 77 new Handle( 78 Opcodes.H_INVOKESTATIC, 79 className, 80 "bootstrapInit", 81 bootstrap.toMethodDescriptorString(), 82 false); 83 BOOTSTRAP = new Handle(Opcodes.H_INVOKESTATIC, className, "bootstrap", bootstrapMethod, false); 84 BOOTSTRAP_STATIC = 85 new Handle(Opcodes.H_INVOKESTATIC, className, "bootstrapStatic", bootstrapMethod, false); 86 BOOTSTRAP_INTRINSIC = 87 new Handle( 88 Opcodes.H_INVOKESTATIC, className, "bootstrapIntrinsic", bootstrapIntrinsic, false); 89 } 90 ClassInstrumentor()91 public ClassInstrumentor() { 92 this(new ShadowDecorator()); 93 } 94 ClassInstrumentor(Decorator decorator)95 protected ClassInstrumentor(Decorator decorator) { 96 this.decorator = decorator; 97 } 98 analyzeClass( byte[] origClassBytes, final InstrumentationConfiguration config, ClassNodeProvider classNodeProvider)99 private MutableClass analyzeClass( 100 byte[] origClassBytes, 101 final InstrumentationConfiguration config, 102 ClassNodeProvider classNodeProvider) { 103 ClassNode classNode = 104 new ClassNode(Opcodes.ASM4) { 105 @Override 106 public MethodVisitor visitMethod( 107 int access, String name, String desc, String signature, String[] exceptions) { 108 MethodVisitor methodVisitor = 109 super.visitMethod(access, name, config.remapParams(desc), signature, exceptions); 110 return new JSRInlinerAdapter(methodVisitor, access, name, desc, signature, exceptions); 111 } 112 }; 113 114 final ClassReader classReader = new ClassReader(origClassBytes); 115 classReader.accept(classNode, 0); 116 return new MutableClass(classNode, config, classNodeProvider); 117 } 118 instrumentToBytes(MutableClass mutableClass)119 byte[] instrumentToBytes(MutableClass mutableClass) { 120 instrument(mutableClass); 121 122 ClassNode classNode = mutableClass.classNode; 123 ClassWriter writer = new InstrumentingClassWriter(mutableClass.classNodeProvider, classNode); 124 Remapper remapper = 125 new Remapper() { 126 @Override 127 public String map(final String internalName) { 128 return mutableClass.config.mappedTypeName(internalName); 129 } 130 }; 131 ClassRemapper visitor = new ClassRemapper(writer, remapper); 132 classNode.accept(visitor); 133 134 return writer.toByteArray(); 135 } 136 instrument( ClassDetails classDetails, InstrumentationConfiguration config, ClassNodeProvider classNodeProvider)137 public byte[] instrument( 138 ClassDetails classDetails, 139 InstrumentationConfiguration config, 140 ClassNodeProvider classNodeProvider) { 141 PerfStatsCollector perfStats = PerfStatsCollector.getInstance(); 142 MutableClass mutableClass = 143 perfStats.measure( 144 "analyze class", 145 () -> analyzeClass(classDetails.getClassBytes(), config, classNodeProvider)); 146 byte[] instrumentedBytes = 147 perfStats.measure("instrument class", () -> instrumentToBytes(mutableClass)); 148 recordPackageStats(perfStats, mutableClass); 149 return instrumentedBytes; 150 } 151 instrument(MutableClass mutableClass)152 public void instrument(MutableClass mutableClass) { 153 if (hasAnyJUnitAnnotation(mutableClass)) { 154 // Detected a JUnit related test class, skip instrumentation 155 return; 156 } 157 try { 158 // Need Java version >=7 to allow invokedynamic 159 mutableClass.classNode.version = Math.max(mutableClass.classNode.version, Opcodes.V1_7); 160 161 if (mutableClass.getName().equals("android.util.SparseArray")) { 162 addSetToSparseArray(mutableClass); 163 } 164 165 instrumentMethods(mutableClass); 166 167 if (mutableClass.isInterface()) { 168 mutableClass.addInterface(Type.getInternalName(InstrumentedInterface.class)); 169 } else { 170 makeClassPublic(mutableClass.classNode); 171 if ((mutableClass.classNode.access & Opcodes.ACC_FINAL) == Opcodes.ACC_FINAL) { 172 mutableClass 173 .classNode 174 .visitAnnotation("Lcom/google/errorprone/annotations/DoNotMock;", true) 175 .visit( 176 "value", 177 "This class is final. Consider either:\n" 178 + "1. Using the real class.\n" 179 + "2. If it's a pure data class, adding a Robolectric Builder for it.\n" 180 + "3. If it cannot function on the JVM, adding or enhancing a Robolectric" 181 + " Shadow for it"); 182 } 183 mutableClass.classNode.access = mutableClass.classNode.access & ~Opcodes.ACC_FINAL; 184 185 // If there is no constructor, adds one 186 addNoArgsConstructor(mutableClass); 187 188 addRoboInitMethod(mutableClass); 189 190 removeFinalFromFields(mutableClass); 191 192 decorator.decorate(mutableClass); 193 } 194 } catch (Exception e) { 195 throw new RuntimeException("failed to instrument " + mutableClass.getName(), e); 196 } 197 } 198 recordPackageStats(PerfStatsCollector perfStats, MutableClass mutableClass)199 private void recordPackageStats(PerfStatsCollector perfStats, MutableClass mutableClass) { 200 String className = mutableClass.getName(); 201 for (int i = className.indexOf('.'); i != -1; i = className.indexOf('.', i + 1)) { 202 perfStats.incrementCount("instrument package " + className.substring(0, i)); 203 } 204 } 205 hasAnyJUnitAnnotation(MutableClass mutableClass)206 private static boolean hasAnyJUnitAnnotation(MutableClass mutableClass) { 207 for (MethodNode method : mutableClass.getMethods()) { 208 if (method.visibleAnnotations == null) { 209 continue; 210 } 211 if ((method.access & Opcodes.ACC_PUBLIC) == 0) { 212 // Only check public methods. 213 continue; 214 } 215 // Because robolectric will generate private versions of the method for 216 // internal use and any junit annotated method that is private will cause junit to 217 // throw exceptions 218 if (method.visibleAnnotations.stream() 219 .anyMatch(annotation -> annotation.desc.startsWith("Lorg/junit/"))) { 220 return true; 221 } 222 } 223 return false; 224 } 225 226 // See https://github.com/robolectric/robolectric/issues/6840 227 // Adds Set(int, object) to android.util.SparseArray. addSetToSparseArray(MutableClass mutableClass)228 private void addSetToSparseArray(MutableClass mutableClass) { 229 for (MethodNode method : mutableClass.getMethods()) { 230 if ("set".equals(method.name)) { 231 return; 232 } 233 } 234 235 MethodNode setFunction = 236 new MethodNode( 237 Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC, 238 "set", 239 "(ILjava/lang/Object;)V", 240 "(ITE;)V", 241 null); 242 RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(setFunction); 243 generator.loadThis(); 244 generator.loadArg(0); 245 generator.loadArg(1); 246 generator.invokeVirtual(mutableClass.classType, new Method("put", "(ILjava/lang/Object;)V")); 247 generator.returnValue(); 248 mutableClass.addMethod(setFunction); 249 } 250 251 /** 252 * Checks if the first or second instruction is a Jacoco load instruction. Robolectric is not 253 * capable at the moment of re-instrumenting Jacoco-instrumented constructors, so these are 254 * currently skipped. 255 * 256 * @param ctor constructor method node 257 * @return whether or not the constructor can be instrumented 258 */ isJacocoInstrumented(MethodNode ctor)259 private boolean isJacocoInstrumented(MethodNode ctor) { 260 AbstractInsnNode[] insns = ctor.instructions.toArray(); 261 if (insns.length > 1) { 262 AbstractInsnNode node = insns[0]; 263 if (node instanceof LabelNode) { 264 node = insns[1]; 265 } 266 if ((node instanceof LdcInsnNode && ((LdcInsnNode) node).cst instanceof ConstantDynamic)) { 267 ConstantDynamic cst = (ConstantDynamic) ((LdcInsnNode) node).cst; 268 return cst.getName().equals("$jacocoData"); 269 } else if (node instanceof MethodInsnNode) { 270 return Objects.equals(((MethodInsnNode) node).name, "$jacocoInit"); 271 } 272 } 273 return false; 274 } 275 276 /** 277 * Adds a call $$robo$init, which instantiates a shadow object if required. This is to support 278 * custom shadows for Jacoco-instrumented classes (except cnstructor shadows). 279 */ addCallToRoboInit(MutableClass mutableClass, MethodNode ctor)280 protected void addCallToRoboInit(MutableClass mutableClass, MethodNode ctor) { 281 AbstractInsnNode returnNode = 282 Iterables.find( 283 ctor.instructions, 284 node -> { 285 if (node.getOpcode() == Opcodes.INVOKESPECIAL) { 286 MethodInsnNode mNode = (MethodInsnNode) node; 287 return (mNode.owner.equals(mutableClass.internalClassName) 288 || mNode.owner.equals(mutableClass.classNode.superName)); 289 } 290 return false; 291 }, 292 null); 293 ctor.instructions.insert( 294 returnNode, 295 new MethodInsnNode( 296 Opcodes.INVOKEVIRTUAL, 297 mutableClass.classType.getInternalName(), 298 ROBO_INIT_METHOD_NAME, 299 "()V")); 300 ctor.instructions.insert(returnNode, new VarInsnNode(Opcodes.ALOAD, 0)); 301 } 302 instrumentMethods(MutableClass mutableClass)303 private void instrumentMethods(MutableClass mutableClass) { 304 if (mutableClass.isInterface()) { 305 for (MethodNode method : mutableClass.getMethods()) { 306 rewriteMethodBody(mutableClass, method); 307 } 308 } else { 309 for (MethodNode method : mutableClass.getMethods()) { 310 rewriteMethodBody(mutableClass, method); 311 312 if (method.name.equals("<clinit>")) { 313 method.name = ShadowConstants.STATIC_INITIALIZER_METHOD_NAME; 314 mutableClass.addMethod(generateStaticInitializerNotifierMethod(mutableClass)); 315 } else if (method.name.equals("<init>")) { 316 if (isJacocoInstrumented(method)) { 317 addCallToRoboInit(mutableClass, method); 318 } else { 319 instrumentConstructor(mutableClass, method); 320 } 321 } else if (!isSyntheticAccessorMethod(method) && !Modifier.isAbstract(method.access)) { 322 instrumentNormalMethod(mutableClass, method); 323 } 324 } 325 } 326 } 327 addNoArgsConstructor(MutableClass mutableClass)328 private static void addNoArgsConstructor(MutableClass mutableClass) { 329 if (!mutableClass.foundMethods.contains("<init>()V")) { 330 MethodNode defaultConstructor = 331 new MethodNode(Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC, "<init>", "()V", "()V", null); 332 RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(defaultConstructor); 333 generator.loadThis(); 334 generator.visitMethodInsn( 335 Opcodes.INVOKESPECIAL, mutableClass.classNode.superName, "<init>", "()V", false); 336 generator.loadThis(); 337 generator.invokeVirtual(mutableClass.classType, new Method(ROBO_INIT_METHOD_NAME, "()V")); 338 generator.returnValue(); 339 mutableClass.addMethod(defaultConstructor); 340 } 341 } 342 343 /** 344 * Generates code like this: 345 * 346 * <pre> 347 * protected void $$robo$init() { 348 * if (__robo_data__ == null) { 349 * __robo_data__ = RobolectricInternals.initializing(this); 350 * } 351 * } 352 * </pre> 353 */ addRoboInitMethod(MutableClass mutableClass)354 private void addRoboInitMethod(MutableClass mutableClass) { 355 MethodNode initMethodNode = 356 new MethodNode( 357 Opcodes.ACC_PROTECTED | Opcodes.ACC_SYNTHETIC, 358 ROBO_INIT_METHOD_NAME, 359 "()V", 360 null, 361 null); 362 RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(initMethodNode); 363 Label alreadyInitialized = new Label(); 364 generator.loadThis(); // this 365 generator.getField( 366 mutableClass.classType, 367 ShadowConstants.CLASS_HANDLER_DATA_FIELD_NAME, 368 OBJECT_TYPE); // contents of __robo_data__ 369 generator.ifNonNull(alreadyInitialized); 370 generator.loadThis(); // this 371 generator.loadThis(); // this, this 372 writeCallToInitializing(mutableClass, generator); 373 // this, __robo_data__ 374 generator.putField( 375 mutableClass.classType, ShadowConstants.CLASS_HANDLER_DATA_FIELD_NAME, OBJECT_TYPE); 376 generator.mark(alreadyInitialized); 377 generator.returnValue(); 378 mutableClass.addMethod(initMethodNode); 379 } 380 writeCallToInitializing( MutableClass mutableClass, RobolectricGeneratorAdapter generator)381 protected void writeCallToInitializing( 382 MutableClass mutableClass, RobolectricGeneratorAdapter generator) { 383 generator.invokeDynamic( 384 "initializing", 385 Type.getMethodDescriptor(OBJECT_TYPE, mutableClass.classType), 386 BOOTSTRAP_INIT); 387 } 388 removeFinalFromFields(MutableClass mutableClass)389 private static void removeFinalFromFields(MutableClass mutableClass) { 390 for (FieldNode fieldNode : mutableClass.getFields()) { 391 fieldNode.access &= ~Modifier.FINAL; 392 } 393 } 394 isSyntheticAccessorMethod(MethodNode method)395 private static boolean isSyntheticAccessorMethod(MethodNode method) { 396 return (method.access & Opcodes.ACC_SYNTHETIC) != 0; 397 } 398 399 /** 400 * Constructors are instrumented as follows: 401 * 402 * <ul> 403 * <li>The original constructor will be stripped of its instructions leading up to, and 404 * including, the call to super() or this(). It is also renamed to $$robo$$__constructor__ 405 * <li>A method called __constructor__ is created and its job is to call 406 * $$robo$$__constructor__. The __constructor__ method is what gets shadowed if a Shadow 407 * wants to shadow a constructor. 408 * <li>A new constructor is created and contains the stripped instructions of the original 409 * constructor leading up to, and including, the call to super() or this(). Then, it has a 410 * call to $$robo$init to initialize the Class' Shadow Object. Then, it uses invokedynamic 411 * to call __constructor__. Finally, it contains any instructions that might occur after the 412 * return statement in the original constructor. 413 * </ul> 414 * 415 * @param method the constructor to instrument 416 */ instrumentConstructor(MutableClass mutableClass, MethodNode method)417 protected void instrumentConstructor(MutableClass mutableClass, MethodNode method) { 418 int methodAccess = method.access; 419 makeMethodPrivate(method); 420 421 InsnList callSuper = extractCallToSuperConstructor(mutableClass, method); 422 method.name = directMethodName(mutableClass, ShadowConstants.CONSTRUCTOR_METHOD_NAME); 423 mutableClass.addMethod( 424 redirectorMethod(mutableClass, method, ShadowConstants.CONSTRUCTOR_METHOD_NAME)); 425 426 String[] exceptions = exceptionArray(method); 427 MethodNode initMethodNode = 428 new MethodNode(methodAccess, "<init>", method.desc, method.signature, exceptions); 429 RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(initMethodNode); 430 initMethodNode.instructions.add(callSuper); 431 generator.loadThis(); 432 generator.invokeVirtual(mutableClass.classType, new Method(ROBO_INIT_METHOD_NAME, "()V")); 433 generateClassHandlerCall( 434 mutableClass, method, ShadowConstants.CONSTRUCTOR_METHOD_NAME, generator, false); 435 436 generator.endMethod(); 437 438 InsnList postamble = extractInstructionsAfterReturn(method, initMethodNode); 439 if (postamble.size() > 0) { 440 initMethodNode.instructions.add(postamble); 441 } 442 mutableClass.addMethod(initMethodNode); 443 } 444 445 /** 446 * Checks to see if there are instructions after RETURN. If there are, it will check to see if 447 * they belong in the call-to-super, or the shadowable part of the constructor. 448 */ extractInstructionsAfterReturn(MethodNode method, MethodNode initMethodNode)449 private InsnList extractInstructionsAfterReturn(MethodNode method, MethodNode initMethodNode) { 450 InsnList removedInstructions = new InsnList(); 451 AbstractInsnNode returnNode = 452 Iterables.find( 453 method.instructions, 454 node -> node instanceof InsnNode && node.getOpcode() == Opcodes.RETURN, 455 null); 456 if (returnNode == null) { 457 return removedInstructions; 458 } 459 if (returnNode.getNext() instanceof LabelNode) { 460 // There are instructions after the return, check where they belong. Note this is a very rare 461 // edge case and only seems to happen with desugared+proguarded classes such as 462 // play-services-basement's ApiException. 463 LabelNode labelAfterReturn = (LabelNode) returnNode.getNext(); 464 boolean inInitMethodNode = 465 Iterables.any( 466 initMethodNode.instructions, 467 input -> 468 input instanceof JumpInsnNode 469 && ((JumpInsnNode) input).label == labelAfterReturn); 470 471 if (inInitMethodNode) { 472 while (returnNode.getNext() != null) { 473 AbstractInsnNode node = returnNode.getNext(); 474 method.instructions.remove(node); 475 removedInstructions.add(node); 476 } 477 } 478 } 479 return removedInstructions; 480 } 481 extractCallToSuperConstructor( MutableClass mutableClass, MethodNode ctor)482 private static InsnList extractCallToSuperConstructor( 483 MutableClass mutableClass, MethodNode ctor) { 484 InsnList removedInstructions = new InsnList(); 485 // Start removing instructions at the beginning of the method. The first instructions of 486 // constructors may vary. 487 int startIndex = 0; 488 489 AbstractInsnNode[] insns = ctor.instructions.toArray(); 490 for (int i = 0; i < insns.length; i++) { 491 AbstractInsnNode node = insns[i]; 492 493 switch (node.getOpcode()) { 494 case Opcodes.INVOKESPECIAL: 495 MethodInsnNode mnode = (MethodInsnNode) node; 496 if (mnode.owner.equals(mutableClass.internalClassName) 497 || mnode.owner.equals(mutableClass.classNode.superName)) { 498 if (!"<init>".equals(mnode.name)) { 499 throw new AssertionError("Invalid MethodInsnNode name"); 500 } 501 502 // remove all instructions in the range 0 (the start) to invokespecial 503 // <init> 504 while (startIndex <= i) { 505 ctor.instructions.remove(insns[startIndex]); 506 removedInstructions.add(insns[startIndex]); 507 startIndex++; 508 } 509 return removedInstructions; 510 } 511 break; 512 513 case Opcodes.ATHROW: 514 ctor.visitCode(); 515 ctor.visitInsn(Opcodes.RETURN); 516 ctor.visitEnd(); 517 return removedInstructions; 518 519 default: 520 // nothing to do 521 } 522 } 523 524 throw new RuntimeException("huh? " + ctor.name + ctor.desc); 525 } 526 527 /** 528 * Instruments a normal method 529 * 530 * <ul> 531 * <li>Rename the method from {@code methodName} to {@code $$robo$$methodName}. 532 * <li>Make it private so we can invoke it directly without subclass overrides taking 533 * precedence. 534 * <li>Remove {@code final} modifiers, if present. 535 * <li>Create a delegator method named {@code methodName} which delegates to the {@link 536 * ClassHandler}. 537 * </ul> 538 */ instrumentNormalMethod(MutableClass mutableClass, MethodNode method)539 protected void instrumentNormalMethod(MutableClass mutableClass, MethodNode method) { 540 // if not abstract, set a final modifier 541 if ((method.access & Opcodes.ACC_ABSTRACT) == 0) { 542 method.access = method.access | Opcodes.ACC_FINAL; 543 } 544 boolean isNativeMethod = (method.access & Opcodes.ACC_NATIVE) != 0; 545 if (isNativeMethod) { 546 instrumentNativeMethod(mutableClass, method); 547 } 548 549 // Create delegator method with same name as original method. The delegator method will use 550 // invokedynamic to decide at runtime whether to call original method or shadowed method 551 String originalName = method.name; 552 method.name = directMethodName(mutableClass, originalName); 553 554 MethodNode delegatorMethodNode = 555 new MethodNode( 556 method.access, originalName, method.desc, method.signature, exceptionArray(method)); 557 delegatorMethodNode.visibleAnnotations = method.visibleAnnotations; 558 delegatorMethodNode.access &= ~(Opcodes.ACC_NATIVE | Opcodes.ACC_ABSTRACT | Opcodes.ACC_FINAL); 559 560 makeMethodPrivate(method); 561 562 RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(delegatorMethodNode); 563 generateClassHandlerCall(mutableClass, method, originalName, generator, isNativeMethod); 564 generator.endMethod(); 565 mutableClass.addMethod(delegatorMethodNode); 566 } 567 568 /** 569 * Creates native stub which returns the default return value. 570 * 571 * @param mutableClass Class to be instrumented 572 * @param method Method to be instrumented, must be native 573 */ instrumentNativeMethod(MutableClass mutableClass, MethodNode method)574 protected void instrumentNativeMethod(MutableClass mutableClass, MethodNode method) { 575 576 String nativeBindingMethodName = 577 SHADOW_IMPL.directNativeMethodName(mutableClass.getName(), method.name); 578 579 // Generate native binding method 580 MethodNode nativeBindingMethod = 581 new MethodNode( 582 Opcodes.ASM4, 583 nativeBindingMethodName, 584 method.desc, 585 method.signature, 586 exceptionArray(method)); 587 nativeBindingMethod.access = method.access | Opcodes.ACC_SYNTHETIC; 588 makeMethodPrivate(nativeBindingMethod); 589 mutableClass.addMethod(nativeBindingMethod); 590 591 method.access = method.access & ~Opcodes.ACC_NATIVE; 592 593 RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(method); 594 595 Type returnType = generator.getReturnType(); 596 generator.pushDefaultReturnValueToStack(returnType); 597 generator.returnValue(); 598 } 599 directMethodName(MutableClass mutableClass, String originalName)600 protected static String directMethodName(MutableClass mutableClass, String originalName) { 601 return SHADOW_IMPL.directMethodName(mutableClass.getName(), originalName); 602 } 603 604 // todo rename redirectorMethod( MutableClass mutableClass, MethodNode method, String newName)605 private MethodNode redirectorMethod( 606 MutableClass mutableClass, MethodNode method, String newName) { 607 MethodNode redirector = 608 new MethodNode( 609 Opcodes.ASM4, newName, method.desc, method.signature, exceptionArray(method)); 610 redirector.access = 611 method.access & ~(Opcodes.ACC_NATIVE | Opcodes.ACC_ABSTRACT | Opcodes.ACC_FINAL); 612 makeMethodPrivate(redirector); 613 RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(redirector); 614 generator.invokeMethod(mutableClass.internalClassName, method); 615 generator.returnValue(); 616 return redirector; 617 } 618 exceptionArray(MethodNode method)619 protected String[] exceptionArray(MethodNode method) { 620 List<String> exceptions = method.exceptions; 621 return exceptions.toArray(new String[exceptions.size()]); 622 } 623 624 /** Filters methods that might need special treatment because of various reasons */ rewriteMethodBody(MutableClass mutableClass, MethodNode callingMethod)625 private void rewriteMethodBody(MutableClass mutableClass, MethodNode callingMethod) { 626 ListIterator<AbstractInsnNode> instructions = callingMethod.instructions.iterator(); 627 while (instructions.hasNext()) { 628 AbstractInsnNode node = instructions.next(); 629 630 switch (node.getOpcode()) { 631 case Opcodes.NEW: 632 TypeInsnNode newInsnNode = (TypeInsnNode) node; 633 newInsnNode.desc = mutableClass.config.mappedTypeName(newInsnNode.desc); 634 break; 635 636 case Opcodes.GETFIELD: 637 /* falls through */ 638 case Opcodes.PUTFIELD: 639 /* falls through */ 640 case Opcodes.GETSTATIC: 641 /* falls through */ 642 case Opcodes.PUTSTATIC: 643 FieldInsnNode fieldInsnNode = (FieldInsnNode) node; 644 fieldInsnNode.desc = mutableClass.config.mappedTypeName(fieldInsnNode.desc); // todo test 645 break; 646 647 case Opcodes.INVOKESTATIC: 648 /* falls through */ 649 case Opcodes.INVOKEINTERFACE: 650 /* falls through */ 651 case Opcodes.INVOKESPECIAL: 652 /* falls through */ 653 case Opcodes.INVOKEVIRTUAL: 654 MethodInsnNode targetMethod = (MethodInsnNode) node; 655 targetMethod.desc = mutableClass.config.remapParams(targetMethod.desc); 656 if (isGregorianCalendarBooleanConstructor(targetMethod)) { 657 replaceGregorianCalendarBooleanConstructor(instructions, targetMethod); 658 } else if (mutableClass.config.shouldIntercept(targetMethod)) { 659 interceptInvokeVirtualMethod(mutableClass, instructions, targetMethod); 660 } 661 break; 662 663 case Opcodes.INVOKEDYNAMIC: 664 /* no unusual behavior */ 665 break; 666 667 default: 668 break; 669 } 670 } 671 } 672 673 /** 674 * Verifies if the @targetMethod is a {@code <init>(boolean)} constructor for {@link 675 * java.util.GregorianCalendar}. 676 */ isGregorianCalendarBooleanConstructor(MethodInsnNode targetMethod)677 private static boolean isGregorianCalendarBooleanConstructor(MethodInsnNode targetMethod) { 678 return targetMethod.owner.equals("java/util/GregorianCalendar") 679 && targetMethod.name.equals("<init>") 680 && targetMethod.desc.equals("(Z)V"); 681 } 682 683 /** 684 * Replaces the void {@code <init>(boolean)} constructor for a call to the {@code void <init>(int, 685 * int, int)} one. 686 */ replaceGregorianCalendarBooleanConstructor( ListIterator<AbstractInsnNode> instructions, MethodInsnNode targetMethod)687 private static void replaceGregorianCalendarBooleanConstructor( 688 ListIterator<AbstractInsnNode> instructions, MethodInsnNode targetMethod) { 689 // Remove the call to GregorianCalendar(boolean) 690 instructions.remove(); 691 692 // Discard the already-pushed parameter for GregorianCalendar(boolean) 693 instructions.add(new InsnNode(Opcodes.POP)); 694 695 // Add parameters values for calling GregorianCalendar(int, int, int) 696 instructions.add(new InsnNode(Opcodes.ICONST_0)); 697 instructions.add(new InsnNode(Opcodes.ICONST_0)); 698 instructions.add(new InsnNode(Opcodes.ICONST_0)); 699 700 // Call GregorianCalendar(int, int, int) 701 instructions.add( 702 new MethodInsnNode( 703 Opcodes.INVOKESPECIAL, 704 targetMethod.owner, 705 targetMethod.name, 706 "(III)V", 707 targetMethod.itf)); 708 } 709 710 /** 711 * Decides to call through the appropriate method to intercept the method with an INVOKEVIRTUAL 712 * Opcode, depending if the invokedynamic bytecode instruction is available (Java 7+). 713 */ interceptInvokeVirtualMethod( MutableClass mutableClass, ListIterator<AbstractInsnNode> instructions, MethodInsnNode targetMethod)714 protected void interceptInvokeVirtualMethod( 715 MutableClass mutableClass, 716 ListIterator<AbstractInsnNode> instructions, 717 MethodInsnNode targetMethod) { 718 instructions.remove(); // remove the method invocation 719 720 Type type = Type.getObjectType(targetMethod.owner); 721 String description = targetMethod.desc; 722 String owner = type.getClassName(); 723 724 if (targetMethod.getOpcode() != Opcodes.INVOKESTATIC) { 725 String thisType = type.getDescriptor(); 726 description = "(" + thisType + description.substring(1); 727 } 728 729 instructions.add( 730 new InvokeDynamicInsnNode(targetMethod.name, description, BOOTSTRAP_INTRINSIC, owner)); 731 } 732 733 /** Replaces protected and private class modifiers with public. */ makeClassPublic(ClassNode clazz)734 private static void makeClassPublic(ClassNode clazz) { 735 clazz.access = 736 (clazz.access | Opcodes.ACC_PUBLIC) & ~(Opcodes.ACC_PROTECTED | Opcodes.ACC_PRIVATE); 737 } 738 739 /** Replaces protected and public class modifiers with private. */ makeMethodPrivate(MethodNode method)740 protected void makeMethodPrivate(MethodNode method) { 741 method.access = 742 (method.access | Opcodes.ACC_PRIVATE) & ~(Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED); 743 } 744 generateStaticInitializerNotifierMethod(MutableClass mutableClass)745 private static MethodNode generateStaticInitializerNotifierMethod(MutableClass mutableClass) { 746 MethodNode methodNode = new MethodNode(Opcodes.ACC_STATIC, "<clinit>", "()V", "()V", null); 747 RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(methodNode); 748 generator.push(mutableClass.classType); 749 generator.invokeStatic( 750 Type.getType(RobolectricInternals.class), 751 new Method("classInitializing", "(Ljava/lang/Class;)V")); 752 generator.returnValue(); 753 generator.endMethod(); 754 return methodNode; 755 } 756 757 // todo javadocs generateClassHandlerCall( MutableClass mutableClass, MethodNode originalMethod, String originalMethodName, RobolectricGeneratorAdapter generator, boolean isNativeMethod)758 protected void generateClassHandlerCall( 759 MutableClass mutableClass, 760 MethodNode originalMethod, 761 String originalMethodName, 762 RobolectricGeneratorAdapter generator, 763 boolean isNativeMethod) { 764 Handle original = 765 new Handle( 766 getTag(originalMethod), 767 mutableClass.classType.getInternalName(), 768 originalMethod.name, 769 originalMethod.desc, 770 getTag(originalMethod) == Opcodes.H_INVOKEINTERFACE); 771 772 if (generator.isStatic()) { 773 generator.loadArgs(); 774 generator.invokeDynamic( 775 originalMethodName, originalMethod.desc, BOOTSTRAP_STATIC, original, isNativeMethod); 776 } else { 777 String desc = "(" + mutableClass.classType.getDescriptor() + originalMethod.desc.substring(1); 778 generator.loadThis(); 779 generator.loadArgs(); 780 generator.invokeDynamic(originalMethodName, desc, BOOTSTRAP, original, isNativeMethod); 781 } 782 783 generator.returnValue(); 784 } 785 getTag(MethodNode m)786 int getTag(MethodNode m) { 787 return Modifier.isStatic(m.access) ? Opcodes.H_INVOKESTATIC : Opcodes.H_INVOKESPECIAL; 788 } 789 790 // implemented in DirectClassInstrumentor setAndroidJarSDKVersion(int androidJarSDKVersion)791 public void setAndroidJarSDKVersion(int androidJarSDKVersion) {} 792 793 // implemented in DirectClassInstrumentor getAndroidJarSDKVersion()794 protected int getAndroidJarSDKVersion() { 795 return -1; 796 } 797 798 public interface Decorator { decorate(MutableClass mutableClass)799 void decorate(MutableClass mutableClass); 800 } 801 802 /** 803 * Provides try/catch code generation with a {@link org.objectweb.asm.commons.GeneratorAdapter}. 804 */ 805 static class TryCatch { 806 private final Label start; 807 private final Label end; 808 private final Label handler; 809 private final GeneratorAdapter generatorAdapter; 810 TryCatch(GeneratorAdapter generatorAdapter, Type type)811 TryCatch(GeneratorAdapter generatorAdapter, Type type) { 812 this.generatorAdapter = generatorAdapter; 813 this.start = generatorAdapter.mark(); 814 this.end = new Label(); 815 this.handler = new Label(); 816 generatorAdapter.visitTryCatchBlock(start, end, handler, type.getInternalName()); 817 } 818 end()819 void end() { 820 generatorAdapter.mark(end); 821 } 822 handler()823 void handler() { 824 generatorAdapter.mark(handler); 825 } 826 } 827 } 828