Version 1.6.57

Cherry-pick: Testing utilities for transforming classfiles. (with modifications)
CL: https://r8-review.googlesource.com/c/r8/+/45621

Cherry-pick: Add a test for b/146957343 (with modifications)
CL: https://r8-review.googlesource.com/c/r8/+/47139

Cherry-pick: Turn off uninstantiated type optimization for interfaces
CL: https://r8-review.googlesource.com/c/r8/+/47160

Bug: 146957343
Bug: 147153808
Change-Id: I66544813e8457f2811efa18a801f0891acd61fb9
diff --git a/src/main/java/com/android/tools/r8/Version.java b/src/main/java/com/android/tools/r8/Version.java
index 8e7e7fd..34e9cba 100644
--- a/src/main/java/com/android/tools/r8/Version.java
+++ b/src/main/java/com/android/tools/r8/Version.java
@@ -11,7 +11,7 @@
 
   // This field is accessed from release scripts using simple pattern matching.
   // Therefore, changing this field could break our release scripts.
-  public static final String LABEL = "1.6.56";
+  public static final String LABEL = "1.6.57";
 
   private Version() {
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
index 92a4963..54fbf9f 100644
--- a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
@@ -238,6 +238,10 @@
     return this;
   }
 
+  public static DexProgramClass asProgramClassOrNull(DexClass clazz) {
+    return clazz != null ? clazz.asProgramClass() : null;
+  }
+
   @Override
   public boolean isNotProgramClass() {
     return false;
diff --git a/src/main/java/com/android/tools/r8/graph/DexType.java b/src/main/java/com/android/tools/r8/graph/DexType.java
index 055b120..b0e5373 100644
--- a/src/main/java/com/android/tools/r8/graph/DexType.java
+++ b/src/main/java/com/android/tools/r8/graph/DexType.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph;
 
+import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
 import static com.android.tools.r8.ir.desugar.InterfaceMethodRewriter.COMPANION_CLASS_NAME_SUFFIX;
 import static com.android.tools.r8.ir.desugar.InterfaceMethodRewriter.DISPATCH_CLASS_NAME_SUFFIX;
 import static com.android.tools.r8.ir.desugar.InterfaceMethodRewriter.EMULATE_LIBRARY_CLASS_NAME_SUFFIX;
@@ -71,10 +72,16 @@
 
   public boolean isAlwaysNull(AppView<AppInfoWithLiveness> appView) {
     if (isClassType()) {
-      DexClass clazz = appView.definitionFor(this);
-      return clazz != null
-          && clazz.isProgramClass()
-          && !appView.appInfo().isInstantiatedDirectlyOrIndirectly(this);
+      DexProgramClass clazz = asProgramClassOrNull(appView.definitionFor(this));
+      if (clazz == null) {
+        return false;
+      }
+      if (appView.options().enableUninstantiatedTypeOptimizationForInterfaces) {
+        return !appView.appInfo().isInstantiatedDirectlyOrIndirectly(clazz.type);
+      } else {
+        return !clazz.isInterface()
+            && !appView.appInfo().isInstantiatedDirectlyOrIndirectly(clazz.type);
+      }
     }
     return false;
   }
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 0098813..d826aa9 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -240,6 +240,8 @@
   public boolean enableValuePropagation = true;
   public boolean enableFieldTypePropagation = true;
   public boolean enableUninstantiatedTypeOptimization = true;
+  // Currently disabled, see b/146957343.
+  public boolean enableUninstantiatedTypeOptimizationForInterfaces = false;
   // TODO(b/138917494): Disable until we have numbers on potential performance penalties.
   public boolean enableRedundantConstNumberOptimization = false;
 
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index 797d185..3f140b8 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -26,6 +26,7 @@
 import com.android.tools.r8.jasmin.JasminBuilder;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.shaking.serviceloader.ServiceLoaderMultipleTest.Greeter;
+import com.android.tools.r8.transformers.ClassFileTransformer;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.AndroidAppConsumers;
@@ -166,6 +167,10 @@
     return testForMainDexListGenerator(temp);
   }
 
+  public static ClassFileTransformer transformer(Class<?> clazz) throws IOException {
+    return ClassFileTransformer.create(clazz);
+  }
+
   // Actually running Proguard should only be during development.
   private static final boolean RUN_PROGUARD = System.getProperty("run_proguard") != null;
   // Actually running r8.jar in a forked process.
diff --git a/src/test/java/com/android/tools/r8/TestRunResult.java b/src/test/java/com/android/tools/r8/TestRunResult.java
index 4dd10a1..7440dba 100644
--- a/src/test/java/com/android/tools/r8/TestRunResult.java
+++ b/src/test/java/com/android/tools/r8/TestRunResult.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
+import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
@@ -99,6 +100,15 @@
     return self();
   }
 
+  public RR assertFailureWithErrorThatThrows(Class<? extends Throwable> expectedError) {
+    assertFailure();
+    assertThat(
+        errorMessage("Run stderr incorrect.", expectedError.getName()),
+        result.stderr,
+        containsString(expectedError.getName()));
+    return self();
+  }
+
   public CodeInspector inspector() throws IOException, ExecutionException {
     // Inspection post run implies success. If inspection of an invalid program is needed it should
     // be done on the compilation result or on the input.
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/B146957343.java b/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/B146957343.java
new file mode 100644
index 0000000..f454913
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/B146957343.java
@@ -0,0 +1,93 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.uninstantiatedtypes;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import java.io.IOException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.objectweb.asm.Opcodes;
+
+@RunWith(Parameterized.class)
+public class B146957343 extends TestBase implements Opcodes {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().withAllApiLevels().build();
+  }
+
+  public B146957343(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  private byte[] getAimplementsI() throws IOException {
+    return transformer(A.class).setImplements(I.class).transform();
+  }
+
+  @Test
+  public void testWithoutR8() throws Exception {
+    testForRuntime(parameters.getRuntime(), parameters.getApiLevel())
+        .addProgramClasses(I.class, J.class, Main.class)
+        .addProgramClassFileData(getAimplementsI())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("In A.f()");
+  }
+
+  @Test
+  public void testWithUninstantiatedTypeOptimizationForInterfaces() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(I.class, J.class, Main.class)
+        .addProgramClassFileData(getAimplementsI())
+        .addKeepMainRule(Main.class)
+        .addKeepRules("-keep class **A { createA(); }")
+        .setMinApi(parameters.getApiLevel())
+        .addOptionsModification(
+            options -> options.enableUninstantiatedTypeOptimizationForInterfaces = true)
+        .compile()
+        .disassemble()
+        .run(parameters.getRuntime(), Main.class)
+        .assertFailureWithErrorThatThrows(NullPointerException.class);
+  }
+
+  @Test
+  public void testWithoutUninstantiatedTypeOptimizationForInterfaces() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(I.class, J.class, Main.class)
+        .addProgramClassFileData(getAimplementsI())
+        .addKeepMainRule(Main.class)
+        .addKeepRules("-keep class **A { createA(); }")
+        .setMinApi(parameters.getApiLevel())
+        .addOptionsModification(
+            options -> options.enableUninstantiatedTypeOptimizationForInterfaces = false)
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("In A.f()");
+  }
+
+  public interface I {}
+
+  public interface J extends I {}
+
+  public static class A implements J {
+    public static J createA() {
+      return new A();
+    }
+
+    public void f() {
+      System.out.println("In A.f()");
+    }
+  }
+
+  public static class Main {
+    public static void main(String[] args) {
+      ((A) A.createA()).f();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/InvalidTypesTest.java b/src/test/java/com/android/tools/r8/shaking/InvalidTypesTest.java
index b951b4f..b42be41 100644
--- a/src/test/java/com/android/tools/r8/shaking/InvalidTypesTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/InvalidTypesTest.java
@@ -40,7 +40,8 @@
     D8,
     JAVAC,
     PROGUARD,
-    R8
+    R8,
+    R8_ENABLE_UNININSTANTATED_TYPE_OPTIMIZATION_FOR_INTERFACES
   }
 
   private enum Mode {
@@ -207,6 +208,27 @@
             .setMinApi(parameters.getRuntime())
             .run(parameters.getRuntime(), mainClass.name);
     checkTestRunResult(r8Result, Compiler.R8);
+
+    R8TestRunResult r8ResultWithUninstantiatedTypeOptimizationForInterfaces =
+        testForR8(parameters.getBackend())
+            .addProgramFiles(inputJar)
+            .addKeepMainRule(mainClass.name)
+            .addKeepRules(
+                "-keep class TestClass { public static I g; }",
+                "-neverinline class TestClass { public static void m(); }")
+            .enableProguardTestOptions()
+            .addOptionsModification(
+                options -> {
+                  if (mode == Mode.INVOKE_UNVERIFIABLE_METHOD) {
+                    options.testing.allowTypeErrors = true;
+                  }
+                  options.enableUninstantiatedTypeOptimizationForInterfaces = true;
+                })
+            .setMinApi(parameters.getRuntime())
+            .run(parameters.getRuntime(), mainClass.name);
+    checkTestRunResult(
+        r8ResultWithUninstantiatedTypeOptimizationForInterfaces,
+        Compiler.R8_ENABLE_UNININSTANTATED_TYPE_OPTIMIZATION_FOR_INTERFACES);
   }
 
   private void checkTestRunResult(TestRunResult<?> result, Compiler compiler) {
@@ -219,7 +241,9 @@
         if (useInterface) {
           result.assertSuccessWithOutput(getExpectedOutput(compiler));
         } else {
-          if (compiler == Compiler.R8 || compiler == Compiler.PROGUARD) {
+          if (compiler == Compiler.R8
+              || compiler == Compiler.R8_ENABLE_UNININSTANTATED_TYPE_OPTIMIZATION_FOR_INTERFACES
+              || compiler == Compiler.PROGUARD) {
             result.assertSuccessWithOutput(getExpectedOutput(compiler));
           } else {
             result
@@ -233,7 +257,8 @@
         if (useInterface) {
           result.assertSuccessWithOutput(getExpectedOutput(compiler));
         } else {
-          if (compiler == Compiler.R8) {
+          if (compiler == Compiler.R8
+              || compiler == Compiler.R8_ENABLE_UNININSTANTATED_TYPE_OPTIMIZATION_FOR_INTERFACES) {
             result
                 .assertFailureWithOutput(getExpectedOutput(compiler))
                 .assertFailureWithErrorThatMatches(
@@ -261,7 +286,9 @@
       if (useInterface) {
         return StringUtils.joinLines("Hello!", "In verifiable method!", "Goodbye!", "");
       } else {
-        if (compiler == Compiler.R8 || compiler == Compiler.PROGUARD) {
+        if (compiler == Compiler.R8
+            || compiler == Compiler.R8_ENABLE_UNININSTANTATED_TYPE_OPTIMIZATION_FOR_INTERFACES
+            || compiler == Compiler.PROGUARD) {
           // The unverifiable method has been removed as a result of tree shaking, so the code does
           // not fail with a verification error when trying to load class `UnverifiableClass`.
           return StringUtils.joinLines("Hello!", "In verifiable method!", "Goodbye!", "");
@@ -274,14 +301,14 @@
     }
     assert mode == Mode.INVOKE_UNVERIFIABLE_METHOD;
     if (useInterface) {
-      if (compiler == Compiler.R8) {
+      if (compiler == Compiler.R8_ENABLE_UNININSTANTATED_TYPE_OPTIMIZATION_FOR_INTERFACES) {
         return StringUtils.joinLines(
             "Hello!",
             "Unexpected outcome of getstatic",
             "Unexpected outcome of checkcast",
             "Goodbye!",
             "");
-      } else if (compiler == Compiler.PROGUARD) {
+      } else if (compiler == Compiler.R8 || compiler == Compiler.PROGUARD) {
         return StringUtils.joinLines("Hello!", "Unexpected outcome of checkcast", "Goodbye!", "");
       } else if (compiler == Compiler.DX || compiler == Compiler.D8) {
         if (ToolHelper.getDexVm().getVersion() == Version.V4_0_4
diff --git a/src/test/java/com/android/tools/r8/shaking/annotations/ReflectiveAnnotationUseTest.java b/src/test/java/com/android/tools/r8/shaking/annotations/ReflectiveAnnotationUseTest.java
index 74f4321..5c6a305 100644
--- a/src/test/java/com/android/tools/r8/shaking/annotations/ReflectiveAnnotationUseTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/annotations/ReflectiveAnnotationUseTest.java
@@ -207,9 +207,9 @@
     assertThat(clazz, isPresent());
     assertEquals(minify, clazz.isRenamed());
     MethodSubject f1 = clazz.uniqueMethodWithName("f1");
-    assertThat(f1, not(isPresent()));
+    assertThat(f1, isPresent());
     MethodSubject f2 = clazz.uniqueMethodWithName("f2");
-    assertThat(f2, not(isPresent()));
+    assertThat(f2, isPresent());
     MethodSubject f3 = clazz.uniqueMethodWithName("f3");
     assertThat(f3, not(isPresent()));
     MethodSubject f4 = clazz.uniqueMethodWithName("f4");
diff --git a/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java b/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
new file mode 100644
index 0000000..8157f47
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
@@ -0,0 +1,199 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.transformers;
+
+import static org.objectweb.asm.Opcodes.ASM7;
+
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.naming.MemberNaming.MethodSignature;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.transformers.MethodTransformer.MethodContext;
+import com.android.tools.r8.utils.DescriptorUtils;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.MethodVisitor;
+
+public class ClassFileTransformer {
+
+  /**
+   * Basic algorithm for transforming the content of a class file.
+   *
+   * <p>The provided transformers are nested in the order given: the first in the list will receive
+   * is call back first, if it forwards to 'super' then the seconds call back will be called, etc,
+   * until finally the writer will be called. If the writer is not called the effect is as if the
+   * callback was never called and its content will not be in the result.
+   */
+  public static byte[] transform(
+      byte[] bytes,
+      List<ClassTransformer> classTransformers,
+      List<MethodTransformer> methodTransformers) {
+    ClassReader reader = new ClassReader(bytes);
+    ClassWriter writer = new ClassWriter(reader, 0);
+    ClassVisitor subvisitor = new InnerMostClassTransformer(writer, methodTransformers);
+    for (int i = classTransformers.size() - 1; i >= 0; i--) {
+      classTransformers.get(i).setSubVisitor(subvisitor);
+      subvisitor = classTransformers.get(i);
+    }
+    reader.accept(subvisitor, 0);
+    return writer.toByteArray();
+  }
+
+  // Inner-most bride from the class transformation to the method transformers.
+  private static class InnerMostClassTransformer extends ClassVisitor {
+    ClassReference classReference;
+    final List<MethodTransformer> methodTransformers;
+
+    InnerMostClassTransformer(ClassWriter writer, List<MethodTransformer> methodTransformers) {
+      super(ASM7, writer);
+      this.methodTransformers = methodTransformers;
+    }
+
+    @Override
+    public void visit(
+        int version,
+        int access,
+        String name,
+        String signature,
+        String superName,
+        String[] interfaces) {
+      super.visit(version, access, name, signature, superName, interfaces);
+      classReference = Reference.classFromBinaryName(name);
+    }
+
+    @Override
+    public MethodVisitor visitMethod(
+        int access, String name, String descriptor, String signature, String[] exceptions) {
+      MethodContext context = createMethodContext(access, name, descriptor);
+      MethodVisitor subvisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
+      for (int i = methodTransformers.size() - 1; i >= 0; i--) {
+        MethodTransformer transformer = methodTransformers.get(i);
+        transformer.setSubVisitor(subvisitor);
+        transformer.setContext(context);
+        subvisitor = transformer;
+      }
+      return subvisitor;
+    }
+
+    private MethodContext createMethodContext(int access, String name, String descriptor) {
+      // Maybe clean up this parsing of info as it is not very nice.
+      MethodSignature methodSignature = MethodSignature.fromSignature(name, descriptor);
+      MethodReference methodReference =
+          Reference.method(
+              classReference,
+              name,
+              Arrays.stream(methodSignature.parameters)
+                  .map(DescriptorUtils::javaTypeToDescriptor)
+                  .map(Reference::typeFromDescriptor)
+                  .collect(Collectors.toList()),
+              methodSignature.type.equals("void")
+                  ? null
+                  : Reference.typeFromDescriptor(
+                      DescriptorUtils.javaTypeToDescriptor(methodSignature.type)));
+      return new MethodContext(methodReference, access);
+    }
+  }
+
+  // Transformer utilities.
+
+  private final byte[] bytes;
+  private final List<ClassTransformer> classTransformers = new ArrayList<>();
+  private final List<MethodTransformer> methodTransformers = new ArrayList<>();
+
+  private ClassFileTransformer(byte[] bytes) {
+    this.bytes = bytes;
+  }
+
+  public static ClassFileTransformer create(byte[] bytes) {
+    return new ClassFileTransformer(bytes);
+  }
+
+  public static ClassFileTransformer create(Class<?> clazz) throws IOException {
+    return create(ToolHelper.getClassAsBytes(clazz));
+  }
+
+  public byte[] transform() {
+    return ClassFileTransformer.transform(bytes, classTransformers, methodTransformers);
+  }
+
+  /** Base addition of a transformer on the class. */
+  public ClassFileTransformer addClassTransformer(ClassTransformer transformer) {
+    classTransformers.add(transformer);
+    return this;
+  }
+
+  /** Base addtion of a transformer on methods. */
+  public ClassFileTransformer addMethodTransformer(MethodTransformer transformer) {
+    methodTransformers.add(transformer);
+    return this;
+  }
+
+  /** Unconditionally replace the implements clause of a class. */
+  public ClassFileTransformer setImplements(Class<?>... interfaces) {
+    return addClassTransformer(
+        new ClassTransformer() {
+          @Override
+          public void visit(
+              int version,
+              int access,
+              String name,
+              String signature,
+              String superName,
+              String[] ignoredInterfaces) {
+            super.visit(
+                version,
+                access,
+                name,
+                signature,
+                superName,
+                Arrays.stream(interfaces)
+                    .map(clazz -> DescriptorUtils.getBinaryNameFromJavaType(clazz.getTypeName()))
+                    .toArray(String[]::new));
+          }
+        });
+  }
+
+  /** Abstraction of the MethodVisitor.visitMethodInsn method with its continuation. */
+  @FunctionalInterface
+  public interface MethodInsnTransform {
+    void visitMethodInsn(
+        int opcode,
+        String owner,
+        String name,
+        String descriptor,
+        boolean isInterface,
+        MethodInsnTransformContinuation continuation);
+  }
+
+  /** Continuation for transforming a method. Will continue with the super visitor if called. */
+  @FunctionalInterface
+  public interface MethodInsnTransformContinuation {
+    void visitMethodInsn(
+        int opcode, String owner, String name, String descriptor, boolean isInterface);
+  }
+
+  public ClassFileTransformer transformMethodInsnInMethod(
+      String methodName, MethodInsnTransform transform) {
+    return addMethodTransformer(
+        new MethodTransformer() {
+          @Override
+          public void visitMethodInsn(
+              int opcode, String owner, String name, String descriptor, boolean isInterface) {
+            if (getContext().method.getMethodName().equals(methodName)) {
+              transform.visitMethodInsn(
+                  opcode, owner, name, descriptor, isInterface, super::visitMethodInsn);
+            } else {
+              super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
+            }
+          }
+        });
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/transformers/ClassTransformer.java b/src/test/java/com/android/tools/r8/transformers/ClassTransformer.java
new file mode 100644
index 0000000..885257e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/transformers/ClassTransformer.java
@@ -0,0 +1,25 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.transformers;
+
+import static org.objectweb.asm.Opcodes.ASM7;
+
+import org.objectweb.asm.ClassVisitor;
+
+/**
+ * Class for transforming the content of a class.
+ *
+ * <p>This is just a simple wrapper on the ASM ClassVisitor interface.
+ */
+public class ClassTransformer extends ClassVisitor {
+  public ClassTransformer() {
+    super(ASM7, null);
+  }
+
+  // Package internals.
+
+  void setSubVisitor(ClassVisitor visitor) {
+    this.cv = visitor;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/transformers/MethodTransformer.java b/src/test/java/com/android/tools/r8/transformers/MethodTransformer.java
new file mode 100644
index 0000000..a5ffa82
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/transformers/MethodTransformer.java
@@ -0,0 +1,57 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.transformers;
+
+import static org.objectweb.asm.Opcodes.ASM7;
+
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.MethodReference;
+import org.objectweb.asm.MethodVisitor;
+
+/**
+ * Class for transforming the content of a method.
+ *
+ * <p>This is just a simple wrapper on the ASM MethodVisitor interface with some added methods for
+ * obtaining context information.
+ */
+public class MethodTransformer extends MethodVisitor {
+
+  static class MethodContext {
+    public final MethodReference method;
+    public final int accessFlags;
+
+    public MethodContext(MethodReference method, int accessFlags) {
+      this.method = method;
+      this.accessFlags = accessFlags;
+    }
+  }
+
+  private MethodContext context;
+
+  public MethodTransformer() {
+    super(ASM7, null);
+  }
+
+  public ClassReference getHolder() {
+    return getContext().method.getHolderClass();
+  }
+
+  public MethodReference getMethod() {
+    return getContext().method;
+  }
+
+  // Package internals.
+
+  MethodContext getContext() {
+    return context;
+  }
+
+  void setSubVisitor(MethodVisitor visitor) {
+    this.mv = visitor;
+  }
+
+  void setContext(MethodContext context) {
+    this.context = context;
+  }
+}