Test for adding record synthetics to profiles

Bug: b/265729283
Change-Id: Ie925ce80e6af98cdcac9774d9354a1da21268f0a
diff --git a/src/main/java/com/android/tools/r8/utils/MethodReferenceUtils.java b/src/main/java/com/android/tools/r8/utils/MethodReferenceUtils.java
index 251edcc..558ea43 100644
--- a/src/main/java/com/android/tools/r8/utils/MethodReferenceUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/MethodReferenceUtils.java
@@ -17,6 +17,7 @@
 import com.android.tools.r8.references.TypeReference;
 import com.google.common.collect.ImmutableList;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.Iterator;
@@ -74,6 +75,11 @@
     return Reference.method(type, "<init>", Collections.emptyList(), null);
   }
 
+  public static MethodReference instanceConstructor(
+      ClassReference type, TypeReference... formalTypes) {
+    return Reference.method(type, "<init>", Arrays.asList(formalTypes), null);
+  }
+
   public static int compare(MethodReference methodReference, ClassReference other) {
     return ClassReferenceUtils.compare(other, methodReference) * -1;
   }
diff --git a/src/test/java/com/android/tools/r8/R8TestCompileResult.java b/src/test/java/com/android/tools/r8/R8TestCompileResult.java
index 679584b..1ca26f0 100644
--- a/src/test/java/com/android/tools/r8/R8TestCompileResult.java
+++ b/src/test/java/com/android/tools/r8/R8TestCompileResult.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.shaking.ProguardConfigurationRule;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ThrowingBiConsumer;
 import com.android.tools.r8.utils.ThrowingConsumer;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
@@ -98,7 +99,12 @@
 
   @Override
   public CodeInspector inspector() throws IOException {
-    return new CodeInspector(app, proguardMap);
+    return inspector(null);
+  }
+
+  public CodeInspector inspector(Consumer<InternalOptions> debugOptionsConsumer)
+      throws IOException {
+    return new CodeInspector(app, proguardMap, debugOptionsConsumer);
   }
 
   private CodeInspector featureInspector(Path feature) throws IOException {
diff --git a/src/test/java/com/android/tools/r8/TestParameters.java b/src/test/java/com/android/tools/r8/TestParameters.java
index 1074507..aabdd99 100644
--- a/src/test/java/com/android/tools/r8/TestParameters.java
+++ b/src/test/java/com/android/tools/r8/TestParameters.java
@@ -94,6 +94,17 @@
     return false;
   }
 
+  public boolean canUseRecords() {
+    assert isCfRuntime() || isDexRuntime();
+    return isCfRuntime() && asCfRuntime().isNewerThanOrEqual(CfVm.JDK14);
+  }
+
+  public boolean canUseRecordsWhenDesugaring() {
+    assert isCfRuntime() || isDexRuntime();
+    assert apiLevel != null;
+    return false;
+  }
+
   // Convenience predicates.
   public boolean isDexRuntime() {
     return runtime.isDex();
@@ -182,6 +193,15 @@
     return this;
   }
 
+  public TestParameters assumeJvmTestParameters() {
+    assertFalse(
+        "No need to use assumeR8TestParameters() when not using api levels for CF",
+        apiLevel == null);
+    assumeCfRuntime();
+    assumeTrue(representativeApiLevelForRuntime);
+    return this;
+  }
+
   public TestParameters assumeR8TestParameters() {
     assertFalse(
         "No need to use assumeR8TestParameters() when not using api levels for CF",
diff --git a/src/test/java/com/android/tools/r8/profile/art/completeness/RecordProfileRewritingTest.java b/src/test/java/com/android/tools/r8/profile/art/completeness/RecordProfileRewritingTest.java
new file mode 100644
index 0000000..dcf365b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/profile/art/completeness/RecordProfileRewritingTest.java
@@ -0,0 +1,289 @@
+// Copyright (c) 2023, 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.profile.art.completeness;
+
+import static com.android.tools.r8.ir.desugar.records.RecordDesugaring.EQUALS_RECORD_METHOD_NAME;
+import static com.android.tools.r8.ir.desugar.records.RecordDesugaring.GET_FIELDS_AS_OBJECTS_METHOD_NAME;
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethod;
+import static com.android.tools.r8.utils.codeinspector.Matchers.ifThen;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.notIf;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.D8TestCompileResult;
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.desugar.records.RecordTestUtils;
+import com.android.tools.r8.profile.art.model.ExternalArtProfile;
+import com.android.tools.r8.profile.art.utils.ArtProfileInspector;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
+import com.android.tools.r8.utils.InternalOptions.InlinerOptions;
+import com.android.tools.r8.utils.MethodReferenceUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.collect.ImmutableList;
+import java.util.Collections;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class RecordProfileRewritingTest extends TestBase {
+
+  private static final String RECORD_NAME = "SimpleRecord";
+  private static final byte[][] PROGRAM_DATA = RecordTestUtils.getProgramData(RECORD_NAME);
+  private static final String EXPECTED_RESULT =
+      StringUtils.lines("Jane Doe", "42", "Jane Doe", "42");
+
+  private static final ClassReference MAIN_REFERENCE =
+      Reference.classFromTypeName(RecordTestUtils.getMainType(RECORD_NAME));
+  private static final ClassReference PERSON_REFERENCE =
+      Reference.classFromTypeName(MAIN_REFERENCE.getTypeName() + "$Person");
+  private static final ClassReference RECORD_REFERENCE =
+      Reference.classFromTypeName("java.lang.Record");
+  private static final ClassReference OBJECT_REFERENCE = Reference.classFromClass(Object.class);
+  private static final ClassReference STRING_REFERENCE = Reference.classFromClass(String.class);
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build();
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    parameters.assumeJvmTestParameters();
+    assumeTrue(parameters.canUseRecords());
+    testForJvm()
+        .addProgramClassFileData(PROGRAM_DATA)
+        .run(parameters.getRuntime(), MAIN_REFERENCE.getTypeName())
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    D8TestCompileResult compileResult =
+        testForD8(parameters.getBackend())
+            .addProgramClassFileData(PROGRAM_DATA)
+            .addArtProfileForRewriting(getArtProfile())
+            .setMinApi(parameters.getApiLevel())
+            .compile();
+    compileResult
+        .inspectResidualArtProfile(
+            profileInspector ->
+                compileResult.inspectWithOptions(
+                    inspector -> inspectD8(profileInspector, inspector),
+                    options -> options.testing.disableRecordApplicationReaderMap = true))
+        .run(parameters.getRuntime(), MAIN_REFERENCE.getTypeName())
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    parameters.assumeR8TestParameters();
+    assumeTrue(parameters.canUseRecords() || parameters.isDexRuntime());
+    R8TestCompileResult compileResult =
+        testForR8(parameters.getBackend())
+            .addProgramClassFileData(PROGRAM_DATA)
+            .addKeepMainRule(MAIN_REFERENCE.getTypeName())
+            .addKeepRules(
+                "-neverpropagatevalue class " + PERSON_REFERENCE.getTypeName() + " { <fields>; }")
+            .addArtProfileForRewriting(getArtProfile())
+            .addOptionsModification(InlinerOptions::disableInlining)
+            .applyIf(
+                parameters.isCfRuntime(),
+                testBuilder ->
+                    testBuilder.addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp)))
+            .enableProguardTestOptions()
+            .noHorizontalClassMergingOfSynthetics()
+            .setMinApi(parameters.getApiLevel())
+            .compile();
+    compileResult
+        .inspectResidualArtProfile(
+            profileInspector ->
+                compileResult.inspectWithOptions(
+                    inspector -> inspectR8(profileInspector, inspector),
+                    options -> options.testing.disableRecordApplicationReaderMap = true))
+        .run(parameters.getRuntime(), MAIN_REFERENCE.getTypeName())
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+
+  private ExternalArtProfile getArtProfile() {
+    return ExternalArtProfile.builder()
+        .addMethodRule(MethodReferenceUtils.mainMethod(MAIN_REFERENCE))
+        .addClassRule(PERSON_REFERENCE)
+        .addMethodRule(
+            MethodReferenceUtils.instanceConstructor(
+                PERSON_REFERENCE, STRING_REFERENCE, Reference.INT))
+        .addMethodRule(
+            Reference.method(PERSON_REFERENCE, "name", Collections.emptyList(), STRING_REFERENCE))
+        .addMethodRule(
+            Reference.method(PERSON_REFERENCE, "age", Collections.emptyList(), Reference.INT))
+        .addMethodRule(
+            Reference.method(
+                PERSON_REFERENCE, "equals", ImmutableList.of(OBJECT_REFERENCE), Reference.BOOL))
+        .addMethodRule(
+            Reference.method(PERSON_REFERENCE, "hashCode", Collections.emptyList(), Reference.INT))
+        .addMethodRule(
+            Reference.method(
+                PERSON_REFERENCE, "toString", Collections.emptyList(), STRING_REFERENCE))
+        .build();
+  }
+
+  private void inspectD8(ArtProfileInspector profileInspector, CodeInspector inspector) {
+    inspect(
+        profileInspector,
+        inspector,
+        SyntheticItemsTestUtils.syntheticRecordTagClass(),
+        parameters.canUseNestBasedAccessesWhenDesugaring(),
+        parameters.canUseRecordsWhenDesugaring());
+  }
+
+  private void inspectR8(ArtProfileInspector profileInspector, CodeInspector inspector) {
+    inspect(
+        profileInspector,
+        inspector,
+        RECORD_REFERENCE,
+        parameters.canUseNestBasedAccesses(),
+        parameters.canUseRecords());
+  }
+
+  private void inspect(
+      ArtProfileInspector profileInspector,
+      CodeInspector inspector,
+      ClassReference recordClassReference,
+      boolean canUseNestBasedAccesses,
+      boolean canUseRecords) {
+    ClassSubject mainClassSubject = inspector.clazz(MAIN_REFERENCE);
+    assertThat(mainClassSubject, isPresent());
+
+    MethodSubject mainMethodSubject = mainClassSubject.mainMethod();
+    assertThat(mainMethodSubject, isPresent());
+
+    ClassSubject recordTagClassSubject = inspector.clazz(recordClassReference);
+    assertThat(recordTagClassSubject, notIf(isPresent(), canUseRecords));
+    if (!canUseRecords) {
+      assertEquals(1, recordTagClassSubject.allMethods().size());
+    }
+
+    MethodSubject recordTagInstanceInitializerSubject = recordTagClassSubject.init();
+    assertThat(recordTagInstanceInitializerSubject, notIf(isPresent(), canUseRecords));
+
+    ClassSubject personRecordClassSubject = inspector.clazz(PERSON_REFERENCE);
+    assertThat(personRecordClassSubject, isPresent());
+    assertEquals(
+        canUseRecords
+            ? inspector.getTypeSubject(RECORD_REFERENCE.getTypeName())
+            : recordTagClassSubject.asTypeSubject(),
+        personRecordClassSubject.getSuperType());
+    assertEquals(canUseRecords ? 6 : 10, personRecordClassSubject.allMethods().size());
+
+    MethodSubject personInstanceInitializerSubject =
+        personRecordClassSubject.uniqueInstanceInitializer();
+    assertThat(personInstanceInitializerSubject, isPresent());
+
+    // Name getters.
+    MethodSubject nameMethodSubject = personRecordClassSubject.uniqueMethodWithOriginalName("name");
+    assertThat(nameMethodSubject, isPresent());
+
+    MethodSubject nameNestAccessorMethodSubject =
+        personRecordClassSubject.uniqueMethodWithOriginalName(
+            SyntheticItemsTestUtils.syntheticNestInstanceFieldGetter(
+                    Reference.field(PERSON_REFERENCE, "name", STRING_REFERENCE))
+                .getMethodName());
+    assertThat(nameNestAccessorMethodSubject, notIf(isPresent(), canUseNestBasedAccesses));
+
+    // Age getters.
+    MethodSubject ageMethodSubject = personRecordClassSubject.uniqueMethodWithOriginalName("age");
+    assertThat(ageMethodSubject, isPresent());
+
+    MethodSubject ageNestAccessorMethodSubject =
+        personRecordClassSubject.uniqueMethodWithOriginalName(
+            SyntheticItemsTestUtils.syntheticNestInstanceFieldGetter(
+                    Reference.field(PERSON_REFERENCE, "age", Reference.INT))
+                .getMethodName());
+    assertThat(ageNestAccessorMethodSubject, notIf(isPresent(), canUseNestBasedAccesses));
+
+    // boolean equals(Object)
+    MethodSubject getFieldsAsObjectsMethodSubject =
+        personRecordClassSubject.uniqueMethodWithOriginalName(GET_FIELDS_AS_OBJECTS_METHOD_NAME);
+    assertThat(getFieldsAsObjectsMethodSubject, notIf(isPresent(), canUseRecords));
+
+    MethodSubject equalsHelperMethodSubject =
+        personRecordClassSubject.uniqueMethodWithOriginalName(EQUALS_RECORD_METHOD_NAME);
+    assertThat(equalsHelperMethodSubject, notIf(isPresent(), canUseRecords));
+
+    MethodSubject equalsMethodSubject =
+        personRecordClassSubject.uniqueMethodWithOriginalName("equals");
+    assertThat(equalsMethodSubject, isPresent());
+    assertThat(
+        equalsMethodSubject, ifThen(!canUseRecords, invokesMethod(equalsHelperMethodSubject)));
+
+    // int hashCode()
+    ClassSubject hashCodeHelperClassSubject =
+        inspector.clazz(SyntheticItemsTestUtils.syntheticRecordHelperClass(PERSON_REFERENCE, 0));
+    assertThat(hashCodeHelperClassSubject, notIf(isPresent(), canUseRecords));
+
+    MethodSubject hashCodeHelperMethodSubject = hashCodeHelperClassSubject.uniqueMethod();
+    assertThat(hashCodeHelperMethodSubject, notIf(isPresent(), canUseRecords));
+
+    MethodSubject hashCodeMethodSubject =
+        personRecordClassSubject.uniqueMethodWithOriginalName("hashCode");
+    assertThat(hashCodeMethodSubject, isPresent());
+    assertThat(
+        hashCodeMethodSubject,
+        ifThen(!canUseRecords, invokesMethod(getFieldsAsObjectsMethodSubject)));
+    assertThat(
+        hashCodeMethodSubject, ifThen(!canUseRecords, invokesMethod(hashCodeHelperMethodSubject)));
+
+    // String toString()
+    ClassSubject toStringHelperClassSubject =
+        inspector.clazz(SyntheticItemsTestUtils.syntheticRecordHelperClass(PERSON_REFERENCE, 1));
+    assertThat(toStringHelperClassSubject, notIf(isPresent(), canUseRecords));
+
+    MethodSubject toStringHelperMethodSubject = toStringHelperClassSubject.uniqueMethod();
+    assertThat(toStringHelperMethodSubject, notIf(isPresent(), canUseRecords));
+
+    MethodSubject toStringMethodSubject =
+        personRecordClassSubject.uniqueMethodWithOriginalName("toString");
+    assertThat(toStringMethodSubject, isPresent());
+    assertThat(
+        toStringMethodSubject,
+        ifThen(!canUseRecords, invokesMethod(getFieldsAsObjectsMethodSubject)));
+    assertThat(
+        toStringMethodSubject, ifThen(!canUseRecords, invokesMethod(toStringHelperMethodSubject)));
+
+    // TODO(b/265729283): Should include all the synthetics from above when there is no native
+    //  support.
+    profileInspector
+        .assertContainsClassRule(personRecordClassSubject)
+        .assertContainsMethodRules(
+            mainMethodSubject,
+            personInstanceInitializerSubject,
+            nameMethodSubject,
+            ageMethodSubject,
+            equalsMethodSubject,
+            hashCodeMethodSubject,
+            toStringMethodSubject)
+        .applyIf(
+            !canUseNestBasedAccesses,
+            i ->
+                i.assertContainsMethodRules(
+                    nameNestAccessorMethodSubject, ageNestAccessorMethodSubject))
+        .assertContainsNoOtherRules();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/synthesis/SyntheticItemsTestUtils.java b/src/test/java/com/android/tools/r8/synthesis/SyntheticItemsTestUtils.java
index d7df00b..478af32 100644
--- a/src/test/java/com/android/tools/r8/synthesis/SyntheticItemsTestUtils.java
+++ b/src/test/java/com/android/tools/r8/synthesis/SyntheticItemsTestUtils.java
@@ -13,6 +13,7 @@
 import static com.android.tools.r8.synthesis.SyntheticNaming.EXTERNAL_SYNTHETIC_CLASS_SEPARATOR;
 import static org.hamcrest.CoreMatchers.containsString;
 
+import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.ir.desugar.invokespecial.InvokeSpecialToSelfDesugaring;
 import com.android.tools.r8.ir.desugar.itf.InterfaceDesugaringForTesting;
 import com.android.tools.r8.references.ClassReference;
@@ -130,6 +131,14 @@
     return syntheticClass(classReference, naming.BACKPORT_WITH_FORWARDING, id);
   }
 
+  public static ClassReference syntheticRecordTagClass() {
+    return Reference.classFromDescriptor(DexItemFactory.recordTagDescriptorString);
+  }
+
+  public static ClassReference syntheticRecordHelperClass(ClassReference reference, int id) {
+    return syntheticClass(reference, naming.RECORD_HELPER, id);
+  }
+
   public static ClassReference syntheticTwrCloseResourceClass(Class<?> clazz, int id) {
     return syntheticClass(clazz, naming.TWR_CLOSE_RESOURCE, id);
   }
@@ -155,10 +164,13 @@
   }
 
   public static MethodReference syntheticNestInstanceFieldGetter(Field field) {
-    FieldReference fieldReference = Reference.fieldFromField(field);
+    return syntheticNestInstanceFieldGetter(Reference.fieldFromField(field));
+  }
+
+  public static MethodReference syntheticNestInstanceFieldGetter(FieldReference fieldReference) {
     return Reference.method(
         fieldReference.getHolderClass(),
-        NEST_ACCESS_FIELD_GET_NAME_PREFIX + field.getName(),
+        NEST_ACCESS_FIELD_GET_NAME_PREFIX + fieldReference.getFieldName(),
         Collections.emptyList(),
         fieldReference.getFieldType());
   }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentClassSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentClassSubject.java
index ba1898d..7596ee6 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentClassSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentClassSubject.java
@@ -130,6 +130,11 @@
   }
 
   @Override
+  public TypeSubject getSuperType() {
+    throw new Unreachable("Absent class has no super type");
+  }
+
+  @Override
   public boolean isInterface() {
     throw new Unreachable("Cannot determine if an absent class is an interface");
   }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/ClassSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/ClassSubject.java
index 58dbf1d..1aebbdd 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/ClassSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/ClassSubject.java
@@ -187,6 +187,8 @@
   @Override
   public abstract ClassAccessFlags getAccessFlags();
 
+  public abstract TypeSubject getSuperType();
+
   public abstract boolean isInterface();
 
   public abstract boolean isAbstract();
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/CodeInspector.java b/src/test/java/com/android/tools/r8/utils/codeinspector/CodeInspector.java
index b973782..81c7400 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/CodeInspector.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/CodeInspector.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.utils.codeinspector;
 
+import static com.android.tools.r8.utils.ConsumerUtils.emptyConsumer;
+
 import com.android.tools.r8.DexIndexedConsumer;
 import com.android.tools.r8.StringResource;
 import com.android.tools.r8.TestDiagnosticMessagesImpl;
@@ -159,10 +161,15 @@
   }
 
   public CodeInspector(AndroidApp app, String proguardMapContent) throws IOException {
+    this(app, proguardMapContent, emptyConsumer());
+  }
+
+  public CodeInspector(
+      AndroidApp app, String proguardMapContent, Consumer<InternalOptions> optionsConsumer)
+      throws IOException {
     this(
-        new ApplicationReader(app, runOptionsConsumer(null), Timing.empty())
-            .read(
-                StringResource.fromString(proguardMapContent, Origin.unknown())));
+        new ApplicationReader(app, runOptionsConsumer(optionsConsumer), Timing.empty())
+            .read(StringResource.fromString(proguardMapContent, Origin.unknown())));
   }
 
   public CodeInspector(DexApplication application) {
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/CodeMatchers.java b/src/test/java/com/android/tools/r8/utils/codeinspector/CodeMatchers.java
index 1f9b372..d711ac1 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/CodeMatchers.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/CodeMatchers.java
@@ -4,6 +4,9 @@
 
 package com.android.tools.r8.utils.codeinspector;
 
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.references.ClassReference;
 import com.android.tools.r8.references.FieldReference;
@@ -12,6 +15,7 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.function.Predicate;
+import java.util.function.Supplier;
 import java.util.stream.Collectors;
 import org.hamcrest.Description;
 import org.hamcrest.Matcher;
@@ -179,14 +183,21 @@
   }
 
   public static Matcher<MethodSubject> invokesMethod(MethodSubject targetSubject) {
-    if (!targetSubject.isPresent()) {
-      throw new IllegalArgumentException();
-    }
-    return invokesMethod(targetSubject.getFinalReference());
+    return invokesMethod(
+        () -> {
+          assertThat(targetSubject, isPresent());
+          return targetSubject.getFinalReference();
+        });
   }
 
   public static Matcher<MethodSubject> invokesMethod(MethodReference targetReference) {
+    return invokesMethod(() -> targetReference);
+  }
+
+  public static Matcher<MethodSubject> invokesMethod(
+      Supplier<MethodReference> targetReferenceSupplier) {
     return new TypeSafeMatcher<MethodSubject>() {
+
       @Override
       protected boolean matchesSafely(MethodSubject subject) {
         if (!subject.isPresent()) {
@@ -195,11 +206,13 @@
         if (!subject.getMethod().hasCode()) {
           return false;
         }
+        MethodReference targetReference = targetReferenceSupplier.get();
         return subject.streamInstructions().anyMatch(isInvokeWithTarget(targetReference));
       }
 
       @Override
       public void describeTo(Description description) {
+        MethodReference targetReference = targetReferenceSupplier.get();
         description.appendText(
             "invokes method `" + MethodReferenceUtils.toSourceString(targetReference) + "`");
       }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/FoundClassSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/FoundClassSubject.java
index 738507a..fcfe160 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/FoundClassSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/FoundClassSubject.java
@@ -388,6 +388,11 @@
   }
 
   @Override
+  public TypeSubject getSuperType() {
+    return new TypeSubject(codeInspector, dexClass.getSuperType());
+  }
+
+  @Override
   public String getOriginalName() {
     if (getNaming() != null) {
       return getNaming().originalName;
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/Matchers.java b/src/test/java/com/android/tools/r8/utils/codeinspector/Matchers.java
index 4a3d5f9..82eaa9d 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/Matchers.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/Matchers.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.utils.codeinspector;
 
+import static org.hamcrest.CoreMatchers.anything;
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
@@ -846,6 +847,14 @@
     }
   }
 
+  @SuppressWarnings("unchecked")
+  public static <T> Matcher<T> ifThen(boolean condition, Matcher<T> matcher) {
+    if (condition) {
+      return matcher;
+    }
+    return (Matcher<T>) anything();
+  }
+
   public static <T> Matcher<T> onlyIf(boolean condition, Matcher<T> matcher) {
     return notIf(matcher, !condition);
   }