[KeepAnno] Add support for method return type and parameters.
Bug: b/248408342
Change-Id: Ib6914bdb27f8116ac64acf679cf0ff5e4a1185ea
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepConstants.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepConstants.java
index 260c31c..a9bd519 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepConstants.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepConstants.java
@@ -37,8 +37,16 @@
// Implicit hidden item which is "super type" of Condition and Target.
public static final class Item {
public static final String classConstant = "classConstant";
+
public static final String methodName = "methodName";
+ public static final String methodReturnType = "methodReturnType";
+ public static final String methodParameters = "methodParameters";
+ public static final String methodNameDefaultValue = "";
+ public static final String methodReturnTypeDefaultValue = "";
+ public static final String[] methodParametersDefaultValue = new String[] {"<any>"};
+
public static final String fieldName = "fieldName";
+ public static final String fieldNameDefaultValue = "";
}
public static final class Condition {
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java
index f87e32c..1b9fe64 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java
@@ -17,5 +17,9 @@
String methodName() default "";
+ String methodReturnType() default "";
+
+ String[] methodParameters() default {"<any>"};
+
String fieldName() default "";
}
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 3545a25..4efa5e8 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
@@ -17,12 +17,19 @@
import com.android.tools.r8.keepanno.ast.KeepItemPattern;
import com.android.tools.r8.keepanno.ast.KeepItemPattern.Builder;
import com.android.tools.r8.keepanno.ast.KeepMethodNamePattern;
+import com.android.tools.r8.keepanno.ast.KeepMethodParametersPattern;
import com.android.tools.r8.keepanno.ast.KeepMethodPattern;
+import com.android.tools.r8.keepanno.ast.KeepMethodReturnTypePattern;
import com.android.tools.r8.keepanno.ast.KeepPreconditions;
import com.android.tools.r8.keepanno.ast.KeepQualifiedClassNamePattern;
import com.android.tools.r8.keepanno.ast.KeepTarget;
+import com.android.tools.r8.keepanno.ast.KeepTypePattern;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashSet;
+import java.util.List;
import java.util.Set;
+import java.util.function.Consumer;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
@@ -115,14 +122,24 @@
}
private KeepItemPattern createItemContext() {
- Type returnType = Type.getReturnType(methodDescriptor);
+ String returnTypeDescriptor = Type.getReturnType(methodDescriptor).getDescriptor();
Type[] argumentTypes = Type.getArgumentTypes(methodDescriptor);
- // TODO(b/248408342): Defaults are "any", support setting actual return type and params.
+ KeepMethodParametersPattern.Builder builder = KeepMethodParametersPattern.builder();
+ for (Type type : argumentTypes) {
+ builder.addParameterTypePattern(KeepTypePattern.fromDescriptor(type.getDescriptor()));
+ }
+ KeepMethodReturnTypePattern returnTypePattern =
+ "V".equals(returnTypeDescriptor)
+ ? KeepMethodReturnTypePattern.voidType()
+ : KeepMethodReturnTypePattern.fromType(
+ KeepTypePattern.fromDescriptor(returnTypeDescriptor));
return KeepItemPattern.builder()
.setClassPattern(KeepQualifiedClassNamePattern.exact(className))
.setMemberPattern(
KeepMethodPattern.builder()
.setNamePattern(KeepMethodNamePattern.exact(methodName))
+ .setReturnTypePattern(returnTypePattern)
+ .setParametersPattern(builder.build())
.build())
.build();
}
@@ -320,13 +337,33 @@
private final Parent<KeepItemPattern> parent;
private KeepQualifiedClassNamePattern classNamePattern = null;
- private KeepMethodNamePattern methodName = null;
- private KeepFieldNamePattern fieldName = null;
+ private KeepMethodPattern.Builder lazyMethodBuilder = null;
+ private KeepFieldPattern.Builder lazyFieldBuilder = null;
public KeepItemVisitorBase(Parent<KeepItemPattern> parent) {
this.parent = parent;
}
+ private KeepMethodPattern.Builder methodBuilder() {
+ if (lazyFieldBuilder != null) {
+ throw new KeepEdgeException("Cannot define both a field and a method pattern");
+ }
+ if (lazyMethodBuilder == null) {
+ lazyMethodBuilder = KeepMethodPattern.builder();
+ }
+ return lazyMethodBuilder;
+ }
+
+ private KeepFieldPattern.Builder fieldBuilder() {
+ if (lazyMethodBuilder != null) {
+ throw new KeepEdgeException("Cannot define both a field and a method pattern");
+ }
+ if (lazyFieldBuilder == null) {
+ lazyFieldBuilder = KeepFieldPattern.builder();
+ }
+ return lazyFieldBuilder;
+ }
+
@Override
public void visit(String name, Object value) {
if (name.equals(Item.classConstant) && value instanceof Type) {
@@ -334,36 +371,90 @@
return;
}
if (name.equals(Item.methodName) && value instanceof String) {
- methodName = KeepMethodNamePattern.exact((String) value);
+ String methodName = (String) value;
+ if (!Item.methodNameDefaultValue.equals(methodName)) {
+ methodBuilder().setNamePattern(KeepMethodNamePattern.exact(methodName));
+ }
+ return;
+ }
+ if (name.equals(Item.methodReturnType) && value instanceof String) {
+ String returnType = (String) value;
+ if (!Item.methodReturnTypeDefaultValue.equals(returnType)) {
+ methodBuilder()
+ .setReturnTypePattern(KeepEdgeReaderUtils.methodReturnTypeFromString(returnType));
+ }
return;
}
if (name.equals(Item.fieldName) && value instanceof String) {
- fieldName = KeepFieldNamePattern.exact((String) value);
+ String fieldName = (String) value;
+ if (!Item.fieldNameDefaultValue.equals(fieldName)) {
+ fieldBuilder().setNamePattern(KeepFieldNamePattern.exact(fieldName));
+ }
return;
}
super.visit(name, value);
}
@Override
+ public AnnotationVisitor visitArray(String name) {
+ if (name.equals(Item.methodParameters)) {
+ return new StringArrayVisitor(
+ params -> {
+ if (Arrays.asList(Item.methodParametersDefaultValue).equals(params)) {
+ return;
+ }
+ KeepMethodParametersPattern.Builder builder = KeepMethodParametersPattern.builder();
+ for (String param : params) {
+ builder.addParameterTypePattern(KeepEdgeReaderUtils.typePatternFromString(param));
+ }
+ methodBuilder().setParametersPattern(builder.build());
+ });
+ }
+ return super.visitArray(name);
+ }
+
+ @Override
public void visitEnd() {
+ assert lazyMethodBuilder == null || lazyFieldBuilder == null;
Builder itemBuilder = KeepItemPattern.builder();
if (classNamePattern != null) {
itemBuilder.setClassPattern(classNamePattern);
}
- if (methodName != null && fieldName != null) {
- throw new KeepEdgeException("Cannot define both a field and a method pattern.");
+ if (lazyMethodBuilder != null) {
+ itemBuilder.setMemberPattern(lazyMethodBuilder.build());
}
- if (methodName != null) {
- itemBuilder.setMemberPattern(
- KeepMethodPattern.builder().setNamePattern(methodName).build());
- }
- if (fieldName != null) {
- itemBuilder.setMemberPattern(KeepFieldPattern.builder().setNamePattern(fieldName).build());
+ if (lazyFieldBuilder != null) {
+ itemBuilder.setMemberPattern(lazyFieldBuilder.build());
}
parent.accept(itemBuilder.build());
}
}
+ private static class StringArrayVisitor extends AnnotationVisitorBase {
+
+ private final Consumer<List<String>> fn;
+ private final List<String> strings = new ArrayList<>();
+
+ public StringArrayVisitor(Consumer<List<String>> fn) {
+ this.fn = fn;
+ }
+
+ @Override
+ public void visit(String name, Object value) {
+ if (value instanceof String) {
+ strings.add((String) value);
+ } else {
+ super.visit(name, value);
+ }
+ }
+
+ @Override
+ public void visitEnd() {
+ super.visitEnd();
+ fn.accept(strings);
+ }
+ }
+
private static class KeepTargetVisitor extends KeepItemVisitorBase {
public KeepTargetVisitor(Parent<KeepTarget> parent) {
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReaderUtils.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReaderUtils.java
new file mode 100644
index 0000000..e18bf1e
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReaderUtils.java
@@ -0,0 +1,69 @@
+// Copyright (c) 2022, 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.asm;
+
+import com.android.tools.r8.keepanno.ast.KeepEdgeException;
+import com.android.tools.r8.keepanno.ast.KeepMethodReturnTypePattern;
+import com.android.tools.r8.keepanno.ast.KeepTypePattern;
+
+/**
+ * Utilities for mapping the syntax used in annotations to the keep-edge AST.
+ *
+ * <p>The AST explicitly avoids interpreting type strings as they are potentially ambiguous. These
+ * utilities define the mappings from such syntax strings into the AST.
+ */
+public class KeepEdgeReaderUtils {
+
+ public static KeepTypePattern typePatternFromString(String string) {
+ if (string.equals("<any>")) {
+ return KeepTypePattern.any();
+ }
+ return KeepTypePattern.fromDescriptor(javaTypeToDescriptor(string));
+ }
+
+ public static String javaTypeToDescriptor(String type) {
+ switch (type) {
+ case "boolean":
+ return "Z";
+ case "byte":
+ return "B";
+ case "short":
+ return "S";
+ case "int":
+ return "I";
+ case "long":
+ return "J";
+ case "float":
+ return "F";
+ case "double":
+ return "D";
+ default:
+ {
+ StringBuilder builder = new StringBuilder(type.length());
+ int i = type.length() - 1;
+ while (type.charAt(i) == ']') {
+ if (type.charAt(--i) != '[') {
+ throw new KeepEdgeException("Invalid type: " + type);
+ }
+ builder.append('[');
+ --i;
+ }
+ builder.append('L');
+ for (int j = 0; j <= i; j++) {
+ char c = type.charAt(j);
+ builder.append(c == '.' ? '/' : c);
+ }
+ builder.append(';');
+ return builder.toString();
+ }
+ }
+ }
+
+ public static KeepMethodReturnTypePattern methodReturnTypeFromString(String returnType) {
+ if ("void".equals(returnType)) {
+ return KeepMethodReturnTypePattern.voidType();
+ }
+ return KeepMethodReturnTypePattern.fromType(typePatternFromString(returnType));
+ }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdge.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdge.java
index b2da25a..e1d6d4e 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdge.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdge.java
@@ -33,7 +33,7 @@
* ::= any
* | class QUALIFIED_CLASS_NAME_PATTERN extends EXTENDS_PATTERN { MEMBER_PATTERN }
*
- * TYPE_PATTERN ::= any
+ * TYPE_PATTERN ::= any | exact type-descriptor
* PACKAGE_PATTERN ::= any | exact package-name
* QUALIFIED_CLASS_NAME_PATTERN ::= any | PACKAGE_PATTERN | UNQUALIFIED_CLASS_NAME_PATTERN
* UNQUALIFIED_CLASS_NAME_PATTERN ::= any | exact simple-class-name
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 29442ff..73f8669 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
@@ -3,11 +3,16 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.keepanno.ast;
+import com.google.common.collect.ImmutableList;
import java.util.Collections;
import java.util.List;
public abstract class KeepMethodParametersPattern {
+ public static Builder builder() {
+ return new Builder();
+ }
+
public static KeepMethodParametersPattern any() {
return Any.getInstance();
}
@@ -30,6 +35,25 @@
return null;
}
+ public static class Builder {
+ ImmutableList.Builder<KeepTypePattern> parameterPatterns = ImmutableList.builder();
+
+ private Builder() {}
+
+ public Builder addParameterTypePattern(KeepTypePattern typePattern) {
+ parameterPatterns.add(typePattern);
+ return this;
+ }
+
+ public KeepMethodParametersPattern build() {
+ List<KeepTypePattern> list = parameterPatterns.build();
+ if (list.isEmpty()) {
+ return Some.EMPTY_INSTANCE;
+ }
+ return new Some(list);
+ }
+ }
+
private static class Some extends KeepMethodParametersPattern {
private static final Some EMPTY_INSTANCE = new Some(Collections.emptyList());
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodPattern.java
index e00fedd..f84a4b1 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodPattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodPattern.java
@@ -35,11 +35,15 @@
return self();
}
- public Builder setReturnTypeVoid() {
- returnTypePattern = KeepMethodReturnTypePattern.voidType();
+ public Builder setReturnTypePattern(KeepMethodReturnTypePattern returnTypePattern) {
+ this.returnTypePattern = returnTypePattern;
return self();
}
+ public Builder setReturnTypeVoid() {
+ return setReturnTypePattern(KeepMethodReturnTypePattern.voidType());
+ }
+
public Builder setParametersPattern(KeepMethodParametersPattern parametersPattern) {
this.parametersPattern = parametersPattern;
return self();
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodReturnTypePattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodReturnTypePattern.java
index ccfc183..807b1bc 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodReturnTypePattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodReturnTypePattern.java
@@ -14,6 +14,10 @@
return VoidType.getInstance();
}
+ public static KeepMethodReturnTypePattern fromType(KeepTypePattern typePattern) {
+ return typePattern.isAny() ? any() : new SomeType(typePattern);
+ }
+
public boolean isAny() {
return isType() && asType().isAny();
}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepTypePattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepTypePattern.java
index c7c24b5..872ad07 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepTypePattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepTypePattern.java
@@ -9,7 +9,57 @@
return Any.getInstance();
}
+ public static KeepTypePattern fromDescriptor(String typeDescriptor) {
+ return new Some(typeDescriptor);
+ }
+
+ public boolean isAny() {
+ return false;
+ }
+
+ public String getDescriptor() {
+ return null;
+ }
+
+ private static class Some extends KeepTypePattern {
+
+ private final String descriptor;
+
+ private Some(String descriptor) {
+ assert descriptor != null;
+ this.descriptor = descriptor;
+ }
+
+ @Override
+ public String getDescriptor() {
+ return descriptor;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Some some = (Some) o;
+ return descriptor.equals(some.descriptor);
+ }
+
+ @Override
+ public int hashCode() {
+ return descriptor.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return descriptor;
+ }
+ }
+
private static class Any extends KeepTypePattern {
+
private static final Any INSTANCE = new Any();
public static Any getInstance() {
@@ -33,9 +83,7 @@
@Override
public String toString() {
- return "*";
+ return "<any>";
}
}
-
- public abstract boolean isAny();
}
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 6781a22..286c07a 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
@@ -5,6 +5,7 @@
import com.android.tools.r8.keepanno.ast.KeepConsequences;
import com.android.tools.r8.keepanno.ast.KeepEdge;
+import com.android.tools.r8.keepanno.ast.KeepEdgeException;
import com.android.tools.r8.keepanno.ast.KeepFieldAccessPattern;
import com.android.tools.r8.keepanno.ast.KeepFieldNamePattern;
import com.android.tools.r8.keepanno.ast.KeepFieldPattern;
@@ -157,7 +158,7 @@
.append('(')
.append(
parametersPattern.asList().stream()
- .map(Object::toString)
+ .map(KeepRuleExtractor::getTypePatternString)
.collect(Collectors.joining(", ")))
.append(')');
}
@@ -185,10 +186,7 @@
}
private static StringBuilder printType(StringBuilder builder, KeepTypePattern typePattern) {
- if (typePattern.isAny()) {
- return builder.append("***");
- }
- throw new Unimplemented();
+ return builder.append(getTypePatternString(typePattern));
}
private static StringBuilder printAccess(
@@ -256,6 +254,70 @@
}
}
+ private static String getTypePatternString(KeepTypePattern typePattern) {
+ if (typePattern.isAny()) {
+ return "***";
+ }
+ return descriptorToJavaType(typePattern.getDescriptor());
+ }
+
+ private static String descriptorToJavaType(String descriptor) {
+ if (descriptor.isEmpty()) {
+ throw new KeepEdgeException("Invalid empty type descriptor");
+ }
+ if (descriptor.length() == 1) {
+ return primitiveDescriptorToJavaType(descriptor.charAt(0));
+ }
+ if (descriptor.charAt(0) == '[') {
+ return arrayDescriptorToJavaType(descriptor);
+ }
+ return classDescriptorToJavaType(descriptor);
+ }
+
+ private static String primitiveDescriptorToJavaType(char descriptor) {
+ switch (descriptor) {
+ case 'Z':
+ return "boolean";
+ case 'B':
+ return "byte";
+ case 'S':
+ return "short";
+ case 'I':
+ return "int";
+ case 'J':
+ return "long";
+ case 'F':
+ return "float";
+ case 'D':
+ return "double";
+ default:
+ throw new KeepEdgeException("Invalid primitive descriptor: " + descriptor);
+ }
+ }
+
+ private static String classDescriptorToJavaType(String descriptor) {
+ int last = descriptor.length() - 1;
+ if (descriptor.charAt(0) != 'L' || descriptor.charAt(last) != ';') {
+ throw new KeepEdgeException("Invalid class descriptor: " + descriptor);
+ }
+ return descriptor.substring(1, last).replace('/', '.');
+ }
+
+ private static String arrayDescriptorToJavaType(String descriptor) {
+ for (int i = 0; i < descriptor.length(); i++) {
+ char c = descriptor.charAt(i);
+ if (c != '[') {
+ StringBuilder builder = new StringBuilder();
+ builder.append(descriptorToJavaType(descriptor.substring(i)));
+ for (int j = 0; j < i; j++) {
+ builder.append("[]");
+ }
+ return builder.toString();
+ }
+ }
+ throw new KeepEdgeException("Invalid array descriptor: " + descriptor);
+ }
+
private static class ItemRule {
private final KeepTarget target;
private final KeepOptions options;
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionAnnotationTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionAnnotationTest.java
index 9bee408..f9d98e3 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionAnnotationTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionAnnotationTest.java
@@ -3,6 +3,7 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.keepanno;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
import static org.hamcrest.MatcherAssert.assertThat;
@@ -53,13 +54,14 @@
.addKeepRules(rules)
.addKeepMainRule(TestClass.class)
.setMinApi(parameters.getApiLevel())
+ .allowUnusedProguardConfigurationRules()
.run(parameters.getRuntime(), TestClass.class)
.assertSuccessWithOutput(EXPECTED)
.inspect(this::checkOutput);
}
public List<Class<?>> getInputClasses() {
- return ImmutableList.of(TestClass.class, A.class, B.class);
+ return ImmutableList.of(TestClass.class, A.class, B.class, C.class);
}
public List<byte[]> getInputClassesWithoutAnnotations() throws Exception {
@@ -83,7 +85,9 @@
private void checkOutput(CodeInspector inspector) {
assertThat(inspector.clazz(A.class), isPresent());
assertThat(inspector.clazz(B.class), isPresent());
- assertThat(inspector.clazz(B.class).uniqueMethodWithOriginalName("bar"), isPresent());
+ assertThat(inspector.clazz(C.class), isAbsent());
+ assertThat(inspector.clazz(B.class).method("void", "bar"), isPresent());
+ assertThat(inspector.clazz(B.class).method("void", "bar", "int"), isAbsent());
}
static class A {
@@ -94,18 +98,36 @@
// Ensure that the class B remains as we are looking it up by reflected name.
@KeepTarget(classConstant = B.class),
// Ensure the method 'bar' remains as we are invoking it by reflected name.
- @KeepTarget(classConstant = B.class, methodName = "bar")
+ @KeepTarget(
+ classConstant = B.class,
+ methodName = "bar",
+ methodParameters = {},
+ methodReturnType = "void")
})
public void foo() throws Exception {
Class<?> clazz = Class.forName(A.class.getTypeName().replace("$A", "$B"));
clazz.getDeclaredMethod("bar").invoke(clazz);
}
+
+ // This annotation is not active as its implicit precondition "void A.foo(int)" is not used.
+ @UsesReflection({@KeepTarget(classConstant = C.class)})
+ public void foo(int unused) {
+ // Unused.
+ }
}
static class B {
public static void bar() {
System.out.println("Hello, world");
}
+
+ public static void bar(int ignore) {
+ throw new RuntimeException("UNUSED");
+ }
+ }
+
+ static class C {
+ // Unused.
}
static class TestClass {