[KeepAnno] Conservatively extract rules for inclusive instance-of

Bug: b/248408342
Change-Id: I0d6290be4e69cb9db4a7925370eb89841ac9b777
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java
index f6aca4d..ed65592 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java
@@ -505,7 +505,7 @@
         if (classItemPattern.isMemberItemPattern() && items.size() == 1) {
             throw new KeepEdgeException("@KeepForApi kind must include its class");
         }
-        if (!classItemPattern.getExtendsPattern().isAny()) {
+        if (!classItemPattern.getInstanceOfPattern().isAny()) {
           throw new KeepEdgeException("@KeepForApi cannot define an 'extends' pattern.");
         }
         consequences.addTarget(KeepTarget.builder().setItemReference(item).build());
@@ -684,7 +684,7 @@
         if (itemPattern.isMemberItemPattern() && items.size() == 1) {
           throw new KeepEdgeException("@" + getAnnotationName() + " kind must include its class");
         }
-        if (!holderPattern.getExtendsPattern().isAny()) {
+        if (!holderPattern.getInstanceOfPattern().isAny()) {
           throw new KeepEdgeException(
               "@" + getAnnotationName() + " cannot define an 'extends' pattern.");
         }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java
index e11e1cc..568e6fe 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java
@@ -155,7 +155,7 @@
     } else {
       throw new Unimplemented();
     }
-    if (!classItemPattern.getExtendsPattern().isAny()) {
+    if (!classItemPattern.getInstanceOfPattern().isAny()) {
       throw new Unimplemented();
     }
   }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepClassItemPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepClassItemPattern.java
index ae2a330..d26b888 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepClassItemPattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepClassItemPattern.java
@@ -27,7 +27,7 @@
 
     public Builder copyFrom(com.android.tools.r8.keepanno.ast.KeepClassItemPattern pattern) {
       return setClassNamePattern(pattern.getClassNamePattern())
-          .setInstanceOfPattern(pattern.getExtendsPattern());
+          .setInstanceOfPattern(pattern.getInstanceOfPattern());
     }
 
     public Builder setClassNamePattern(KeepQualifiedClassNamePattern classNamePattern) {
@@ -47,14 +47,14 @@
   }
 
   private final KeepQualifiedClassNamePattern classNamePattern;
-  private final KeepInstanceOfPattern extendsPattern;
+  private final KeepInstanceOfPattern instanceOfPattern;
 
   public KeepClassItemPattern(
       KeepQualifiedClassNamePattern classNamePattern, KeepInstanceOfPattern extendsPattern) {
     assert classNamePattern != null;
     assert extendsPattern != null;
     this.classNamePattern = classNamePattern;
-    this.extendsPattern = extendsPattern;
+    this.instanceOfPattern = extendsPattern;
   }
 
   @Override
@@ -80,12 +80,12 @@
     return classNamePattern;
   }
 
-  public KeepInstanceOfPattern getExtendsPattern() {
-    return extendsPattern;
+  public KeepInstanceOfPattern getInstanceOfPattern() {
+    return instanceOfPattern;
   }
 
   public boolean isAny() {
-    return classNamePattern.isAny() && extendsPattern.isAny();
+    return classNamePattern.isAny() && instanceOfPattern.isAny();
   }
 
   @Override
@@ -98,12 +98,12 @@
     }
     KeepClassItemPattern that = (KeepClassItemPattern) obj;
     return classNamePattern.equals(that.classNamePattern)
-        && extendsPattern.equals(that.extendsPattern);
+        && instanceOfPattern.equals(that.instanceOfPattern);
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(classNamePattern, extendsPattern);
+    return Objects.hash(classNamePattern, instanceOfPattern);
   }
 
   @Override
@@ -111,8 +111,8 @@
     return "KeepClassItemPattern"
         + "{ class="
         + classNamePattern
-        + ", extends="
-        + extendsPattern
+        + ", instance-of="
+        + instanceOfPattern
         + '}';
   }
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepInstanceOfPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepInstanceOfPattern.java
index 410bc26..79e2f43 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepInstanceOfPattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepInstanceOfPattern.java
@@ -64,6 +64,11 @@
     }
 
     @Override
+    public boolean isInclusive() {
+      return isInclusive;
+    }
+
+    @Override
     public KeepQualifiedClassNamePattern getClassNamePattern() {
       return namePattern;
     }
@@ -87,7 +92,8 @@
 
     @Override
     public String toString() {
-      return namePattern.toString();
+      String nameString = namePattern.toString();
+      return isInclusive ? nameString : ("excl(" + nameString + ")");
     }
   }
 
@@ -100,4 +106,10 @@
   public abstract boolean isAny();
 
   public abstract KeepQualifiedClassNamePattern getClassNamePattern();
+
+  public abstract boolean isInclusive();
+
+  public final boolean isExclusive() {
+    return !isInclusive();
+  }
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodParametersPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodParametersPattern.java
index c67fd9a..aec5fbf 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodParametersPattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodParametersPattern.java
@@ -6,6 +6,7 @@
 import com.google.common.collect.ImmutableList;
 import java.util.Collections;
 import java.util.List;
+import java.util.stream.Collectors;
 
 public abstract class KeepMethodParametersPattern {
 
@@ -88,6 +89,13 @@
     public int hashCode() {
       return parameterPatterns.hashCode();
     }
+
+    @Override
+    public String toString() {
+      return "("
+          + parameterPatterns.stream().map(Object::toString).collect(Collectors.joining(", "))
+          + ")";
+    }
   }
 
   private static class Any extends KeepMethodParametersPattern {
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractor.java b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractor.java
index ec5d61a..41f593e 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractor.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractor.java
@@ -9,7 +9,6 @@
 import com.android.tools.r8.keepanno.ast.KeepCheck;
 import com.android.tools.r8.keepanno.ast.KeepCheck.KeepCheckKind;
 import com.android.tools.r8.keepanno.ast.KeepClassItemPattern;
-import com.android.tools.r8.keepanno.ast.KeepClassItemReference;
 import com.android.tools.r8.keepanno.ast.KeepCondition;
 import com.android.tools.r8.keepanno.ast.KeepDeclaration;
 import com.android.tools.r8.keepanno.ast.KeepEdge;
@@ -17,6 +16,7 @@
 import com.android.tools.r8.keepanno.ast.KeepEdgeMetaInfo;
 import com.android.tools.r8.keepanno.ast.KeepFieldAccessPattern;
 import com.android.tools.r8.keepanno.ast.KeepFieldPattern;
+import com.android.tools.r8.keepanno.ast.KeepInstanceOfPattern;
 import com.android.tools.r8.keepanno.ast.KeepItemPattern;
 import com.android.tools.r8.keepanno.ast.KeepItemReference;
 import com.android.tools.r8.keepanno.ast.KeepMemberItemPattern;
@@ -135,25 +135,76 @@
     return rules;
   }
 
-  /**
-   * Utility to package up a class binding with its name and item pattern.
-   *
-   * <p>This is useful as the normalizer will have introduced class reference indirections so a
-   * given item may need to.
-   */
+  /** Utility to package up a class binding with its name and item pattern. */
   public static class Holder {
-    final KeepClassItemPattern itemPattern;
-    final KeepQualifiedClassNamePattern namePattern;
+    private final KeepClassItemPattern itemPattern;
 
     static Holder create(KeepBindingSymbol bindingName, KeepBindings bindings) {
       KeepClassItemPattern itemPattern = bindings.get(bindingName).getItem().asClassItemPattern();
-      KeepQualifiedClassNamePattern namePattern = getClassNamePattern(itemPattern, bindings);
-      return new Holder(itemPattern, namePattern);
+      return new Holder(itemPattern);
     }
 
-    private Holder(KeepClassItemPattern itemPattern, KeepQualifiedClassNamePattern namePattern) {
+    private Holder(KeepClassItemPattern itemPattern) {
+      assert itemPattern != null;
       this.itemPattern = itemPattern;
-      this.namePattern = namePattern;
+    }
+
+    public KeepClassItemPattern getClassItemPattern() {
+      return itemPattern;
+    }
+
+    public KeepQualifiedClassNamePattern getNamePattern() {
+      return getClassItemPattern().getClassNamePattern();
+    }
+
+    public void onTargetHolders(Consumer<Holder> fn) {
+      KeepInstanceOfPattern instanceOfPattern = itemPattern.getInstanceOfPattern();
+      if (instanceOfPattern.isAny()) {
+        // An any-pattern does not give rise to 'extends' and maps as is.
+        fn.accept(this);
+        return;
+      }
+      if (instanceOfPattern.isExclusive()) {
+        // An exclusive-pattern maps to the "extends" clause as is.
+        fn.accept(this);
+        return;
+      }
+      if (getNamePattern().isExact()) {
+        // This case is a pattern of "Foo instance-of Bar" and only makes sense if Foo==Bar.
+        // In any case we can conservatively cover this case by ignoring the instance-of clause.
+        Holder holderWithoutExtends =
+            new Holder(
+                KeepClassItemPattern.builder()
+                    .copyFrom(itemPattern)
+                    .setInstanceOfPattern(KeepInstanceOfPattern.any())
+                    .build());
+        fn.accept(holderWithoutExtends);
+        return;
+      }
+      if (getNamePattern().isAny()) {
+        // This case is a pattern of "* instance-of Bar" and we match that as two rules, one of
+        // which is just the rule on the instance-of moved to the class name.
+        Holder holderWithInstanceOfAsName =
+            new Holder(
+                KeepClassItemPattern.builder()
+                    .copyFrom(itemPattern)
+                    .setClassNamePattern(instanceOfPattern.getClassNamePattern())
+                    .setInstanceOfPattern(KeepInstanceOfPattern.any())
+                    .build());
+        fn.accept(this);
+        fn.accept(holderWithInstanceOfAsName);
+        return;
+      }
+      // The remaining case is the general "*Foo* instance-of *Bar*" case. Here it unfolds to two
+      // cases matching anything of the form "*Foo*" and the other being the exclusive extends.
+      Holder holderWithNoInstanceOf =
+          new Holder(
+              KeepClassItemPattern.builder()
+                  .copyFrom(itemPattern)
+                  .setInstanceOfPattern(KeepInstanceOfPattern.any())
+                  .build());
+      fn.accept(this);
+      fn.accept(holderWithNoInstanceOf);
     }
   }
 
@@ -290,12 +341,14 @@
   @FunctionalInterface
   private interface OnTargetCallback {
     void accept(
+        Holder targetHolder,
         Map<KeepBindingSymbol, KeepMemberPattern> memberPatterns,
         List<KeepBindingSymbol> memberTargets,
         TargetKeepKind keepKind);
   }
 
   private static void computeTargets(
+      Holder targetHolder,
       Set<KeepBindingSymbol> targets,
       KeepBindings bindings,
       Map<KeepBindingSymbol, KeepMemberPattern> memberPatterns,
@@ -315,7 +368,10 @@
     if (targetMembers.isEmpty()) {
       keepKind = TargetKeepKind.CLASS_OR_MEMBERS;
     }
-    callback.accept(memberPatterns, targetMembers, keepKind);
+    final TargetKeepKind finalKeepKind = keepKind;
+    targetHolder.onTargetHolders(
+        newTargetHolder ->
+            callback.accept(newTargetHolder, memberPatterns, targetMembers, finalKeepKind));
   }
 
   private static void createUnconditionalRules(
@@ -326,16 +382,17 @@
       KeepOptions options,
       Set<KeepBindingSymbol> targets) {
     computeTargets(
+        holder,
         targets,
         bindings,
         new HashMap<>(),
-        (memberPatterns, targetMembers, targetKeepKind) -> {
+        (targetHolder, memberPatterns, targetMembers, targetKeepKind) -> {
           if (targetKeepKind.equals(TargetKeepKind.JUST_MEMBERS)) {
             // Members dependent on the class, so they go to the implicitly dependent rule.
             rules.add(
                 new PgDependentMembersRule(
                     metaInfo,
-                    holder,
+                    targetHolder,
                     options,
                     memberPatterns,
                     Collections.emptyList(),
@@ -344,7 +401,12 @@
           } else {
             rules.add(
                 new PgUnconditionalRule(
-                    metaInfo, holder, options, memberPatterns, targetMembers, targetKeepKind));
+                    metaInfo,
+                    targetHolder,
+                    options,
+                    memberPatterns,
+                    targetMembers,
+                    targetKeepKind));
           }
         });
   }
@@ -358,8 +420,8 @@
       KeepOptions options,
       Set<KeepBindingSymbol> conditions,
       Set<KeepBindingSymbol> targets) {
-    if (conditionHolder.namePattern.isExact()
-        && conditionHolder.itemPattern.equals(targetHolder.itemPattern)) {
+    if (conditionHolder.getNamePattern().isExact()
+        && conditionHolder.getClassItemPattern().equals(targetHolder.getClassItemPattern())) {
       // If the targets are conditional on its holder, the rule can be simplified as a dependent
       // rule. Note that this is only valid on an *exact* class matching as otherwise any
       // wildcard is allowed to be matched independently on the left and right of the edge.
@@ -370,16 +432,17 @@
     List<KeepBindingSymbol> conditionMembers =
         computeConditions(conditions, bindings, memberPatterns);
     computeTargets(
+        targetHolder,
         targets,
         bindings,
         memberPatterns,
-        (ignore, targetMembers, targetKeepKind) ->
+        (newTargetHolder, ignore, targetMembers, targetKeepKind) ->
             rules.add(
                 new PgConditionalRule(
                     metaInfo,
                     options,
                     conditionHolder,
-                    targetHolder,
+                    newTargetHolder,
                     memberPatterns,
                     conditionMembers,
                     targetMembers,
@@ -388,7 +451,7 @@
 
   private static void createDependentRules(
       List<PgRule> rules,
-      Holder holder,
+      Holder initialHolder,
       KeepEdgeMetaInfo metaInfo,
       KeepBindings bindings,
       KeepOptions options,
@@ -398,10 +461,11 @@
     List<KeepBindingSymbol> conditionMembers =
         computeConditions(conditions, bindings, memberPatterns);
     computeTargets(
+        initialHolder,
         targets,
         bindings,
         memberPatterns,
-        (ignore, targetMembers, targetKeepKind) -> {
+        (holder, ignore, targetMembers, targetKeepKind) -> {
           List<KeepBindingSymbol> nonAllMemberTargets = new ArrayList<>(targetMembers.size());
           for (KeepBindingSymbol targetMember : targetMembers) {
             KeepMemberPattern memberPattern = memberPatterns.get(targetMember);
@@ -464,18 +528,6 @@
     return KeepFieldPattern.builder().setAccessPattern(accessPattern).build();
   }
 
-  private static KeepQualifiedClassNamePattern getClassNamePattern(
-      KeepItemPattern itemPattern, KeepBindings bindings) {
-    if (itemPattern.isClassItemPattern()) {
-      return itemPattern.asClassItemPattern().getClassNamePattern();
-    }
-    KeepMemberItemPattern memberItemPattern = itemPattern.asMemberItemPattern();
-    KeepClassItemReference classReference = memberItemPattern.getClassReference();
-    assert classReference.isBindingReference();
-    return getClassNamePattern(
-        bindings.get(classReference.asBindingReference()).getItem(), bindings);
-  }
-
   private static KeepBindingSymbol getClassItemBindingReference(
       KeepItemReference itemReference, KeepBindings bindings) {
     KeepBindingSymbol classReference = null;
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/PgRule.java b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/PgRule.java
index 0f06c52..96e4bde 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/PgRule.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/PgRule.java
@@ -196,8 +196,8 @@
         TargetKeepKind targetKeepKind) {
       super(metaInfo, options);
       assert !targetKeepKind.equals(TargetKeepKind.JUST_MEMBERS);
-      this.holderNamePattern = holder.namePattern;
-      this.holderPattern = holder.itemPattern;
+      this.holderNamePattern = holder.getNamePattern();
+      this.holderPattern = holder.getClassItemPattern();
       this.targetKeepKind = targetKeepKind;
       this.memberPatterns = memberPatterns;
       this.targetMembers = targetMembers;
@@ -257,8 +257,8 @@
         List<KeepBindingSymbol> memberTargets,
         TargetKeepKind keepKind) {
       super(metaInfo, options);
-      this.classCondition = classCondition.itemPattern;
-      this.classTarget = classTarget.itemPattern;
+      this.classCondition = classCondition.getClassItemPattern();
+      this.classTarget = classTarget.getClassItemPattern();
       this.memberPatterns = memberPatterns;
       this.memberConditions = memberConditions;
       this.memberTargets = memberTargets;
@@ -351,8 +351,8 @@
         List<KeepBindingSymbol> memberTargets,
         TargetKeepKind keepKind) {
       super(metaInfo, options);
-      this.holderNamePattern = holder.namePattern;
-      this.holderPattern = holder.itemPattern;
+      this.holderNamePattern = holder.getNamePattern();
+      this.holderPattern = holder.getClassItemPattern();
       this.memberPatterns = memberPatterns;
       this.memberConditions = memberConditions;
       this.memberTargets = memberTargets;
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrintingUtils.java b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrintingUtils.java
index 3a73dfd..3669002 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrintingUtils.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrintingUtils.java
@@ -93,7 +93,7 @@
       BiConsumer<StringBuilder, KeepQualifiedClassNamePattern> printClassName) {
     builder.append("class ");
     printClassName.accept(builder, classPattern.getClassNamePattern());
-    KeepInstanceOfPattern extendsPattern = classPattern.getExtendsPattern();
+    KeepInstanceOfPattern extendsPattern = classPattern.getInstanceOfPattern();
     if (!extendsPattern.isAny()) {
       builder.append(" extends ");
       printClassName(
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepInclusiveInstanceOfTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepInclusiveInstanceOfTest.java
index 366975d..5991554 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepInclusiveInstanceOfTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepInclusiveInstanceOfTest.java
@@ -48,8 +48,7 @@
         .addKeepMainRule(TestClass.class)
         .setMinApi(parameters)
         .run(parameters.getRuntime(), TestClass.class)
-        // TODO(b/248408342): This should be expected one "instance-of" is inclusive.
-        .assertFailureWithErrorThatThrows(NoSuchMethodException.class);
+        .assertSuccessWithOutput(EXPECTED);
   }
 
   public List<Class<?>> getInputClasses() {
@@ -57,13 +56,13 @@
   }
 
   static class Base {
-    void hiddenMethod() {
+    static void hiddenMethod() {
       System.out.println("on Base");
     }
   }
 
   static class Sub extends Base {
-    void hiddenMethod() {
+    static void hiddenMethod() {
       System.out.println("on Sub");
     }
   }
@@ -71,10 +70,12 @@
   static class A {
 
     @UsesReflection({
+      // Because the method is static, this works whereas `classConstant = Base.class` won't
+      // keep the method on `Sub`.
       @KeepTarget(instanceOfClassConstant = Base.class, methodName = "hiddenMethod")
     })
     public void foo(Base base) throws Exception {
-      base.getClass().getDeclaredMethod("hiddenMethod").invoke(base);
+      base.getClass().getDeclaredMethod("hiddenMethod").invoke(null);
     }
   }
 
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepNameAndInstanceOfTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepNameAndInstanceOfTest.java
new file mode 100644
index 0000000..19505ca
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepNameAndInstanceOfTest.java
@@ -0,0 +1,102 @@
+// 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.keepanno;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.keepanno.annotations.KeepTarget;
+import com.android.tools.r8.keepanno.annotations.UsesReflection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class KeepNameAndInstanceOfTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("on Base", "on Sub");
+  static final String EXPECTED_R8 = StringUtils.lines("on Base", "No method on Sub");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+  }
+
+  public KeepNameAndInstanceOfTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(getInputClasses())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testWithRuleExtraction() throws Exception {
+    testForR8(parameters.getBackend())
+        .enableExperimentalKeepAnnotations()
+        .addProgramClasses(getInputClasses())
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED_R8);
+  }
+
+  public List<Class<?>> getInputClasses() {
+    return ImmutableList.of(TestClass.class, Base.class, Sub.class, A.class);
+  }
+
+  static class Base {
+
+    static void hiddenMethod() {
+      System.out.println("on Base");
+    }
+  }
+
+  static class Sub extends Base {
+
+    static void hiddenMethod() {
+      System.out.println("on Sub");
+    }
+  }
+
+  static class A {
+
+    @UsesReflection({
+      @KeepTarget(
+          // Restricting the matching to Base will cause Sub to be stripped.
+          classConstant = Base.class,
+          instanceOfClassConstant = Base.class,
+          methodName = "hiddenMethod")
+    })
+    public void foo(Base base) throws Exception {
+      base.getClass().getDeclaredMethod("hiddenMethod").invoke(null);
+    }
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) throws Exception {
+      try {
+        new A().foo(new Base());
+      } catch (Exception e) {
+        System.out.println("No method on Base");
+      }
+      try {
+        new A().foo(new Sub());
+      } catch (Exception e) {
+        System.out.println("No method on Sub");
+      }
+    }
+  }
+}