[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");
+ }
+ }
+ }
+}