aboutsummaryrefslogtreecommitdiff
path: root/robolectric-sandbox/src/main/java/org/robolectric/internal/bytecode/SandboxClassLoader.java
blob: 8b1904208c4b26a945e422c2bdbf3a43d099e0a5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
package org.robolectric.internal.bytecode;

import javax.annotation.Nonnull;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.GeneratorAdapter;
import org.objectweb.asm.commons.JSRInlinerAdapter;
import org.objectweb.asm.commons.Method;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.InvokeDynamicInsnNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.TypeInsnNode;
import org.objectweb.asm.tree.VarInsnNode;
import org.robolectric.util.Logger;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.Util;

import java.io.IOException;
import java.io.InputStream;
import java.lang.invoke.CallSite;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;

import static java.lang.invoke.MethodType.methodType;
import static org.objectweb.asm.Type.ARRAY;
import static org.objectweb.asm.Type.OBJECT;
import static org.objectweb.asm.Type.VOID;
import static org.robolectric.util.ReflectionHelpers.ClassParameter.from;

/**
 * Class loader that modifies the bytecode of Android classes to insert calls to Robolectric's shadow classes.
 */
public class SandboxClassLoader extends URLClassLoader implements Opcodes {
  private final URLClassLoader systemClassLoader;
  private final URLClassLoader urls;
  private final InstrumentationConfiguration config;
  private final Map<String, String> classesToRemap;
  private final Set<MethodRef> methodsToIntercept;

  public SandboxClassLoader(InstrumentationConfiguration config) {
    this(((URLClassLoader) ClassLoader.getSystemClassLoader()), config);
  }

  public SandboxClassLoader(URLClassLoader systemClassLoader, InstrumentationConfiguration config, URL... urls) {
    super(systemClassLoader.getURLs(), systemClassLoader.getParent());
    this.systemClassLoader = systemClassLoader;

    this.config = config;
    this.urls = new URLClassLoader(urls, null);
    classesToRemap = convertToSlashes(config.classNameTranslations());
    methodsToIntercept = convertToSlashes(config.methodsToIntercept());
    for (URL url : urls) {
      Logger.debug("Loading classes from: %s", url);
    }
  }

  @Override
  public URL getResource(String name) {
    URL fromParent = super.getResource(name);
    if (fromParent != null) {
      return fromParent;
    }
    return urls.getResource(name);
  }

  private InputStream getClassBytesAsStreamPreferringLocalUrls(String resName) {
    InputStream fromUrlsClassLoader = urls.getResourceAsStream(resName);
    if (fromUrlsClassLoader != null) {
      return fromUrlsClassLoader;
    }
    return super.getResourceAsStream(resName);
  }

  @Override
  protected Class<?> findClass(String name) throws ClassNotFoundException {
    if (config.shouldAcquire(name)) {
      return maybeInstrumentClass(name);
    } else {
      return systemClassLoader.loadClass(name);
    }
  }

  protected Class<?> maybeInstrumentClass(String className) throws ClassNotFoundException {
    final byte[] origClassBytes = getByteCode(className);

    ClassNode classNode = new ClassNode(Opcodes.ASM4) {
      @Override
      public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
        desc = remapParamType(desc);
        return super.visitField(access, name, desc, signature, value);
      }

      @Override
      public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor methodVisitor = super.visitMethod(access, name, remapParams(desc), signature, exceptions);
        return new JSRInlinerAdapter(methodVisitor, access, name, desc, signature, exceptions);
      }
    };

    final ClassReader classReader = new ClassReader(origClassBytes);
    classReader.accept(classNode, 0);

    classNode.interfaces.add(Type.getInternalName(ShadowedObject.class));

    try {
      byte[] bytes;
      ClassInfo classInfo = new ClassInfo(className, classNode);
      if (config.shouldInstrument(classInfo)) {
        bytes = getInstrumentedBytes(classNode, config.containsStubs(classInfo));
      } else {
        bytes = origClassBytes;
      }
      ensurePackage(className);
      return defineClass(className, bytes, 0, bytes.length);
    } catch (Exception e) {
      throw new ClassNotFoundException("couldn't load " + className, e);
    } catch (OutOfMemoryError e) {
      System.err.println("[ERROR] couldn't load " + className + " in " + this);
      throw e;
    }
  }

  @Override
  protected Package getPackage(String name) {
    Package aPackage = super.getPackage(name);
    if (aPackage != null) {
      return aPackage;
    }

    return ReflectionHelpers.callInstanceMethod(systemClassLoader, "getPackage",
        from(String.class, name));
  }

  protected byte[] getByteCode(String className) throws ClassNotFoundException {
    String classFilename = className.replace('.', '/') + ".class";
    try (InputStream classBytesStream = getClassBytesAsStreamPreferringLocalUrls(classFilename)) {
      if (classBytesStream == null) throw new ClassNotFoundException(className);

      return Util.readBytes(classBytesStream);
    } catch (IOException e) {
      throw new ClassNotFoundException("couldn't load " + className, e);
    }
  }

  private void ensurePackage(final String className) {
    int lastDotIndex = className.lastIndexOf('.');
    if (lastDotIndex != -1) {
      String pckgName = className.substring(0, lastDotIndex);
      Package pckg = getPackage(pckgName);
      if (pckg == null) {
        definePackage(pckgName, null, null, null, null, null, null, null);
      }
    }
  }

  private String remapParams(String desc) {
    StringBuilder buf = new StringBuilder();
    buf.append("(");
    for (Type type : Type.getArgumentTypes(desc)) {
      buf.append(remapParamType(type));
    }
    buf.append(")");
    buf.append(remapParamType(Type.getReturnType(desc)));
    return buf.toString();
  }

  // remap Landroid/Foo; to Landroid/Bar;
  private String remapParamType(String desc) {
    return remapParamType(Type.getType(desc));
  }

  private String remapParamType(Type type) {
    String remappedName;
    String internalName;

    switch (type.getSort()) {
      case ARRAY:
        internalName = type.getInternalName();
        int count = 0;
        while (internalName.charAt(count) == '[') count++;

        remappedName = remapParamType(internalName.substring(count));
        if (remappedName != null) {
          return Type.getObjectType(internalName.substring(0, count) + remappedName).getDescriptor();
        }
        break;

      case OBJECT:
        internalName = type.getInternalName();
        remappedName = classesToRemap.get(internalName);
        if (remappedName != null) {
          return Type.getObjectType(remappedName).getDescriptor();
        }
        break;

      default:
        break;
    }
    return type.getDescriptor();
  }

  // remap android/Foo to android/Bar
  private String remapType(String value) {
    String remappedValue = classesToRemap.get(value);
    if (remappedValue != null) {
      value = remappedValue;
    }
    return value;
  }

  private byte[] getInstrumentedBytes(ClassNode classNode, boolean containsStubs) throws ClassNotFoundException {
    if (InvokeDynamic.ENABLED) {
      new InvokeDynamicClassInstrumentor(classNode, containsStubs).instrument();
    } else {
      new OldClassInstrumentor(classNode, containsStubs).instrument();
    }
    ClassWriter writer = new InstrumentingClassWriter(classNode);
    classNode.accept(writer);
    return writer.toByteArray();
  }

  private Map<String, String> convertToSlashes(Map<String, String> map) {
    HashMap<String, String> newMap = new HashMap<>();
    for (Map.Entry<String, String> entry : map.entrySet()) {
      String key = internalize(entry.getKey());
      String value = internalize(entry.getValue());
      newMap.put(key, value);
      newMap.put("L" + key + ";", "L" + value + ";"); // also the param reference form
    }
    return newMap;
  }

  private Set<MethodRef> convertToSlashes(Set<MethodRef> methodRefs) {
    HashSet<MethodRef> transformed = new HashSet<>();
    for (MethodRef methodRef : methodRefs) {
      transformed.add(new MethodRef(internalize(methodRef.className), methodRef.methodName));
    }
    return transformed;
  }

  private String internalize(String className) {
    return className.replace('.', '/');
  }

  public static void box(final Type type, ListIterator<AbstractInsnNode> instructions) {
    if (type.getSort() == Type.OBJECT || type.getSort() == Type.ARRAY) {
      return;
    }

    if (type == Type.VOID_TYPE) {
      instructions.add(new InsnNode(ACONST_NULL));
    } else {
      Type boxed = getBoxedType(type);
      instructions.add(new TypeInsnNode(NEW, boxed.getInternalName()));
      if (type.getSize() == 2) {
        // Pp -> Ppo -> oPpo -> ooPpo -> ooPp -> o
        instructions.add(new InsnNode(DUP_X2));
        instructions.add(new InsnNode(DUP_X2));
        instructions.add(new InsnNode(POP));
      } else {
        // p -> po -> opo -> oop -> o
        instructions.add(new InsnNode(DUP_X1));
        instructions.add(new InsnNode(SWAP));
      }
      instructions.add(new MethodInsnNode(INVOKESPECIAL, boxed.getInternalName(), "<init>", "(" + type.getDescriptor() + ")V"));
    }
  }

  private static Type getBoxedType(final Type type) {
    switch (type.getSort()) {
      case Type.BYTE:
        return Type.getObjectType("java/lang/Byte");
      case Type.BOOLEAN:
        return Type.getObjectType("java/lang/Boolean");
      case Type.SHORT:
        return Type.getObjectType("java/lang/Short");
      case Type.CHAR:
        return Type.getObjectType("java/lang/Character");
      case Type.INT:
        return Type.getObjectType("java/lang/Integer");
      case Type.FLOAT:
        return Type.getObjectType("java/lang/Float");
      case Type.LONG:
        return Type.getObjectType("java/lang/Long");
      case Type.DOUBLE:
        return Type.getObjectType("java/lang/Double");
    }
    return type;
  }

  private boolean shouldIntercept(MethodInsnNode targetMethod) {
    if (targetMethod.name.equals("<init>")) return false; // sorry, can't strip out calls to super() in constructor
    return methodsToIntercept.contains(new MethodRef(targetMethod.owner, targetMethod.name))
        || methodsToIntercept.contains(new MethodRef(targetMethod.owner, "*"));
  }

  abstract class ClassInstrumentor {
    private static final String ROBO_INIT_METHOD_NAME = "$$robo$init";
    static final String GET_ROBO_DATA_SIGNATURE = "()Ljava/lang/Object;";
    final Type OBJECT_TYPE = Type.getType(Object.class);
    private final String OBJECT_DESC = Type.getDescriptor(Object.class);

    final ClassNode classNode;
    private final boolean containsStubs;
    final String internalClassName;
    private final String className;
    final Type classType;

    public ClassInstrumentor(ClassNode classNode, boolean containsStubs) {
      this.classNode = classNode;
      this.containsStubs = containsStubs;

      this.internalClassName = classNode.name;
      this.className = classNode.name.replace('/', '.');
      this.classType = Type.getObjectType(internalClassName);
    }

    //todo javadoc. Extract blocks to separate methods.
    public void instrument() {
      makeClassPublic(classNode);
      classNode.access = classNode.access & ~ACC_FINAL;

      // Need Java version >=7 to allow invokedynamic
      classNode.version = Math.max(classNode.version, V1_7);

      classNode.fields.add(0, new FieldNode(ACC_PUBLIC | ACC_FINAL,
          ShadowConstants.CLASS_HANDLER_DATA_FIELD_NAME, OBJECT_DESC, OBJECT_DESC, null));

      Set<String> foundMethods = instrumentMethods();

      // If there is no constructor, adds one
      addNoArgsConstructor(foundMethods);

      addDirectCallConstructor();

      // Do not override final #equals, #hashCode, and #toString for all classes
      instrumentInheritedObjectMethod(classNode, foundMethods, "equals", "(Ljava/lang/Object;)Z");
      instrumentInheritedObjectMethod(classNode, foundMethods, "hashCode", "()I");
      instrumentInheritedObjectMethod(classNode, foundMethods, "toString", "()Ljava/lang/String;");

      addRoboInitMethod();

      addRoboGetDataMethod();

      doSpecialHandling();
    }

    @Nonnull
    private Set<String> instrumentMethods() {
      Set<String> foundMethods = new HashSet<>();
      List<MethodNode> methods = new ArrayList<>(classNode.methods);
      for (MethodNode method : methods) {
        foundMethods.add(method.name + method.desc);

        filterSpecialMethods(method);

        if (method.name.equals("<clinit>")) {
          method.name = ShadowConstants.STATIC_INITIALIZER_METHOD_NAME;
          classNode.methods.add(generateStaticInitializerNotifierMethod());
        } else if (method.name.equals("<init>")) {
          instrumentConstructor(method);
        } else if (!isSyntheticAccessorMethod(method) && !Modifier.isAbstract(method.access)) {
          instrumentNormalMethod(method);
        }
      }
      return foundMethods;
    }

    private void addNoArgsConstructor(Set<String> foundMethods) {
      if (!foundMethods.contains("<init>()V")) {
        MethodNode defaultConstructor = new MethodNode(ACC_PUBLIC, "<init>", "()V", "()V", null);
        RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(defaultConstructor);
        generator.loadThis();
        generator.visitMethodInsn(INVOKESPECIAL, classNode.superName, "<init>", "()V");
        generator.loadThis();
        generator.invokeVirtual(classType, new Method(ROBO_INIT_METHOD_NAME, "()V"));
        generator.returnValue();
        classNode.methods.add(defaultConstructor);
      }
    }

    abstract protected void addDirectCallConstructor();

    private void addRoboInitMethod() {
      MethodNode initMethodNode = new MethodNode(ACC_PROTECTED, ROBO_INIT_METHOD_NAME, "()V", null, null);
      RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(initMethodNode);
      Label alreadyInitialized = new Label();
      generator.loadThis();                                         // this
      generator.getField(classType, ShadowConstants.CLASS_HANDLER_DATA_FIELD_NAME, OBJECT_TYPE);  // contents of __robo_data__
      generator.ifNonNull(alreadyInitialized);
      generator.loadThis();                                         // this
      generator.loadThis();                                         // this, this
      writeCallToInitializing(generator);
      // this, __robo_data__
      generator.putField(classType, ShadowConstants.CLASS_HANDLER_DATA_FIELD_NAME, OBJECT_TYPE);
      generator.mark(alreadyInitialized);
      generator.returnValue();
      classNode.methods.add(initMethodNode);
    }

    abstract protected void writeCallToInitializing(RobolectricGeneratorAdapter generator);

    private void addRoboGetDataMethod() {
      MethodNode initMethodNode = new MethodNode(ACC_PUBLIC, ShadowConstants.GET_ROBO_DATA_METHOD_NAME, GET_ROBO_DATA_SIGNATURE, null, null);
      RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(initMethodNode);
      generator.loadThis();                                         // this
      generator.getField(classType, ShadowConstants.CLASS_HANDLER_DATA_FIELD_NAME, OBJECT_TYPE);  // contents of __robo_data__
      generator.returnValue();
      generator.endMethod();
      classNode.methods.add(initMethodNode);
    }

    private void doSpecialHandling() {
      if (className.equals("android.os.Build$VERSION")) {
        for (Object field : classNode.fields) {
          FieldNode fieldNode = (FieldNode) field;
          fieldNode.access &= ~(Modifier.FINAL);
        }
      }
    }

    /**
     * Checks if the given method in the class if overriding, at some point of it's
     * inheritance tree, a final method
     */
    private boolean isOverridingFinalMethod(ClassNode classNode, String methodName, String methodSignature) {
      while (true) {
        List<MethodNode> methods = new ArrayList<>(classNode.methods);

        for (MethodNode method : methods) {
          if (method.name.equals(methodName) && method.desc.equals(methodSignature)) {
            if ((method.access & ACC_FINAL) != 0) {
              return true;
            }
          }
        }

        if (classNode.superName == null) {
          return false;
        }

        try {
          byte[] byteCode = getByteCode(classNode.superName);
          ClassReader classReader = new ClassReader(byteCode);
          classNode = new ClassNode();
          classReader.accept(classNode, 0);
        } catch (ClassNotFoundException e) {
          e.printStackTrace();
        }

      }
    }

    private boolean isSyntheticAccessorMethod(MethodNode method) {
      return (method.access & ACC_SYNTHETIC) != 0;
    }

    /**
     * To be used to instrument methods inherited from the Object class,
     * such as hashCode, equals, and toString.
     * Adds the methods directly to the class.
     */
    private void instrumentInheritedObjectMethod(ClassNode classNode, Set<String> foundMethods, final String methodName, String methodDesc) {
      // Won't instrument if method is overriding a final method
      if (isOverridingFinalMethod(classNode, methodName, methodDesc)) {
        return;
      }

      // if the class doesn't directly override the method, it adds it as a direct invocation and instruments it
      if (!foundMethods.contains(methodName + methodDesc)) {
        MethodNode methodNode = new MethodNode(ACC_PUBLIC, methodName, methodDesc, null, null);
        RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(methodNode);
        generator.invokeMethod("java/lang/Object", methodNode);
        generator.returnValue();
        generator.endMethod();
        this.classNode.methods.add(methodNode);
        instrumentNormalMethod(methodNode);
      }
    }

    private void instrumentConstructor(MethodNode method) {
      makeMethodPrivate(method);

      if (containsStubs) {
        method.instructions.clear();

        RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(method);
        generator.loadThis();
        generator.visitMethodInsn(INVOKESPECIAL, classNode.superName, "<init>", "()V");
        generator.returnValue();
        generator.endMethod();
      }

      InsnList removedInstructions = extractCallToSuperConstructor(method);
      method.name = new ShadowImpl().directMethodName(ShadowConstants.CONSTRUCTOR_METHOD_NAME);
      classNode.methods.add(redirectorMethod(method, ShadowConstants.CONSTRUCTOR_METHOD_NAME));

      String[] exceptions = exceptionArray(method);
      MethodNode methodNode = new MethodNode(method.access, "<init>", method.desc, method.signature, exceptions);
      makeMethodPublic(methodNode);
      RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(methodNode);

      methodNode.instructions = removedInstructions;

      generator.loadThis();
      generator.invokeVirtual(classType, new Method(ROBO_INIT_METHOD_NAME, "()V"));
      generateShadowCall(method, ShadowConstants.CONSTRUCTOR_METHOD_NAME, generator);

      generator.endMethod();
      classNode.methods.add(methodNode);
    }

    private InsnList extractCallToSuperConstructor(MethodNode ctor) {
      InsnList removedInstructions = new InsnList();
      int startIndex = 0;

      AbstractInsnNode[] insns = ctor.instructions.toArray();
      for (int i = 0; i < insns.length; i++) {
        AbstractInsnNode node = insns[i];

        switch (node.getOpcode()) {
          case ALOAD:
            VarInsnNode vnode = (VarInsnNode) node;
            if (vnode.var == 0) {
              startIndex = i;
            }
            break;

          case INVOKESPECIAL:
            MethodInsnNode mnode = (MethodInsnNode) node;
            if (mnode.owner.equals(internalClassName) || mnode.owner.equals(classNode.superName)) {
              assert mnode.name.equals("<init>");

              // remove all instructions in the range startIndex..i, from aload_0 to invokespecial <init>
              while (startIndex <= i) {
                ctor.instructions.remove(insns[startIndex]);
                removedInstructions.add(insns[startIndex]);
                startIndex++;
              }
              return removedInstructions;
            }
            break;

          case ATHROW:
            ctor.visitCode();
            ctor.visitInsn(RETURN);
            ctor.visitEnd();
            return removedInstructions;
        }
      }

      throw new RuntimeException("huh? " + ctor.name + ctor.desc);
    }

    //TODO javadocs
    private void instrumentNormalMethod(MethodNode method) {
      // if not abstract, set a final modifier
      if ((method.access & ACC_ABSTRACT) == 0) {
        method.access = method.access | ACC_FINAL;
      }
      // if a native method, remove native modifier and force return a default value
      if ((method.access & ACC_NATIVE) != 0) {
        method.access = method.access & ~ACC_NATIVE;

        RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(method);
        Type returnType = generator.getReturnType();
        generator.pushDefaultReturnValueToStack(returnType);
        generator.returnValue();
      }

      // todo figure out
      String originalName = method.name;
      method.name = new ShadowImpl().directMethodName(originalName);

      MethodNode delegatorMethodNode = new MethodNode(method.access, originalName, method.desc, method.signature, exceptionArray(method));
      delegatorMethodNode.visibleAnnotations = method.visibleAnnotations;
      delegatorMethodNode.access &= ~(ACC_NATIVE | ACC_ABSTRACT | ACC_FINAL);

      makeMethodPrivate(method);

      RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(delegatorMethodNode);

      generateShadowCall(method, originalName, generator);

      generator.endMethod();

      classNode.methods.add(delegatorMethodNode);
    }

    //todo rename
    private MethodNode redirectorMethod(MethodNode method, String newName) {
      MethodNode redirector = new MethodNode(ASM4, newName, method.desc, method.signature, exceptionArray(method));
      redirector.access = method.access & ~(ACC_NATIVE | ACC_ABSTRACT | ACC_FINAL);
      makeMethodPrivate(redirector);
      RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(redirector);
      generator.invokeMethod(internalClassName, method);
      generator.returnValue();
      return redirector;
    }

    private String[] exceptionArray(MethodNode method) {
      return ((List<String>) method.exceptions).toArray(new String[method.exceptions.size()]);
    }

    /**
     * Filters methods that might need special treatment because of various reasons
     */
    private void filterSpecialMethods(MethodNode callingMethod) {
      ListIterator<AbstractInsnNode> instructions = callingMethod.instructions.iterator();
      while (instructions.hasNext()) {
        AbstractInsnNode node = instructions.next();

        switch (node.getOpcode()) {
          case NEW:
            TypeInsnNode newInsnNode = (TypeInsnNode) node;
            newInsnNode.desc = remapType(newInsnNode.desc);
            break;

          case GETFIELD:
            /* falls through */
          case PUTFIELD:
            /* falls through */
          case GETSTATIC:
            /* falls through */
          case PUTSTATIC:
            FieldInsnNode fieldInsnNode = (FieldInsnNode) node;
            fieldInsnNode.desc = remapType(fieldInsnNode.desc); // todo test
            break;

          case INVOKESTATIC:
            /* falls through */
          case INVOKEINTERFACE:
            /* falls through */
          case INVOKESPECIAL:
            /* falls through */
          case INVOKEVIRTUAL:
            MethodInsnNode targetMethod = (MethodInsnNode) node;
            targetMethod.desc = remapParams(targetMethod.desc);
            if (isGregorianCalendarBooleanConstructor(targetMethod)) {
              replaceGregorianCalendarBooleanConstructor(instructions, targetMethod);
            } else if (shouldIntercept(targetMethod)) {
              interceptInvokeVirtualMethod(instructions, targetMethod);
            }
            break;

          case INVOKEDYNAMIC:
            /* no unusual behavior */
            break;

          default:
            break;
        }
      }
    }

    /**
     * Verifies if the @targetMethod is a <init>(boolean) constructor for {@link java.util.GregorianCalendar}
     */
    private boolean isGregorianCalendarBooleanConstructor(MethodInsnNode targetMethod) {
      return targetMethod.owner.equals("java/util/GregorianCalendar") &&
          targetMethod.name.equals("<init>") &&
          targetMethod.desc.equals("(Z)V");
    }

    /**
     * Replaces the void <init> (boolean) constructor for a call to the void <init> (int, int, int) one
     */
    private void replaceGregorianCalendarBooleanConstructor(ListIterator<AbstractInsnNode> instructions, MethodInsnNode targetMethod) {
      // Remove the call to GregorianCalendar(boolean)
      instructions.remove();

      // Discard the already-pushed parameter for GregorianCalendar(boolean)
      instructions.add(new InsnNode(POP));

      // Add parameters values for calling GregorianCalendar(int, int, int)
      instructions.add(new InsnNode(ICONST_0));
      instructions.add(new InsnNode(ICONST_0));
      instructions.add(new InsnNode(ICONST_0));

      // Call GregorianCalendar(int, int, int)
      instructions.add(new MethodInsnNode(INVOKESPECIAL, targetMethod.owner, targetMethod.name, "(III)V", targetMethod.itf));
    }

    /**
     * Decides to call through the appropriate method to intercept the method with an INVOKEVIRTUAL Opcode,
     * depending if the invokedynamic bytecode instruction is available (Java 7+)
     */
    abstract protected void interceptInvokeVirtualMethod(ListIterator<AbstractInsnNode> instructions, MethodInsnNode targetMethod);

    /**
     * Replaces protected and private class modifiers with public
     */
    private void makeClassPublic(ClassNode clazz) {
      clazz.access = (clazz.access | ACC_PUBLIC) & ~(ACC_PROTECTED | ACC_PRIVATE);
    }

    /**
     * Replaces protected and private method modifiers with public
     */
    private void makeMethodPublic(MethodNode method) {
      method.access = (method.access | ACC_PUBLIC) & ~(ACC_PROTECTED | ACC_PRIVATE);
    }

    /**
     * Replaces protected and public class modifiers with private
     */
    private void makeMethodPrivate(MethodNode method) {
      method.access = (method.access | ACC_PRIVATE) & ~(ACC_PUBLIC | ACC_PROTECTED);
    }

    private MethodNode generateStaticInitializerNotifierMethod() {
      MethodNode methodNode = new MethodNode(ACC_STATIC, "<clinit>", "()V", "()V", null);
      RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(methodNode);
      generator.push(classType);
      generator.invokeStatic(Type.getType(RobolectricInternals.class), new Method("classInitializing", "(Ljava/lang/Class;)V"));
      generator.returnValue();
      generator.endMethod();
      return methodNode;
    }

    // todo javadocs
    protected abstract void generateShadowCall(MethodNode originalMethod, String originalMethodName, RobolectricGeneratorAdapter generator);

    int getTag(MethodNode m) {
      return Modifier.isStatic(m.access) ? H_INVOKESTATIC : H_INVOKESPECIAL;
    }
  }

  /**
   * ClassWriter implementation that verifies classes by comparing type information obtained
   * from loading the classes as resources. This was taken from the ASM ClassWriter unit tests.
   */
  private class InstrumentingClassWriter extends ClassWriter {

    /**
     * Preserve stack map frames for V51 and newer bytecode. This fixes class verification errors
     * for JDK7 and JDK8. The option to disable bytecode verification was removed in JDK8.
     * <p>
     * Don't bother for V50 and earlier bytecode, because it doesn't contain stack map frames, and
     * also because ASM's stack map frame handling doesn't support the JSR and RET instructions
     * present in legacy bytecode.
     */
    public InstrumentingClassWriter(ClassNode classNode) {
      super(classNode.version >= 51 ? ClassWriter.COMPUTE_FRAMES : ClassWriter.COMPUTE_MAXS);
    }

    @Override
    public int newNameType(String name, String desc) {
      return super.newNameType(name, desc.charAt(0) == ')' ? remapParams(desc) : remapParamType(desc));
    }

    @Override
    public int newClass(String value) {
      value = remapType(value);
      return super.newClass(value);
    }

    @Override
    protected String getCommonSuperClass(final String type1, final String type2) {
      try {
        ClassReader info1 = typeInfo(type1);
        ClassReader info2 = typeInfo(type2);
        if ((info1.getAccess() & Opcodes.ACC_INTERFACE) != 0) {
          if (typeImplements(type2, info2, type1)) {
            return type1;
          }
          if ((info2.getAccess() & Opcodes.ACC_INTERFACE) != 0) {
            if (typeImplements(type1, info1, type2)) {
              return type2;
            }
          }
          return "java/lang/Object";
        }
        if ((info2.getAccess() & Opcodes.ACC_INTERFACE) != 0) {
          if (typeImplements(type1, info1, type2)) {
            return type2;
          } else {
            return "java/lang/Object";
          }
        }
        StringBuilder b1 = typeAncestors(type1, info1);
        StringBuilder b2 = typeAncestors(type2, info2);
        String result = "java/lang/Object";
        int end1 = b1.length();
        int end2 = b2.length();
        while (true) {
          int start1 = b1.lastIndexOf(";", end1 - 1);
          int start2 = b2.lastIndexOf(";", end2 - 1);
          if (start1 != -1 && start2 != -1
              && end1 - start1 == end2 - start2) {
            String p1 = b1.substring(start1 + 1, end1);
            String p2 = b2.substring(start2 + 1, end2);
            if (p1.equals(p2)) {
              result = p1;
              end1 = start1;
              end2 = start2;
            } else {
              return result;
            }
          } else {
            return result;
          }
        }
      } catch (IOException e) {
        return "java/lang/Object"; // Handle classes that may be obfuscated
      }
    }

    private StringBuilder typeAncestors(String type, ClassReader info) throws IOException {
      StringBuilder b = new StringBuilder();
      while (!"java/lang/Object".equals(type)) {
        b.append(';').append(type);
        type = info.getSuperName();
        info = typeInfo(type);
      }
      return b;
    }

    private boolean typeImplements(String type, ClassReader info, String itf) throws IOException {
      while (!"java/lang/Object".equals(type)) {
        String[] itfs = info.getInterfaces();
        for (String itf2 : itfs) {
          if (itf2.equals(itf)) {
            return true;
          }
        }
        for (String itf1 : itfs) {
          if (typeImplements(itf1, typeInfo(itf1), itf)) {
            return true;
          }
        }
        type = info.getSuperName();
        info = typeInfo(type);
      }
      return false;
    }

    private ClassReader typeInfo(final String type) throws IOException {
      try (InputStream is = getClassBytesAsStreamPreferringLocalUrls(type + ".class")) {
        return new ClassReader(is);
      }
    }
  }

  /**
   * GeneratorAdapter implementation specific to generate code for Robolectric purposes
   */
  private static class RobolectricGeneratorAdapter extends GeneratorAdapter {
    private final boolean isStatic;
    private final String desc;

    public RobolectricGeneratorAdapter(MethodNode methodNode) {
      super(Opcodes.ASM4, methodNode, methodNode.access, methodNode.name, methodNode.desc);
      this.isStatic = Modifier.isStatic(methodNode.access);
      this.desc = methodNode.desc;
    }

    public void loadThisOrNull() {
      if (isStatic) {
        loadNull();
      } else {
        loadThis();
      }
    }

    public boolean isStatic() {
      return isStatic;
    }

    public void loadNull() {
      visitInsn(ACONST_NULL);
    }

    public Type getReturnType() {
      return Type.getReturnType(desc);
    }

    /**
     * Forces a return of a default value, depending on the method's return type
     *
     * @param type The method's return type
     */
    public void pushDefaultReturnValueToStack(Type type) {
      if (type.equals(Type.BOOLEAN_TYPE)) {
        push(false);
      } else if (type.equals(Type.INT_TYPE) || type.equals(Type.SHORT_TYPE) || type.equals(Type.BYTE_TYPE) || type.equals(Type.CHAR_TYPE)) {
        push(0);
      } else if (type.equals(Type.LONG_TYPE)) {
        push(0l);
      } else if (type.equals(Type.FLOAT_TYPE)) {
        push(0f);
      } else if (type.equals(Type.DOUBLE_TYPE)) {
        push(0d);
      } else if (type.getSort() == ARRAY || type.getSort() == OBJECT) {
        loadNull();
      }
    }

    private void invokeMethod(String internalClassName, MethodNode method) {
      invokeMethod(internalClassName, method.name, method.desc);
    }

    private void invokeMethod(String internalClassName, String methodName, String methodDesc) {
      if (isStatic()) {
        loadArgs();                                             // this, [args]
        visitMethodInsn(INVOKESTATIC, internalClassName, methodName, methodDesc);
      } else {
        loadThisOrNull();                                       // this
        loadArgs();                                             // this, [args]
        visitMethodInsn(INVOKESPECIAL, internalClassName, methodName, methodDesc);
      }
    }

    public TryCatch tryStart(Type exceptionType) {
      return new TryCatch(this, exceptionType);
    }
  }

  /**
   * Provides try/catch code generation with a {@link org.objectweb.asm.commons.GeneratorAdapter}
   */
  static class TryCatch {
    private final Label start;
    private final Label end;
    private final Label handler;
    private final GeneratorAdapter generatorAdapter;

    TryCatch(GeneratorAdapter generatorAdapter, Type type) {
      this.generatorAdapter = generatorAdapter;
      this.start = generatorAdapter.mark();
      this.end = new Label();
      this.handler = new Label();
      generatorAdapter.visitTryCatchBlock(start, end, handler, type.getInternalName());
    }

    void end() {
      generatorAdapter.mark(end);
    }

    void handler() {
      generatorAdapter.mark(handler);
    }
  }

  public class OldClassInstrumentor extends SandboxClassLoader.ClassInstrumentor {
    private final Type PLAN_TYPE = Type.getType(ClassHandler.Plan.class);
    private final Type THROWABLE_TYPE = Type.getType(Throwable.class);
    private final Method INITIALIZING_METHOD = new Method("initializing", "(Ljava/lang/Object;)Ljava/lang/Object;");
    private final Method METHOD_INVOKED_METHOD = new Method("methodInvoked", "(Ljava/lang/String;ZLjava/lang/Class;)L" + PLAN_TYPE.getInternalName() + ";");
    private final Method PLAN_RUN_METHOD = new Method("run", OBJECT_TYPE, new Type[]{OBJECT_TYPE, OBJECT_TYPE, Type.getType(Object[].class)});
    private final Method HANDLE_EXCEPTION_METHOD = new Method("cleanStackTrace", THROWABLE_TYPE, new Type[]{THROWABLE_TYPE});
    private final String DIRECT_OBJECT_MARKER_TYPE_DESC = Type.getObjectType(DirectObjectMarker.class.getName().replace('.', '/')).getDescriptor();
    private final Type ROBOLECTRIC_INTERNALS_TYPE = Type.getType(RobolectricInternals.class);

    public OldClassInstrumentor(ClassNode classNode, boolean containsStubs) {
      super(classNode, containsStubs);
    }

    @Override
    protected void addDirectCallConstructor() {
      MethodNode directCallConstructor = new MethodNode(ACC_PUBLIC,
          "<init>", "(" + DIRECT_OBJECT_MARKER_TYPE_DESC + classType.getDescriptor() + ")V", null, null);
      RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(directCallConstructor);
      generator.loadThis();
      if (classNode.superName.equals("java/lang/Object")) {
        generator.visitMethodInsn(INVOKESPECIAL, classNode.superName, "<init>", "()V");
      } else {
        generator.loadArgs();
        generator.visitMethodInsn(INVOKESPECIAL, classNode.superName,
            "<init>", "(" + DIRECT_OBJECT_MARKER_TYPE_DESC + "L" + classNode.superName + ";)V");
      }
      generator.loadThis();
      generator.loadArg(1);
      generator.putField(classType, ShadowConstants.CLASS_HANDLER_DATA_FIELD_NAME, OBJECT_TYPE);
      generator.returnValue();
      classNode.methods.add(directCallConstructor);
    }

    @Override
    protected void writeCallToInitializing(RobolectricGeneratorAdapter generator) {
      generator.invokeStatic(ROBOLECTRIC_INTERNALS_TYPE, INITIALIZING_METHOD);
    }

    @Override
    protected void generateShadowCall(MethodNode originalMethod, String originalMethodName, RobolectricGeneratorAdapter generator) {
      generateCallToClassHandler(originalMethod, originalMethodName, generator);
    }

    //TODO clean up & javadocs
    private void generateCallToClassHandler(MethodNode originalMethod, String originalMethodName, RobolectricGeneratorAdapter generator) {
      int planLocalVar = generator.newLocal(PLAN_TYPE);
      int exceptionLocalVar = generator.newLocal(THROWABLE_TYPE);
      Label directCall = new Label();
      Label doReturn = new Label();

      boolean isNormalInstanceMethod = !generator.isStatic && !originalMethodName.equals(ShadowConstants.CONSTRUCTOR_METHOD_NAME);

      // maybe perform proxy call...
      if (isNormalInstanceMethod) {
        Label notInstanceOfThis = new Label();

        generator.loadThis();                                         // this
        generator.getField(classType, ShadowConstants.CLASS_HANDLER_DATA_FIELD_NAME, OBJECT_TYPE);  // contents of __robo_data__
        generator.instanceOf(classType);                              // __robo_data__, is instance of same class?
        generator.visitJumpInsn(IFEQ, notInstanceOfThis);             // jump if no (is not instance)

        TryCatch tryCatchForProxyCall = generator.tryStart(THROWABLE_TYPE);
        generator.loadThis();                                         // this
        generator.getField(classType, ShadowConstants.CLASS_HANDLER_DATA_FIELD_NAME, OBJECT_TYPE);  // contents of __robo_data__
        generator.checkCast(classType);                               // __robo_data__ but cast to my class
        generator.loadArgs();                                         // __robo_data__ instance, [args]

        generator.visitMethodInsn(INVOKESPECIAL, internalClassName, originalMethod.name, originalMethod.desc);
        tryCatchForProxyCall.end();

        generator.returnValue();

        // catch(Throwable)
        tryCatchForProxyCall.handler();
        generator.storeLocal(exceptionLocalVar);
        generator.loadLocal(exceptionLocalVar);
        generator.invokeStatic(ROBOLECTRIC_INTERNALS_TYPE, HANDLE_EXCEPTION_METHOD);
        generator.throwException();

        // callClassHandler...
        generator.mark(notInstanceOfThis);
      }

      // prepare for call to classHandler.methodInvoked(String signature, boolean isStatic)
      generator.push(classType.getInternalName() + "/" + originalMethodName + originalMethod.desc);
      generator.push(generator.isStatic());
      generator.push(classType);                                         // my class
      generator.invokeStatic(ROBOLECTRIC_INTERNALS_TYPE, METHOD_INVOKED_METHOD);
      generator.storeLocal(planLocalVar);

      generator.loadLocal(planLocalVar); // plan
      generator.ifNull(directCall);

      // prepare for call to plan.run(Object instance, Object[] params)
      TryCatch tryCatchForHandler = generator.tryStart(THROWABLE_TYPE);
      generator.loadLocal(planLocalVar); // plan
      generator.loadThisOrNull();        // instance
      if (generator.isStatic()) {        // roboData
        generator.loadNull();
      } else {
        generator.loadThis();
        generator.invokeVirtual(classType, new Method(ShadowConstants.GET_ROBO_DATA_METHOD_NAME, GET_ROBO_DATA_SIGNATURE));
      }
      generator.loadArgArray();          // params
      generator.invokeInterface(PLAN_TYPE, PLAN_RUN_METHOD);

      Type returnType = generator.getReturnType();
      int sort = returnType.getSort();
      switch (sort) {
        case VOID:
          generator.pop();
          break;
        case OBJECT:
          /* falls through */
        case ARRAY:
          generator.checkCast(returnType);
          break;
        default:
          int unboxLocalVar = generator.newLocal(OBJECT_TYPE);
          generator.storeLocal(unboxLocalVar);
          generator.loadLocal(unboxLocalVar);
          Label notNull = generator.newLabel();
          Label afterward = generator.newLabel();
          generator.ifNonNull(notNull);
          generator.pushDefaultReturnValueToStack(returnType); // return zero, false, whatever
          generator.goTo(afterward);

          generator.mark(notNull);
          generator.loadLocal(unboxLocalVar);
          generator.unbox(returnType);
          generator.mark(afterward);
          break;
      }
      tryCatchForHandler.end();
      generator.goTo(doReturn);

      // catch(Throwable)
      tryCatchForHandler.handler();
      generator.storeLocal(exceptionLocalVar);
      generator.loadLocal(exceptionLocalVar);
      generator.invokeStatic(ROBOLECTRIC_INTERNALS_TYPE, HANDLE_EXCEPTION_METHOD);
      generator.throwException();


      if (!originalMethod.name.equals("<init>")) {
        generator.mark(directCall);
        TryCatch tryCatchForDirect = generator.tryStart(THROWABLE_TYPE);
        generator.invokeMethod(classType.getInternalName(), originalMethod.name, originalMethod.desc);
        tryCatchForDirect.end();
        generator.returnValue();

        // catch(Throwable)
        tryCatchForDirect.handler();
        generator.storeLocal(exceptionLocalVar);
        generator.loadLocal(exceptionLocalVar);
        generator.invokeStatic(ROBOLECTRIC_INTERNALS_TYPE, HANDLE_EXCEPTION_METHOD);
        generator.throwException();
      }

      generator.mark(doReturn);
      generator.returnValue();
    }

    /**
     * Decides to call through the appropriate method to intercept the method with an INVOKEVIRTUAL Opcode,
     * depending if the invokedynamic bytecode instruction is available (Java 7+)
     */
    @Override
    protected void interceptInvokeVirtualMethod(ListIterator<AbstractInsnNode> instructions, MethodInsnNode targetMethod) {
      interceptInvokeVirtualMethodWithoutInvokeDynamic(instructions, targetMethod);
    }

    /**
     * Intercepts the method without using the invokedynamic bytecode instruction.
     * Should be called through interceptInvokeVirtualMethod, not directly
     */
    private void interceptInvokeVirtualMethodWithoutInvokeDynamic(ListIterator<AbstractInsnNode> instructions, MethodInsnNode targetMethod) {
      boolean isStatic = targetMethod.getOpcode() == INVOKESTATIC;

      instructions.remove(); // remove the method invocation

      Type[] argumentTypes = Type.getArgumentTypes(targetMethod.desc);

      instructions.add(new LdcInsnNode(argumentTypes.length));
      instructions.add(new TypeInsnNode(ANEWARRAY, "java/lang/Object"));

      // first, move any arguments into an Object[] in reverse order
      for (int i = argumentTypes.length - 1; i >= 0; i--) {
        Type type = argumentTypes[i];
        int argWidth = type.getSize();

        if (argWidth == 1) {                       // A B C []
          instructions.add(new InsnNode(DUP_X1));  // A B [] C []
          instructions.add(new InsnNode(SWAP));    // A B [] [] C
          instructions.add(new LdcInsnNode(i));    // A B [] [] C 2
          instructions.add(new InsnNode(SWAP));    // A B [] [] 2 C
          box(type, instructions);                 // A B [] [] 2 (C)
          instructions.add(new InsnNode(AASTORE)); // A B [(C)]
        } else if (argWidth == 2) {                // A B _C_ []
          instructions.add(new InsnNode(DUP_X2));  // A B [] _C_ []
          instructions.add(new InsnNode(DUP_X2));  // A B [] [] _C_ []
          instructions.add(new InsnNode(POP));     // A B [] [] _C_
          box(type, instructions);                 // A B [] [] (C)
          instructions.add(new LdcInsnNode(i));    // A B [] [] (C) 2
          instructions.add(new InsnNode(SWAP));    // A B [] [] 2 (C)
          instructions.add(new InsnNode(AASTORE)); // A B [(C)]
        }
      }

      if (isStatic) { // []
        instructions.add(new InsnNode(Opcodes.ACONST_NULL)); // [] null
        instructions.add(new InsnNode(Opcodes.SWAP));        // null []
      }

      // instance []
      instructions.add(new LdcInsnNode(targetMethod.owner + "/" + targetMethod.name + targetMethod.desc)); // target method signature
      // instance [] signature
      instructions.add(new InsnNode(DUP_X2));       // signature instance [] signature
      instructions.add(new InsnNode(POP));          // signature instance []

      instructions.add(new LdcInsnNode(classType)); // signature instance [] class
      instructions.add(new MethodInsnNode(INVOKESTATIC,
          Type.getType(RobolectricInternals.class).getInternalName(), "intercept",
          "(Ljava/lang/String;Ljava/lang/Object;[Ljava/lang/Object;Ljava/lang/Class;)Ljava/lang/Object;"));

      final Type returnType = Type.getReturnType(targetMethod.desc);
      switch (returnType.getSort()) {
        case ARRAY:
          /* falls through */
        case OBJECT:
          instructions.add(new TypeInsnNode(CHECKCAST, remapType(returnType.getInternalName())));
          break;
        case VOID:
          instructions.add(new InsnNode(POP));
          break;
        case Type.LONG:
          instructions.add(new TypeInsnNode(CHECKCAST, Type.getInternalName(Long.class)));
          instructions.add(new MethodInsnNode(INVOKEVIRTUAL, Type.getInternalName(Long.class), "longValue", Type.getMethodDescriptor(Type.LONG_TYPE), false));
          break;
        case Type.FLOAT:
          instructions.add(new TypeInsnNode(CHECKCAST, Type.getInternalName(Float.class)));
          instructions.add(new MethodInsnNode(INVOKEVIRTUAL, Type.getInternalName(Float.class), "floatValue", Type.getMethodDescriptor(Type.FLOAT_TYPE), false));
          break;
        case Type.DOUBLE:
          instructions.add(new TypeInsnNode(CHECKCAST, Type.getInternalName(Double.class)));
          instructions.add(new MethodInsnNode(INVOKEVIRTUAL, Type.getInternalName(Double.class), "doubleValue", Type.getMethodDescriptor(Type.DOUBLE_TYPE), false));
          break;
        case Type.BOOLEAN:
          instructions.add(new TypeInsnNode(CHECKCAST, Type.getInternalName(Boolean.class)));
          instructions.add(new MethodInsnNode(INVOKEVIRTUAL, Type.getInternalName(Boolean.class), "booleanValue", Type.getMethodDescriptor(Type.BOOLEAN_TYPE), false));
          break;
        case Type.INT:
          instructions.add(new TypeInsnNode(CHECKCAST, Type.getInternalName(Integer.class)));
          instructions.add(new MethodInsnNode(INVOKEVIRTUAL, Type.getInternalName(Integer.class), "intValue", Type.getMethodDescriptor(Type.INT_TYPE), false));
          break;
        case Type.SHORT:
          instructions.add(new TypeInsnNode(CHECKCAST, Type.getInternalName(Short.class)));
          instructions.add(new MethodInsnNode(INVOKEVIRTUAL, Type.getInternalName(Short.class), "shortValue", Type.getMethodDescriptor(Type.SHORT_TYPE), false));
          break;
        case Type.BYTE:
          instructions.add(new TypeInsnNode(CHECKCAST, Type.getInternalName(Byte.class)));
          instructions.add(new MethodInsnNode(INVOKEVIRTUAL, Type.getInternalName(Byte.class), "byteValue", Type.getMethodDescriptor(Type.BYTE_TYPE), false));
          break;
        default:
          throw new RuntimeException("Not implemented: " + getClass().getName() + " cannot intercept methods with return type " + returnType.getClassName());
      }
    }
  }

  public class InvokeDynamicClassInstrumentor extends SandboxClassLoader.ClassInstrumentor {
    private final Handle BOOTSTRAP_INIT;
    private final Handle BOOTSTRAP;
    private final Handle BOOTSTRAP_STATIC;
    private final Handle BOOTSTRAP_INTRINSIC;

    public InvokeDynamicClassInstrumentor(ClassNode classNode, boolean containsStubs) {
      super(classNode, containsStubs);

      String className = Type.getInternalName(InvokeDynamicSupport.class);

      MethodType bootstrap =
          methodType(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class);
      String bootstrapMethod =
          bootstrap.appendParameterTypes(MethodHandle.class).toMethodDescriptorString();
      String bootstrapIntrinsic =
          bootstrap.appendParameterTypes(String.class).toMethodDescriptorString();

      BOOTSTRAP_INIT = new Handle(H_INVOKESTATIC, className, "bootstrapInit", bootstrap.toMethodDescriptorString());
      BOOTSTRAP = new Handle(H_INVOKESTATIC, className, "bootstrap", bootstrapMethod);
      BOOTSTRAP_STATIC = new Handle(H_INVOKESTATIC, className, "bootstrapStatic", bootstrapMethod);
      BOOTSTRAP_INTRINSIC = new Handle(H_INVOKESTATIC, className, "bootstrapIntrinsic", bootstrapIntrinsic);
    }

    @Override
    protected void addDirectCallConstructor() {
      // not needed, for reasons.
    }

    @Override
    protected void writeCallToInitializing(RobolectricGeneratorAdapter generator) {
      generator.invokeDynamic("initializing", Type.getMethodDescriptor(OBJECT_TYPE, classType), BOOTSTRAP_INIT);
    }

    @Override
    protected void generateShadowCall(MethodNode originalMethod, String originalMethodName, RobolectricGeneratorAdapter generator) {
      generateInvokeDynamic(originalMethod, originalMethodName, generator);
    }

    // todo javadocs
    private void generateInvokeDynamic(MethodNode originalMethod, String originalMethodName, RobolectricGeneratorAdapter generator) {
      Handle original =
          new Handle(getTag(originalMethod), classType.getInternalName(), originalMethod.name,
              originalMethod.desc);

      if (generator.isStatic()) {
        generator.loadArgs();
        generator.invokeDynamic(originalMethodName, originalMethod.desc, BOOTSTRAP_STATIC, original);
      } else {
        String desc = "(" + classType.getDescriptor() + originalMethod.desc.substring(1);
        generator.loadThis();
        generator.loadArgs();
        generator.invokeDynamic(originalMethodName, desc, BOOTSTRAP, original);
      }

      generator.returnValue();
    }

    @Override
    protected void interceptInvokeVirtualMethod(ListIterator<AbstractInsnNode> instructions, MethodInsnNode targetMethod) {
      interceptInvokeVirtualMethodWithInvokeDynamic(instructions, targetMethod);
    }

    /**
     * Intercepts the method using the invokedynamic bytecode instruction available in Java 7+.
     * Should be called through interceptInvokeVirtualMethod, not directly
     */
    private void interceptInvokeVirtualMethodWithInvokeDynamic(ListIterator<AbstractInsnNode> instructions, MethodInsnNode targetMethod) {
      instructions.remove();  // remove the method invocation

      Type type = Type.getObjectType(targetMethod.owner);
      String description = targetMethod.desc;
      String owner = type.getClassName();

      if (targetMethod.getOpcode() != INVOKESTATIC) {
        String thisType = type.getDescriptor();
        description = "(" + thisType + description.substring(1, description.length());
      }

      instructions.add(new InvokeDynamicInsnNode(targetMethod.name, description, BOOTSTRAP_INTRINSIC, owner));
    }
  }
}