Add validation for publicAlternatives
Validate all the {@link ...} tags in the publicAlternatives field of
@UnsupportedAppUsage annotations, by cross-referencing them against the
current public API.
Test: atest class2greylisttest
Bug: 130721457
Change-Id: Ic3984687cb7ce323d431975767f9ba6b4ef7b49b
diff --git a/tools/class2greylist/Android.bp b/tools/class2greylist/Android.bp
index 371419e..f54aee7 100644
--- a/tools/class2greylist/Android.bp
+++ b/tools/class2greylist/Android.bp
@@ -21,6 +21,8 @@
"commons-cli-1.2",
"apache-bcel",
"guava",
+ "testng",
+ "hamcrest-library",
],
}
diff --git a/tools/class2greylist/src/com/android/class2greylist/AlternativeNotFoundError.java b/tools/class2greylist/src/com/android/class2greylist/AlternativeNotFoundError.java
new file mode 100644
index 0000000..10b2d9a
--- /dev/null
+++ b/tools/class2greylist/src/com/android/class2greylist/AlternativeNotFoundError.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.class2greylist;
+
+public class AlternativeNotFoundError extends Exception {
+}
diff --git a/tools/class2greylist/src/com/android/class2greylist/AnnotationContext.java b/tools/class2greylist/src/com/android/class2greylist/AnnotationContext.java
index 73b74a9..f0f7446 100644
--- a/tools/class2greylist/src/com/android/class2greylist/AnnotationContext.java
+++ b/tools/class2greylist/src/com/android/class2greylist/AnnotationContext.java
@@ -20,7 +20,7 @@
/**
*/
-public abstract class AnnotationContext {
+public abstract class AnnotationContext implements ErrorReporter {
public final Status status;
public final JavaClass definingClass;
@@ -42,10 +42,4 @@
* the greylist.
*/
public abstract String getMemberDescriptor();
-
- /**
- * Report an error in this context. The final error message will include
- * the class and member names, and the source file name.
- */
- public abstract void reportError(String message, Object... args);
}
diff --git a/tools/class2greylist/src/com/android/class2greylist/ApiComponents.java b/tools/class2greylist/src/com/android/class2greylist/ApiComponents.java
new file mode 100644
index 0000000..3da4fe8
--- /dev/null
+++ b/tools/class2greylist/src/com/android/class2greylist/ApiComponents.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.class2greylist;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Class which can parse either dex style signatures (e.g. Lfoo/bar/baz$bat;->foo()V) or javadoc
+ * links to class members (e.g. {@link #toString()} or {@link java.util.List#clear()}).
+ */
+public class ApiComponents {
+ private static final String PRIMITIVE_TYPES = "ZBCSIJFD";
+ private final PackageAndClassName mPackageAndClassName;
+ // The reference can be just to a class, in which case mMemberName should be empty.
+ private final String mMemberName;
+ // If the member being referenced is a field, this will always be empty.
+ private final String mMethodParameterTypes;
+
+ private ApiComponents(PackageAndClassName packageAndClassName, String memberName,
+ String methodParameterTypes) {
+ mPackageAndClassName = packageAndClassName;
+ mMemberName = memberName;
+ mMethodParameterTypes = methodParameterTypes;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder()
+ .append(mPackageAndClassName.packageName)
+ .append(".")
+ .append(mPackageAndClassName.className);
+ if (!mMemberName.isEmpty()) {
+ sb.append("#").append(mMemberName).append("(").append(mMethodParameterTypes).append(
+ ")");
+ }
+ return sb.toString();
+ }
+
+ public PackageAndClassName getPackageAndClassName() {
+ return mPackageAndClassName;
+ }
+
+ public String getMemberName() {
+ return mMemberName;
+ }
+
+ public String getMethodParameterTypes() {
+ return mMethodParameterTypes;
+ }
+
+ /**
+ * Parse a JNI class descriptor. e.g. Lfoo/bar/Baz;
+ *
+ * @param sc Cursor over string assumed to contain a JNI class descriptor.
+ * @return The fully qualified class, in 'dot notation' (e.g. foo.bar.Baz for a class named Baz
+ * in the foo.bar package). The cursor will be placed after the semicolon.
+ */
+ private static String parseJNIClassDescriptor(StringCursor sc)
+ throws SignatureSyntaxError, StringCursorOutOfBoundsException {
+ if (sc.peek() != 'L') {
+ throw new SignatureSyntaxError(
+ "Expected JNI class descriptor to start with L, but instead got " + sc.peek(),
+ sc);
+ }
+ // Consume the L.
+ sc.next();
+ int semiColonPos = sc.find(';');
+ if (semiColonPos == -1) {
+ throw new SignatureSyntaxError("Expected semicolon at the end of JNI class descriptor",
+ sc);
+ }
+ String jniClassDescriptor = sc.next(semiColonPos);
+ // Consume the semicolon.
+ sc.next();
+ return jniClassDescriptor.replace("/", ".");
+ }
+
+ /**
+ * Parse a primitive JNI type
+ *
+ * @param sc Cursor over a string assumed to contain a primitive JNI type.
+ * @return String containing parsed primitive JNI type.
+ */
+ private static String parseJNIPrimitiveType(StringCursor sc)
+ throws SignatureSyntaxError, StringCursorOutOfBoundsException {
+ char c = sc.next();
+ switch (c) {
+ case 'Z':
+ return "boolean";
+ case 'B':
+ return "byte";
+ case 'C':
+ return "char";
+ case 'S':
+ return "short";
+ case 'I':
+ return "int";
+ case 'J':
+ return "long";
+ case 'F':
+ return "float";
+ case 'D':
+ return "double";
+ default:
+ throw new SignatureSyntaxError(c + " is not a primitive type!", sc);
+ }
+ }
+
+ /**
+ * Parse a JNI type; can be either a primitive or object type. Arrays are handled separately.
+ *
+ * @param sc Cursor over the string assumed to contain a JNI type.
+ * @return String containing parsed JNI type.
+ */
+ private static String parseJniTypeWithoutArrayDimensions(StringCursor sc)
+ throws SignatureSyntaxError, StringCursorOutOfBoundsException {
+ char c = sc.peek();
+ if (PRIMITIVE_TYPES.indexOf(c) != -1) {
+ return parseJNIPrimitiveType(sc);
+ } else if (c == 'L') {
+ return parseJNIClassDescriptor(sc);
+ }
+ throw new SignatureSyntaxError("Illegal token " + c + " within signature", sc);
+ }
+
+ /**
+ * Parse a JNI type.
+ *
+ * This parameter can be an array, in which case it will be preceded by a number of open square
+ * brackets (corresponding to its dimensionality)
+ *
+ * @param sc Cursor over the string assumed to contain a JNI type.
+ * @return Same as {@link #parseJniTypeWithoutArrayDimensions}, but also handle arrays.
+ */
+ private static String parseJniType(StringCursor sc)
+ throws SignatureSyntaxError, StringCursorOutOfBoundsException {
+ int arrayDimension = 0;
+ while (sc.peek() == '[') {
+ ++arrayDimension;
+ sc.next();
+ }
+ StringBuilder sb = new StringBuilder();
+ sb.append(parseJniTypeWithoutArrayDimensions(sc));
+ for (int i = 0; i < arrayDimension; ++i) {
+ sb.append("[]");
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Converts the parameters of method from JNI notation to Javadoc link notation. e.g.
+ * "(IILfoo/bar/Baz;)V" turns into "int, int, foo.bar.Baz". The parentheses and return type are
+ * discarded.
+ *
+ * @param sc Cursor over the string assumed to contain a JNI method parameters.
+ * @return Comma separated list of parameter types.
+ */
+ private static String convertJNIMethodParametersToJavadoc(StringCursor sc)
+ throws SignatureSyntaxError, StringCursorOutOfBoundsException {
+ List<String> methodParameterTypes = new ArrayList<>();
+ if (sc.next() != '(') {
+ throw new IllegalArgumentException("Trying to parse method params of an invalid dex " +
+ "signature: " + sc.getOriginalString());
+ }
+ while (sc.peek() != ')') {
+ methodParameterTypes.add(parseJniType(sc));
+ }
+ return String.join(", ", methodParameterTypes);
+ }
+
+ /**
+ * Generate ApiComponents from a dex signature.
+ *
+ * This is used to extract the necessary context for an alternative API to try to infer missing
+ * information.
+ *
+ * @param signature Dex signature.
+ * @return ApiComponents instance with populated package, class name, and parameter types if
+ * applicable.
+ */
+ public static ApiComponents fromDexSignature(String signature) throws SignatureSyntaxError {
+ StringCursor sc = new StringCursor(signature);
+ try {
+ String fullyQualifiedClass = parseJNIClassDescriptor(sc);
+
+ PackageAndClassName packageAndClassName =
+ PackageAndClassName.splitClassName(fullyQualifiedClass);
+ if (!sc.peek(2).equals("->")) {
+ throw new SignatureSyntaxError("Expected '->'", sc);
+ }
+ // Consume "->"
+ sc.next(2);
+ String memberName = "";
+ String methodParameterTypes = "";
+ int leftParenPos = sc.find('(');
+ if (leftParenPos != -1) {
+ memberName = sc.next(leftParenPos);
+ methodParameterTypes = convertJNIMethodParametersToJavadoc(sc);
+ } else {
+ int colonPos = sc.find(':');
+ if (colonPos == -1) {
+ throw new IllegalArgumentException("Expected : or -> beyond position "
+ + sc.position() + " in " + signature);
+ } else {
+ memberName = sc.next(colonPos);
+ // Consume the ':'.
+ sc.next();
+ // Consume the type.
+ parseJniType(sc);
+ }
+ }
+ return new ApiComponents(packageAndClassName, memberName, methodParameterTypes);
+ } catch (StringCursorOutOfBoundsException e) {
+ throw new SignatureSyntaxError(
+ "Unexpectedly reached end of string while trying to parse signature ", sc);
+ }
+ }
+
+ /**
+ * Generate ApiComponents from a link tag.
+ *
+ * @param linkTag The contents of a link tag.
+ * @param contextSignature The signature of the private API that this is an alternative for.
+ * Used to infer unspecified components.
+ */
+ public static ApiComponents fromLinkTag(String linkTag, String contextSignature)
+ throws JavadocLinkSyntaxError {
+ ApiComponents contextAlternative;
+ try {
+ contextAlternative = fromDexSignature(contextSignature);
+ } catch (SignatureSyntaxError e) {
+ throw new RuntimeException(
+ "Failed to parse the context signature for public alternative!");
+ }
+ StringCursor sc = new StringCursor(linkTag);
+ try {
+
+ String memberName = "";
+ String methodParameterTypes = "";
+
+ int tagPos = sc.find('#');
+ String fullyQualifiedClassName = sc.next(tagPos);
+
+ PackageAndClassName packageAndClassName =
+ PackageAndClassName.splitClassName(fullyQualifiedClassName);
+
+ if (packageAndClassName.packageName.isEmpty()) {
+ packageAndClassName.packageName = contextAlternative.getPackageAndClassName()
+ .packageName;
+ }
+
+ if (packageAndClassName.className.isEmpty()) {
+ packageAndClassName.className = contextAlternative.getPackageAndClassName()
+ .className;
+ }
+
+ if (tagPos == -1) {
+ // This suggested alternative is just a class. We can allow that.
+ return new ApiComponents(packageAndClassName, "", "");
+ } else {
+ // Consume the #.
+ sc.next();
+ }
+
+ int leftParenPos = sc.find('(');
+ memberName = sc.next(leftParenPos);
+ if (leftParenPos != -1) {
+ // Consume the '('.
+ sc.next();
+ int rightParenPos = sc.find(')');
+ if (rightParenPos == -1) {
+ throw new JavadocLinkSyntaxError(
+ "Linked method is missing a closing parenthesis", sc);
+ } else {
+ methodParameterTypes = sc.next(rightParenPos);
+ }
+ }
+
+ return new ApiComponents(packageAndClassName, memberName, methodParameterTypes);
+ } catch (StringCursorOutOfBoundsException e) {
+ throw new JavadocLinkSyntaxError(
+ "Unexpectedly reached end of string while trying to parse javadoc link", sc);
+ }
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof ApiComponents)) {
+ return false;
+ }
+ ApiComponents other = (ApiComponents) obj;
+ return mPackageAndClassName.equals(other.mPackageAndClassName) && mMemberName.equals(
+ other.mMemberName) && mMethodParameterTypes.equals(other.mMethodParameterTypes);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mPackageAndClassName, mMemberName, mMethodParameterTypes);
+ }
+
+ /**
+ * Less restrictive comparator to use in case a link tag is missing a method's parameters.
+ * e.g. foo.bar.Baz#foo will be considered the same as foo.bar.Baz#foo(int, int) and
+ * foo.bar.Baz#foo(long, long). If the class only has one method with that name, then specifying
+ * its parameter types is optional within the link tag.
+ */
+ public boolean equalsIgnoringParam(ApiComponents other) {
+ return mPackageAndClassName.equals(other.mPackageAndClassName) &&
+ mMemberName.equals(other.mMemberName);
+ }
+}
diff --git a/tools/class2greylist/src/com/android/class2greylist/ApiResolver.java b/tools/class2greylist/src/com/android/class2greylist/ApiResolver.java
new file mode 100644
index 0000000..f07a0af
--- /dev/null
+++ b/tools/class2greylist/src/com/android/class2greylist/ApiResolver.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.class2greylist;
+
+import com.google.common.base.Strings;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+public class ApiResolver {
+ private final List<ApiComponents> mPotentialPublicAlternatives;
+ private final Set<PackageAndClassName> mPublicApiClasses;
+
+ private static final Pattern LINK_TAG_PATTERN = Pattern.compile("\\{@link ([^\\}]+)\\}");
+ private static final Pattern CODE_TAG_PATTERN = Pattern.compile("\\{@code ([^\\}]+)\\}");
+
+ public ApiResolver() {
+ mPotentialPublicAlternatives = null;
+ mPublicApiClasses = null;
+ }
+
+ public ApiResolver(Set<String> publicApis) {
+ mPotentialPublicAlternatives = publicApis.stream()
+ .map(api -> {
+ try {
+ return ApiComponents.fromDexSignature(api);
+ } catch (SignatureSyntaxError e) {
+ throw new RuntimeException("Could not parse public API signature:", e);
+ }
+ })
+ .collect(Collectors.toList());
+ mPublicApiClasses = mPotentialPublicAlternatives.stream()
+ .map(api -> api.getPackageAndClassName())
+ .collect(Collectors.toCollection(HashSet::new));
+ }
+
+ /**
+ * Verify that all public alternatives are valid.
+ *
+ * @param publicAlternativesString String containing public alternative explanations.
+ * @param signature Signature of the member that has the annotation.
+ */
+ public void resolvePublicAlternatives(String publicAlternativesString, String signature)
+ throws JavadocLinkSyntaxError, AlternativeNotFoundError {
+ if (publicAlternativesString != null && mPotentialPublicAlternatives != null) {
+ // Grab all instances of type {@link foo}
+ Matcher matcher = LINK_TAG_PATTERN.matcher(publicAlternativesString);
+ boolean hasLinkAlternative = false;
+ // Validate all link tags
+ while (matcher.find()) {
+ hasLinkAlternative = true;
+ String alternativeString = matcher.group(1);
+ ApiComponents alternative = ApiComponents.fromLinkTag(alternativeString,
+ signature);
+ if (alternative.getMemberName().isEmpty()) {
+ // Provided class as alternative
+ if (!mPublicApiClasses.contains(alternative.getPackageAndClassName())) {
+ throw new ClassAlternativeNotFoundError(alternative);
+ }
+ } else if (!mPotentialPublicAlternatives.contains(alternative)) {
+ // If the link is not a public alternative, it must because the link does not
+ // contain the method parameter types, e.g. {@link foo.bar.Baz#foo} instead of
+ // {@link foo.bar.Baz#foo(int)}. If the method name is unique within the class,
+ // we can handle it.
+ if (!Strings.isNullOrEmpty(alternative.getMethodParameterTypes())) {
+ throw new MemberAlternativeNotFoundError(alternative);
+ }
+ List<ApiComponents> almostMatches = mPotentialPublicAlternatives.stream()
+ .filter(api -> api.equalsIgnoringParam(alternative))
+ .collect(Collectors.toList());
+ if (almostMatches.size() == 0) {
+ throw new MemberAlternativeNotFoundError(alternative);
+ } else if (almostMatches.size() > 1) {
+ throw new MultipleAlternativesFoundError(alternative, almostMatches);
+ }
+ }
+ }
+ // No {@link ...} alternatives exist; try looking for {@code ...}
+ if (!hasLinkAlternative) {
+ if (!CODE_TAG_PATTERN.matcher(publicAlternativesString).find()) {
+ throw new NoAlternativesSpecifiedError();
+ }
+ }
+ }
+ }
+}
diff --git a/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java b/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java
index 49b8f81..bfa8076 100644
--- a/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java
+++ b/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java
@@ -21,7 +21,6 @@
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Sets;
import com.google.common.io.Files;
import org.apache.commons.cli.CommandLine;
@@ -35,7 +34,6 @@
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
-import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
diff --git a/tools/class2greylist/src/com/android/class2greylist/ClassAlternativeNotFoundError.java b/tools/class2greylist/src/com/android/class2greylist/ClassAlternativeNotFoundError.java
new file mode 100644
index 0000000..1f398f1
--- /dev/null
+++ b/tools/class2greylist/src/com/android/class2greylist/ClassAlternativeNotFoundError.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.class2greylist;
+
+public class ClassAlternativeNotFoundError extends AlternativeNotFoundError {
+ public final ApiComponents alternative;
+
+ ClassAlternativeNotFoundError(ApiComponents alternative) {
+ this.alternative = alternative;
+ }
+
+ @Override
+ public String toString() {
+ return "Specified class " + alternative.getPackageAndClassName() + " does not exist!";
+ }
+}
diff --git a/tools/class2greylist/src/com/android/class2greylist/ErrorReporter.java b/tools/class2greylist/src/com/android/class2greylist/ErrorReporter.java
new file mode 100644
index 0000000..24a92f0
--- /dev/null
+++ b/tools/class2greylist/src/com/android/class2greylist/ErrorReporter.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.class2greylist;
+
+public interface ErrorReporter {
+ /**
+ * Report an error in this context. The final error message will include
+ * the class and member names, and the source file name.
+ */
+ void reportError(String message, Object... args);
+}
diff --git a/tools/class2greylist/src/com/android/class2greylist/JavadocLinkSyntaxError.java b/tools/class2greylist/src/com/android/class2greylist/JavadocLinkSyntaxError.java
new file mode 100644
index 0000000..55014cb
--- /dev/null
+++ b/tools/class2greylist/src/com/android/class2greylist/JavadocLinkSyntaxError.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.class2greylist;
+
+public class JavadocLinkSyntaxError extends Exception {
+ public final String expected;
+ public final int position;
+ public final String context;
+
+ public JavadocLinkSyntaxError(String expected, StringCursor sc) {
+ super(expected + " at position " + sc.position() + " in " + sc.getOriginalString());
+ this.expected = expected;
+ this.position = sc.position();
+ this.context = sc.getOriginalString();
+ }
+}
+
diff --git a/tools/class2greylist/src/com/android/class2greylist/MemberAlternativeNotFoundError.java b/tools/class2greylist/src/com/android/class2greylist/MemberAlternativeNotFoundError.java
new file mode 100644
index 0000000..43f853e
--- /dev/null
+++ b/tools/class2greylist/src/com/android/class2greylist/MemberAlternativeNotFoundError.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.class2greylist;
+
+public class MemberAlternativeNotFoundError extends AlternativeNotFoundError {
+ public final ApiComponents alternative;
+
+ MemberAlternativeNotFoundError(ApiComponents alternative) {
+ this.alternative = alternative;
+ }
+
+ @Override
+ public String toString() {
+ return "Could not find public api " + alternative + ".";
+ }
+}
diff --git a/tools/class2greylist/src/com/android/class2greylist/MultipleAlternativesFoundError.java b/tools/class2greylist/src/com/android/class2greylist/MultipleAlternativesFoundError.java
new file mode 100644
index 0000000..a598534
--- /dev/null
+++ b/tools/class2greylist/src/com/android/class2greylist/MultipleAlternativesFoundError.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.class2greylist;
+
+import com.google.common.base.Joiner;
+
+import java.util.List;
+
+public class MultipleAlternativesFoundError extends AlternativeNotFoundError {
+ public final ApiComponents alternative;
+ public final List<ApiComponents> almostMatches;
+
+ public MultipleAlternativesFoundError(ApiComponents alternative,
+ List<ApiComponents> almostMatches) {
+ this.alternative = alternative;
+ this.almostMatches = almostMatches;
+ }
+
+ @Override
+ public String toString() {
+ return "Alternative " + alternative + " returned multiple matches: "
+ + Joiner.on(", ").join(almostMatches);
+ }
+}
diff --git a/tools/class2greylist/src/com/android/class2greylist/NoAlternativesSpecifiedError.java b/tools/class2greylist/src/com/android/class2greylist/NoAlternativesSpecifiedError.java
new file mode 100644
index 0000000..28c5003
--- /dev/null
+++ b/tools/class2greylist/src/com/android/class2greylist/NoAlternativesSpecifiedError.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.class2greylist;
+
+public class NoAlternativesSpecifiedError extends AlternativeNotFoundError {
+
+ @Override
+ public String toString() {
+ return "Hidden API has a public alternative annotation field, but no concrete "
+ + "explanations. Please provide either a reference to an SDK method using javadoc "
+ + "syntax, e.g. {@link foo.bar.Baz#bat}, or a small code snippet if the "
+ + "alternative is part of a support library or third party library, e.g. "
+ + "{@code foo.bar.Baz bat = new foo.bar.Baz(); bat.doSomething();}.\n"
+ + "If this is too restrictive for your use case, please contact compat-team@.";
+ }
+}
diff --git a/tools/class2greylist/src/com/android/class2greylist/PackageAndClassName.java b/tools/class2greylist/src/com/android/class2greylist/PackageAndClassName.java
new file mode 100644
index 0000000..709092d
--- /dev/null
+++ b/tools/class2greylist/src/com/android/class2greylist/PackageAndClassName.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.class2greylist;
+
+import java.util.Objects;
+
+class PackageAndClassName{
+ public String packageName;
+ public String className;
+
+ private PackageAndClassName(String packageName, String className) {
+ this.packageName = packageName;
+ this.className = className;
+ }
+
+ /**
+ * Given a potentially fully qualified class name, split it into package and class.
+ *
+ * @param fullyQualifiedClassName potentially fully qualified class name.
+ * @return A pair of strings, containing the package name (or empty if not specified) and
+ * the
+ * class name (or empty if string is empty).
+ */
+ public static PackageAndClassName splitClassName(String fullyQualifiedClassName) {
+ int lastDotIdx = fullyQualifiedClassName.lastIndexOf('.');
+ if (lastDotIdx == -1) {
+ return new PackageAndClassName("", fullyQualifiedClassName);
+ }
+ String packageName = fullyQualifiedClassName.substring(0, lastDotIdx);
+ String className = fullyQualifiedClassName.substring(lastDotIdx + 1);
+ return new PackageAndClassName(packageName, className);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof PackageAndClassName)) {
+ return false;
+ }
+ PackageAndClassName other = (PackageAndClassName) obj;
+ return Objects.equals(packageName, other.packageName) && Objects.equals(className,
+ other.className);
+ }
+
+ @Override
+ public String toString() {
+ return packageName + "." + className;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(packageName, className);
+ }
+}
diff --git a/tools/class2greylist/src/com/android/class2greylist/SignatureSyntaxError.java b/tools/class2greylist/src/com/android/class2greylist/SignatureSyntaxError.java
new file mode 100644
index 0000000..7685caa
--- /dev/null
+++ b/tools/class2greylist/src/com/android/class2greylist/SignatureSyntaxError.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.class2greylist;
+
+public class SignatureSyntaxError extends Exception {
+ public final String expected;
+ public final int position;
+ public final String context;
+ public SignatureSyntaxError(String expected, StringCursor sc) {
+ super(expected + " at position " + sc.position() + " in " + sc.getOriginalString());
+ this.expected = expected;
+ this.position = sc.position();
+ this.context = sc.getOriginalString();
+ }
+}
diff --git a/tools/class2greylist/src/com/android/class2greylist/StringCursor.java b/tools/class2greylist/src/com/android/class2greylist/StringCursor.java
new file mode 100644
index 0000000..08e8521
--- /dev/null
+++ b/tools/class2greylist/src/com/android/class2greylist/StringCursor.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.class2greylist;
+
+/**
+ * Utility class to simplify parsing of signatures.
+ */
+public class StringCursor {
+
+ private final String mString;
+ private int mCursor;
+
+ public StringCursor(String str) {
+ mString = str;
+ mCursor = 0;
+ }
+
+ /**
+ * Position of cursor in string.
+ *
+ * @return Current position of cursor in string.
+ */
+ public int position() {
+ return mCursor;
+ }
+
+ /**
+ * Peek current cursor position.
+ *
+ * @return The character at the current cursor position.
+ */
+ public char peek() {
+ return mString.charAt(mCursor);
+ }
+
+ /**
+ * Peek several characters at the current cursor position without moving the cursor.
+ *
+ * @param n The number of characters to peek.
+ * @return A string with x characters from the cursor position. If n is -1, return the whole
+ * rest of the string.
+ */
+ public String peek(int n) throws StringCursorOutOfBoundsException {
+ if (n == -1) {
+ return mString.substring(mCursor);
+ }
+ if (n < 0 || (n + mCursor) >= mString.length()) {
+ throw new StringCursorOutOfBoundsException();
+ }
+ return mString.substring(mCursor, mCursor + n);
+ }
+
+ /**
+ * Consume the character at the current cursor position and move the cursor forwards.
+ *
+ * @return The character at the current cursor position.
+ */
+ public char next() throws StringCursorOutOfBoundsException {
+ if (!hasNext()) {
+ throw new StringCursorOutOfBoundsException();
+ }
+ return mString.charAt(mCursor++);
+ }
+
+ /**
+ * Consume several characters at the current cursor position and move the cursor further along.
+ *
+ * @param n The number of characters to consume.
+ * @return A string with x characters from the cursor position. If n is -1, return the whole
+ * rest of the string.
+ */
+ public String next(int n) throws StringCursorOutOfBoundsException {
+ if (n == -1) {
+ String restOfString = mString.substring(mCursor);
+ mCursor = mString.length();
+ return restOfString;
+ }
+ if (n < 0) {
+ throw new StringCursorOutOfBoundsException();
+ }
+ mCursor += n;
+ return mString.substring(mCursor - n, mCursor);
+ }
+
+ /**
+ * Search for the first occurrence of a character beyond the current cursor position.
+ *
+ * @param c The character to search for.
+ * @return The offset of the first occurrence of c in the string beyond the cursor position.
+ * If the character does not exist, return -1.
+ */
+ public int find(char c) {
+ int firstIndex = mString.indexOf(c, mCursor);
+ if (firstIndex == -1) {
+ return -1;
+ }
+ return firstIndex - mCursor;
+ }
+
+ /**
+ * Check if cursor has reached end of string.
+ *
+ * @return Cursor has reached end of string.
+ */
+ public boolean hasNext() {
+ return mCursor < mString.length();
+ }
+
+ @Override
+ public String toString() {
+ return mString.substring(mCursor);
+ }
+
+ public String getOriginalString() {
+ return mString;
+ }
+}
\ No newline at end of file
diff --git a/tools/class2greylist/src/com/android/class2greylist/StringCursorOutOfBoundsException.java b/tools/class2greylist/src/com/android/class2greylist/StringCursorOutOfBoundsException.java
new file mode 100644
index 0000000..caf0bd6
--- /dev/null
+++ b/tools/class2greylist/src/com/android/class2greylist/StringCursorOutOfBoundsException.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.class2greylist;
+
+public class StringCursorOutOfBoundsException extends IndexOutOfBoundsException {
+
+}
diff --git a/tools/class2greylist/src/com/android/class2greylist/UnsupportedAppUsageAnnotationHandler.java b/tools/class2greylist/src/com/android/class2greylist/UnsupportedAppUsageAnnotationHandler.java
index b45e1b3..17ec732 100644
--- a/tools/class2greylist/src/com/android/class2greylist/UnsupportedAppUsageAnnotationHandler.java
+++ b/tools/class2greylist/src/com/android/class2greylist/UnsupportedAppUsageAnnotationHandler.java
@@ -32,12 +32,15 @@
private static final String EXPECTED_SIGNATURE_PROPERTY = "expectedSignature";
private static final String MAX_TARGET_SDK_PROPERTY = "maxTargetSdk";
private static final String IMPLICIT_MEMBER_PROPERTY = "implicitMember";
+ private static final String PUBLIC_ALTERNATIVES_PROPERTY = "publicAlternatives";
private final Status mStatus;
private final Predicate<ClassMember> mClassMemberFilter;
private final Map<Integer, String> mSdkVersionToFlagMap;
private final AnnotationConsumer mAnnotationConsumer;
+ private ApiResolver mApiResolver;
+
/**
* Represents a member of a class file (a field or method).
*/
@@ -66,6 +69,7 @@
this(status, annotationConsumer,
member -> !(member.isBridgeMethod && publicApis.contains(member.signature)),
sdkVersionToFlagMap);
+ mApiResolver = new ApiResolver(publicApis);
}
@VisibleForTesting
@@ -76,6 +80,7 @@
mAnnotationConsumer = annotationConsumer;
mClassMemberFilter = memberFilter;
mSdkVersionToFlagMap = sdkVersionToFlagMap;
+ mApiResolver = new ApiResolver();
}
@Override
@@ -85,7 +90,7 @@
AnnotatedMemberContext memberContext = (AnnotatedMemberContext) context;
FieldOrMethod member = memberContext.member;
isBridgeMethod = (member instanceof Method) &&
- (member.getAccessFlags() & Const.ACC_BRIDGE) != 0;
+ (member.getAccessFlags() & Const.ACC_BRIDGE) != 0;
if (isBridgeMethod) {
mStatus.debug("Member is a bridge method");
}
@@ -94,6 +99,7 @@
String signature = context.getMemberDescriptor();
Integer maxTargetSdk = null;
String implicitMemberSignature = null;
+ String publicAlternativesString = null;
for (ElementValuePair property : annotation.getElementValuePairs()) {
switch (property.getNameString()) {
@@ -102,8 +108,8 @@
// Don't enforce for bridge methods; they're generated so won't match.
if (!isBridgeMethod && !signature.equals(expected)) {
context.reportError("Expected signature does not match generated:\n"
- + "Expected: %s\n"
- + "Generated: %s", expected, signature);
+ + "Expected: %s\n"
+ + "Generated: %s", expected, signature);
return;
}
break;
@@ -121,23 +127,27 @@
implicitMemberSignature = property.getValue().stringifyValue();
if (context instanceof AnnotatedClassContext) {
signature = String.format("L%s;->%s",
- context.getClassDescriptor(), implicitMemberSignature);
+ context.getClassDescriptor(), implicitMemberSignature);
} else {
context.reportError(
- "Expected annotation with an %s property to be on a class but is on %s",
- IMPLICIT_MEMBER_PROPERTY,
- signature);
+ "Expected annotation with an %s property to be on a class but is "
+ + "on %s",
+ IMPLICIT_MEMBER_PROPERTY,
+ signature);
return;
}
break;
+ case PUBLIC_ALTERNATIVES_PROPERTY:
+ publicAlternativesString = property.getValue().stringifyValue();
+ break;
}
}
if (context instanceof AnnotatedClassContext && implicitMemberSignature == null) {
context.reportError(
- "Missing property %s on annotation on class %s",
- IMPLICIT_MEMBER_PROPERTY,
- signature);
+ "Missing property %s on annotation on class %s",
+ IMPLICIT_MEMBER_PROPERTY,
+ signature);
return;
}
@@ -149,6 +159,11 @@
mSdkVersionToFlagMap.keySet());
return;
}
+ try {
+ mApiResolver.resolvePublicAlternatives(publicAlternativesString, signature);
+ } catch (JavadocLinkSyntaxError | AlternativeNotFoundError e) {
+ context.reportError(e.toString());
+ }
// Consume this annotation if it matches the predicate.
if (mClassMemberFilter.test(new ClassMember(signature, isBridgeMethod))) {
diff --git a/tools/class2greylist/test/src/com/android/class2greylist/ApiComponentsTest.java b/tools/class2greylist/test/src/com/android/class2greylist/ApiComponentsTest.java
new file mode 100644
index 0000000..e93d1e1
--- /dev/null
+++ b/tools/class2greylist/test/src/com/android/class2greylist/ApiComponentsTest.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.class2greylist;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import org.junit.Test;
+
+
+public class ApiComponentsTest extends AnnotationHandlerTestBase {
+
+ @Test
+ public void testGetApiComponentsPackageFromSignature() throws SignatureSyntaxError {
+ ApiComponents api = ApiComponents.fromDexSignature("La/b/C;->foo()V");
+ PackageAndClassName packageAndClassName = api.getPackageAndClassName();
+ assertThat(packageAndClassName.packageName).isEqualTo("a.b");
+ }
+
+ @Test
+ public void testGetApiComponentsFromSignature() throws SignatureSyntaxError {
+ ApiComponents api = ApiComponents.fromDexSignature("La/b/C;->foo(IJLfoo2/bar/Baz;)V");
+ PackageAndClassName packageAndClassName = api.getPackageAndClassName();
+ assertThat(packageAndClassName.className).isEqualTo("C");
+ assertThat(api.getMemberName()).isEqualTo("foo");
+ assertThat(api.getMethodParameterTypes()).isEqualTo("int, long, foo2.bar.Baz");
+ }
+
+ @Test
+ public void testInvalidDexSignatureInvalidClassFormat() throws SignatureSyntaxError {
+ assertThrows(SignatureSyntaxError.class, () -> {
+ ApiComponents.fromDexSignature("a/b/C;->foo()V");
+ });
+ assertThrows(SignatureSyntaxError.class, () -> {
+ ApiComponents.fromDexSignature("La/b/C->foo()V");
+ });
+ }
+
+ @Test
+ public void testInvalidDexSignatureInvalidParameterType() throws SignatureSyntaxError {
+ assertThrows(SignatureSyntaxError.class, () -> {
+ ApiComponents.fromDexSignature("a/b/C;->foo(foo)V");
+ });
+ }
+
+ @Test
+ public void testInvalidDexSignatureInvalidReturnType() throws SignatureSyntaxError {
+ assertThrows(SignatureSyntaxError.class, () -> {
+ ApiComponents.fromDexSignature("a/b/C;->foo()foo");
+ });
+ }
+
+ @Test
+ public void testInvalidDexSignatureMissingReturnType() throws SignatureSyntaxError {
+ assertThrows(SignatureSyntaxError.class, () -> {
+ ApiComponents.fromDexSignature("a/b/C;->foo(I)");
+ });
+ }
+
+ @Test
+ public void testInvalidDexSignatureMissingArrowOrColon() throws SignatureSyntaxError {
+ assertThrows(SignatureSyntaxError.class, () -> {
+ ApiComponents.fromDexSignature("La/b/C;foo()V");
+ });
+ }
+
+ @Test
+ public void testGetApiComponentsFromFieldLink() throws JavadocLinkSyntaxError {
+ ApiComponents api = ApiComponents.fromLinkTag("a.b.C#foo(int, long, foo2.bar.Baz)",
+ "La/b/C;->foo:I");
+ PackageAndClassName packageAndClassName = api.getPackageAndClassName();
+ assertThat(packageAndClassName.packageName).isEqualTo("a.b");
+ assertThat(packageAndClassName.className).isEqualTo("C");
+ assertThat(api.getMemberName()).isEqualTo("foo");
+ }
+
+ @Test
+ public void testGetApiComponentsLinkOnlyClass() throws JavadocLinkSyntaxError {
+ ApiComponents api = ApiComponents.fromLinkTag("b.c.D", "La/b/C;->foo:I");
+ PackageAndClassName packageAndClassName = api.getPackageAndClassName();
+ assertThat(packageAndClassName.packageName).isEqualTo("b.c");
+ assertThat(packageAndClassName.className).isEqualTo("D");
+ assertThat(api.getMethodParameterTypes()).isEqualTo("");
+ }
+
+ @Test
+ public void testGetApiComponentsFromLinkOnlyClassDeducePackage() throws JavadocLinkSyntaxError {
+ ApiComponents api = ApiComponents.fromLinkTag("D", "La/b/C;->foo:I");
+ PackageAndClassName packageAndClassName = api.getPackageAndClassName();
+ assertThat(packageAndClassName.packageName).isEqualTo("a.b");
+ assertThat(packageAndClassName.className).isEqualTo("D");
+ assertThat(api.getMemberName().isEmpty()).isTrue();
+ assertThat(api.getMethodParameterTypes().isEmpty()).isTrue();
+ }
+
+ @Test
+ public void testGetApiComponentsParametersFromMethodLink() throws JavadocLinkSyntaxError {
+ ApiComponents api = ApiComponents.fromLinkTag("a.b.C#foo(int, long, foo2.bar.Baz)",
+ "La/b/C;->foo:I");
+ assertThat(api.getMethodParameterTypes()).isEqualTo("int, long, foo2.bar.Baz");
+ }
+
+ @Test
+ public void testDeduceApiComponentsPackageFromLinkUsingContext() throws JavadocLinkSyntaxError {
+ ApiComponents api = ApiComponents.fromLinkTag("C#foo(int, long, foo2.bar.Baz)",
+ "La/b/C;->foo:I");
+ PackageAndClassName packageAndClassName = api.getPackageAndClassName();
+ assertThat(packageAndClassName.packageName).isEqualTo("a.b");
+ }
+
+ @Test
+ public void testDeduceApiComponentsPackageAndClassFromLinkUsingContext()
+ throws JavadocLinkSyntaxError {
+ ApiComponents api = ApiComponents.fromLinkTag("#foo(int, long, foo2.bar.Baz)",
+ "La/b/C;->foo:I");
+ PackageAndClassName packageAndClassName = api.getPackageAndClassName();
+ assertThat(packageAndClassName.packageName).isEqualTo("a.b");
+ assertThat(packageAndClassName.className).isEqualTo("C");
+ }
+
+ @Test
+ public void testInvalidLinkTagUnclosedParenthesis() throws JavadocLinkSyntaxError {
+ assertThrows(JavadocLinkSyntaxError.class, () -> {
+ ApiComponents.fromLinkTag("a.b.C#foo(int,float", "La/b/C;->foo()V");
+ });
+ }
+
+}
\ No newline at end of file
diff --git a/tools/class2greylist/test/src/com/android/class2greylist/ApiResolverTest.java b/tools/class2greylist/test/src/com/android/class2greylist/ApiResolverTest.java
new file mode 100644
index 0000000..8790879
--- /dev/null
+++ b/tools/class2greylist/test/src/com/android/class2greylist/ApiResolverTest.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.class2greylist;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.testng.Assert.expectThrows;
+import static org.testng.Assert.assertThrows;
+import static org.junit.Assert.fail;
+
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeMatcher;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+public class ApiResolverTest extends AnnotationHandlerTestBase {
+ @Test
+ public void testFindPublicAlternativeExactly()
+ throws JavadocLinkSyntaxError, AlternativeNotFoundError {
+ Set<String> publicApis = Collections.unmodifiableSet(new HashSet<String>(
+ Arrays.asList("La/b/C;->foo(I)V", "La/b/C;->bar(I)V")));
+ ApiResolver resolver = new ApiResolver(publicApis);
+ resolver.resolvePublicAlternatives("{@link a.b.C#foo(int)}", "Lb/c/D;->bar()V");
+ }
+
+ @Test
+ public void testFindPublicAlternativeDeducedPackageName()
+ throws JavadocLinkSyntaxError, AlternativeNotFoundError {
+ Set<String> publicApis = Collections.unmodifiableSet(new HashSet<String>(
+ Arrays.asList("La/b/C;->foo(I)V", "La/b/C;->bar(I)V")));
+ ApiResolver resolver = new ApiResolver(publicApis);
+ resolver.resolvePublicAlternatives("{@link C#foo(int)}", "La/b/D;->bar()V");
+ }
+
+ @Test
+ public void testFindPublicAlternativeDeducedPackageAndClassName()
+ throws JavadocLinkSyntaxError, AlternativeNotFoundError {
+ Set<String> publicApis = Collections.unmodifiableSet(new HashSet<String>(
+ Arrays.asList("La/b/C;->foo(I)V", "La/b/C;->bar(I)V")));
+ ApiResolver resolver = new ApiResolver(publicApis);
+ resolver.resolvePublicAlternatives("{@link #foo(int)}", "La/b/C;->bar()V");
+ }
+
+ @Test
+ public void testFindPublicAlternativeDeducedParameterTypes()
+ throws JavadocLinkSyntaxError, AlternativeNotFoundError {
+ Set<String> publicApis = Collections.unmodifiableSet(new HashSet<String>(
+ Arrays.asList("La/b/C;->foo(I)V", "La/b/C;->bar(I)V")));
+ ApiResolver resolver = new ApiResolver(publicApis);
+ resolver.resolvePublicAlternatives("{@link #foo}", "La/b/C;->bar()V");
+ }
+
+ @Test
+ public void testFindPublicAlternativeFailDueToMultipleParameterTypes()
+ throws JavadocLinkSyntaxError, AlternativeNotFoundError, SignatureSyntaxError {
+ Set<String> publicApis = Collections.unmodifiableSet(new HashSet<String>(
+ Arrays.asList("La/b/C;->foo(I)V", "La/b/C;->bar(I)I", "La/b/C;->foo(II)V")));
+ ApiResolver resolver = new ApiResolver(publicApis);
+ MultipleAlternativesFoundError e = expectThrows(MultipleAlternativesFoundError.class,
+ () -> resolver.resolvePublicAlternatives("{@link #foo}", "La/b/C;->bar()V"));
+ assertThat(e.almostMatches).containsExactly(
+ ApiComponents.fromDexSignature("La/b/C;->foo(I)V"),
+ ApiComponents.fromDexSignature("La/b/C;->foo(II)V")
+ );
+ }
+
+ @Test
+ public void testFindPublicAlternativeFailNoAlternative()
+ throws JavadocLinkSyntaxError, AlternativeNotFoundError {
+ Set<String> publicApis = Collections.unmodifiableSet(new HashSet<String>(
+ Arrays.asList("La/b/C;->bar(I)V")));
+ ApiResolver resolver = new ApiResolver(publicApis);
+ assertThrows(MemberAlternativeNotFoundError.class, ()
+ -> resolver.resolvePublicAlternatives("{@link #foo(int)}", "La/b/C;->bar()V"));
+ }
+
+ @Test
+ public void testFindPublicAlternativeFailNoAlternativeNoParameterTypes()
+ throws JavadocLinkSyntaxError, AlternativeNotFoundError {
+
+ Set<String> publicApis = Collections.unmodifiableSet(new HashSet<String>(
+ Arrays.asList("La/b/C;->bar(I)V")));
+ ApiResolver resolver = new ApiResolver(publicApis);
+ assertThrows(MemberAlternativeNotFoundError.class,
+ () -> resolver.resolvePublicAlternatives("{@link #foo}", "La/b/C;->bar()V"));
+ }
+
+ @Test
+ public void testNoPublicClassAlternatives()
+ throws JavadocLinkSyntaxError, AlternativeNotFoundError {
+ Set<String> publicApis = Collections.unmodifiableSet(new HashSet<String>());
+ ApiResolver resolver = new ApiResolver(publicApis);
+ expectThrows(NoAlternativesSpecifiedError.class,
+ () -> resolver.resolvePublicAlternatives("Foo", "La/b/C;->bar()V"));
+ }
+
+ @Test
+ public void testPublicAlternativesJustPackageAndClassName()
+ throws JavadocLinkSyntaxError, AlternativeNotFoundError {
+ Set<String> publicApis = Collections.unmodifiableSet(new HashSet<String>(
+ Arrays.asList("La/b/C;->bar(I)V")));
+ ApiResolver resolver = new ApiResolver(publicApis);
+ resolver.resolvePublicAlternatives("Foo {@link a.b.C}", "Lb/c/D;->bar()V");
+ }
+
+ @Test
+ public void testPublicAlternativesJustClassName()
+ throws JavadocLinkSyntaxError, AlternativeNotFoundError {
+ Set<String> publicApis = Collections.unmodifiableSet(new HashSet<String>(
+ Arrays.asList("La/b/C;->bar(I)V")));
+ ApiResolver resolver = new ApiResolver(publicApis);
+ resolver.resolvePublicAlternatives("Foo {@link C}", "La/b/D;->bar()V");
+ }
+
+ @Test
+ public void testNoPublicAlternativesButHasExplanation()
+ throws JavadocLinkSyntaxError, AlternativeNotFoundError {
+ Set<String> publicApis = Collections.unmodifiableSet(new HashSet<String>());
+ ApiResolver resolver = new ApiResolver(publicApis);
+ resolver.resolvePublicAlternatives("Foo {@code bar}", "La/b/C;->bar()V");
+ }
+
+}
\ No newline at end of file