111396009: Metalava stubs are missing default annotation method values
Test: Unit tests included
Fixes: 111396009
Fixes: 110532131
Merged-In: I6b665abd8dbd6faef7311d7fdbe073a70376b7ce
Change-Id: Ia4c01b0b5ee75953c75354f29e72ea2f31e020f1
diff --git a/src/main/java/com/android/tools/metalava/Compatibility.kt b/src/main/java/com/android/tools/metalava/Compatibility.kt
index 5c21e38..536f5f5 100644
--- a/src/main/java/com/android/tools/metalava/Compatibility.kt
+++ b/src/main/java/com/android/tools/metalava/Compatibility.kt
@@ -45,6 +45,10 @@
/** Add in explicit `valueOf` and `values` methods into annotation classes */
var defaultAnnotationMethods: Boolean = compat
+ /** Whether signature files should contain annotation default values (as is already
+ * done for field default values) */
+ var includeAnnotationDefaults: Boolean = !compat
+
/** In signature files, refer to enums as "class" instead of "enum" */
var classForEnums: Boolean = compat
diff --git a/src/main/java/com/android/tools/metalava/ExtractAnnotations.kt b/src/main/java/com/android/tools/metalava/ExtractAnnotations.kt
index b0a7e8b..56cdd2d 100644
--- a/src/main/java/com/android/tools/metalava/ExtractAnnotations.kt
+++ b/src/main/java/com/android/tools/metalava/ExtractAnnotations.kt
@@ -30,6 +30,8 @@
import com.android.tools.metalava.model.MethodItem
import com.android.tools.metalava.model.PackageItem
import com.android.tools.metalava.model.ParameterItem
+import com.android.tools.metalava.model.canonicalizeFloatingPointString
+import com.android.tools.metalava.model.javaEscapeString
import com.android.tools.metalava.model.psi.PsiAnnotationItem
import com.android.tools.metalava.model.psi.PsiClassItem
import com.android.tools.metalava.model.visitors.ApiVisitor
@@ -37,10 +39,16 @@
import com.google.common.base.Charsets
import com.google.common.xml.XmlEscapers
import com.intellij.psi.PsiAnnotation
+import com.intellij.psi.PsiAnnotationMemberValue
+import com.intellij.psi.PsiArrayInitializerMemberValue
import com.intellij.psi.PsiClass
+import com.intellij.psi.PsiClassObjectAccessExpression
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiField
+import com.intellij.psi.PsiLiteral
import com.intellij.psi.PsiNameValuePair
+import com.intellij.psi.PsiReference
+import com.intellij.psi.PsiTypeCastExpression
import org.jetbrains.uast.UAnnotation
import org.jetbrains.uast.UBinaryExpressionWithType
import org.jetbrains.uast.UCallExpression
@@ -632,4 +640,133 @@
private fun error(string: String) {
reporter.report(Severity.WARNING, null as PsiElement?, string, Errors.ANNOTATION_EXTRACTION)
}
+
+ companion object {
+ /** Given an annotation member value, returns the corresponding Java source expression */
+ fun toSourceExpression(value: PsiAnnotationMemberValue, owner: Item): String {
+ val sb = StringBuilder()
+ appendSourceExpression(value, sb, owner)
+ return sb.toString()
+ }
+
+ private fun appendSourceExpression(value: PsiAnnotationMemberValue, sb: StringBuilder, owner: Item): Boolean {
+ if (value is PsiReference) {
+ val resolved = value.resolve()
+ if (resolved is PsiField) {
+ sb.append(resolved.containingClass?.qualifiedName).append('.').append(resolved.name)
+ return true
+ }
+ } else if (value is PsiLiteral) {
+ return appendSourceLiteral(value.value, sb, owner)
+ } else if (value is PsiClassObjectAccessExpression) {
+ sb.append(value.operand.type.canonicalText).append(".class")
+ return true
+ } else if (value is PsiArrayInitializerMemberValue) {
+ sb.append('{')
+ var first = true
+ val initialLength = sb.length
+ for (e in value.initializers) {
+ val length = sb.length
+ if (first) {
+ first = false
+ } else {
+ sb.append(", ")
+ }
+ val appended = appendSourceExpression(e, sb, owner)
+ if (!appended) {
+ // trunk off comma if it bailed for some reason (e.g. constant
+ // filtered out by API etc)
+ sb.setLength(length)
+ if (length == initialLength) {
+ first = true
+ }
+ }
+ }
+ sb.append('}')
+ return true
+ } else if (value is PsiAnnotation) {
+ sb.append('@').append(value.qualifiedName)
+ return true
+ } else {
+ if (value is PsiTypeCastExpression) {
+ val type = value.castType?.type
+ val operand = value.operand
+ if (type != null && operand is PsiAnnotationMemberValue) {
+ sb.append('(')
+ sb.append(type.canonicalText)
+ sb.append(')')
+ return appendSourceExpression(operand, sb, owner)
+ }
+ }
+ val constant = ConstantEvaluator.evaluate(null, value)
+ if (constant != null) {
+ return appendSourceLiteral(constant, sb, owner)
+ }
+ }
+ reporter.report(Errors.INTERNAL_ERROR, owner, "Unexpected annotation default value $value")
+ return false
+ }
+
+ private fun appendSourceLiteral(v: Any?, sb: StringBuilder, owner: Item): Boolean {
+ if (v == null) {
+ sb.append("null")
+ return true
+ }
+ when (v) {
+ is Int, is Long, is Boolean, is Byte, is Short -> {
+ sb.append(v.toString())
+ return true
+ }
+ is String -> {
+ sb.append('"').append(javaEscapeString(v)).append('"')
+ return true
+ }
+ is Float -> {
+ return when (v) {
+ Float.POSITIVE_INFINITY -> {
+ // This convention (displaying fractions) is inherited from doclava
+ sb.append("(1.0f/0.0f)"); true
+ }
+ Float.NEGATIVE_INFINITY -> {
+ sb.append("(-1.0f/0.0f)"); true
+ }
+ Float.NaN -> {
+ sb.append("(0.0f/0.0f)"); true
+ }
+ else -> {
+ sb.append(canonicalizeFloatingPointString(v.toString()) + "f")
+ true
+ }
+ }
+ }
+ is Double -> {
+ return when (v) {
+ Double.POSITIVE_INFINITY -> {
+ // This convention (displaying fractions) is inherited from doclava
+ sb.append("(1.0/0.0)"); true
+ }
+ Double.NEGATIVE_INFINITY -> {
+ sb.append("(-1.0/0.0)"); true
+ }
+ Double.NaN -> {
+ sb.append("(0.0/0.0)"); true
+ }
+ else -> {
+ sb.append(canonicalizeFloatingPointString(v.toString()))
+ true
+ }
+ }
+ }
+ is Char -> {
+ sb.append('\'').append(javaEscapeString(v.toString())).append('\'')
+ return true
+ }
+ else -> {
+ reporter.report(Errors.INTERNAL_ERROR, owner, "Unexpected literal value $v")
+ }
+ }
+
+ return false
+ }
+ }
}
diff --git a/src/main/java/com/android/tools/metalava/SignatureWriter.kt b/src/main/java/com/android/tools/metalava/SignatureWriter.kt
index 5577234..f15f039 100644
--- a/src/main/java/com/android/tools/metalava/SignatureWriter.kt
+++ b/src/main/java/com/android/tools/metalava/SignatureWriter.kt
@@ -98,6 +98,17 @@
writer.print(method.name())
writeParameterList(method)
writeThrowsList(method)
+
+ if (compatibility.includeAnnotationDefaults) {
+ if (method.containingClass().isAnnotationType()) {
+ val default = method.defaultValue()
+ if (default.isNotEmpty()) {
+ writer.print(" default ")
+ writer.print(default)
+ }
+ }
+ }
+
writer.print(";\n")
}
diff --git a/src/main/java/com/android/tools/metalava/StubWriter.kt b/src/main/java/com/android/tools/metalava/StubWriter.kt
index 06d5221..e55138e 100644
--- a/src/main/java/com/android/tools/metalava/StubWriter.kt
+++ b/src/main/java/com/android/tools/metalava/StubWriter.kt
@@ -501,6 +501,14 @@
generateParameterList(method)
generateThrowsList(method)
+ if (isAnnotation) {
+ val default = method.defaultValue()
+ if (default.isNotEmpty()) {
+ writer.print(" default ")
+ writer.print(default)
+ }
+ }
+
if (modifiers.isAbstract() && !removeAbstract && !isEnum || isAnnotation || modifiers.isNative()) {
writer.println(";")
} else {
diff --git a/src/main/java/com/android/tools/metalava/doclava1/ApiFile.java b/src/main/java/com/android/tools/metalava/doclava1/ApiFile.java
index b38c1ee..5456d75 100644
--- a/src/main/java/com/android/tools/metalava/doclava1/ApiFile.java
+++ b/src/main/java/com/android/tools/metalava/doclava1/ApiFile.java
@@ -568,6 +568,9 @@
if ("throws".equals(token)) {
token = parseThrows(tokenizer, method);
}
+ if ("default".equals(token)) {
+ token = parseDefault(tokenizer, method);
+ }
if (!";".equals(token)) {
throw new ApiParseException("expected ; found " + token, tokenizer);
}
@@ -852,6 +855,20 @@
}
}
+ private static String parseDefault(Tokenizer tokenizer, TextMethodItem method)
+ throws ApiParseException {
+ StringBuilder sb = new StringBuilder();
+ while (true) {
+ String token = tokenizer.requireToken();
+ if (";".equals(token)) {
+ method.setAnnotationDefault(sb.toString());
+ return token;
+ } else {
+ sb.append(token);
+ }
+ }
+ }
+
private static String parseThrows(Tokenizer tokenizer, TextMethodItem method)
throws ApiParseException {
String token = tokenizer.requireToken();
diff --git a/src/main/java/com/android/tools/metalava/doclava1/Errors.java b/src/main/java/com/android/tools/metalava/doclava1/Errors.java
index 69521c1..4f48174 100644
--- a/src/main/java/com/android/tools/metalava/doclava1/Errors.java
+++ b/src/main/java/com/android/tools/metalava/doclava1/Errors.java
@@ -198,6 +198,7 @@
public static final Error SUPERFLUOUS_PREFIX = new Error(147, WARNING);
public static final Error HIDDEN_TYPEDEF_CONSTANT = new Error(148, ERROR);
public static final Error MISSING_TYPEDEF_CONSTANT = new Error(149, LINT);
+ public static final Error INTERNAL_ERROR = new Error(150, ERROR);
static {
// Attempt to initialize error names based on the field names
diff --git a/src/main/java/com/android/tools/metalava/model/MethodItem.kt b/src/main/java/com/android/tools/metalava/model/MethodItem.kt
index 03eb387..a3836c0 100644
--- a/src/main/java/com/android/tools/metalava/model/MethodItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/MethodItem.kt
@@ -406,6 +406,9 @@
* declared in the signature) */
fun findThrownExceptions(): Set<ClassItem> = codebase.unsupported()
+ /** If annotation method, returns the default value as a source expression */
+ fun defaultValue(): String = ""
+
/**
* Returns true if this method is a signature match for the given method (e.g. can
* be overriding). This checks that the name and parameter lists match, but ignores
diff --git a/src/main/java/com/android/tools/metalava/model/psi/PsiMethodItem.kt b/src/main/java/com/android/tools/metalava/model/psi/PsiMethodItem.kt
index bbcae2a..75709ac 100644
--- a/src/main/java/com/android/tools/metalava/model/psi/PsiMethodItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/psi/PsiMethodItem.kt
@@ -16,6 +16,7 @@
package com.android.tools.metalava.model.psi
+import com.android.tools.metalava.ExtractAnnotations
import com.android.tools.metalava.compatibility
import com.android.tools.metalava.model.ClassItem
import com.android.tools.metalava.model.MethodItem
@@ -23,6 +24,7 @@
import com.android.tools.metalava.model.ParameterItem
import com.android.tools.metalava.model.TypeItem
import com.android.tools.metalava.model.TypeParameterList
+import com.intellij.psi.PsiAnnotationMethod
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiMethod
import com.intellij.psi.util.PsiTypesUtil
@@ -211,6 +213,17 @@
return exceptions
}
+ override fun defaultValue(): String {
+ if (psiMethod is PsiAnnotationMethod) {
+ val value = psiMethod.defaultValue
+ if (value != null) {
+ return ExtractAnnotations.toSourceExpression(value, this)
+ }
+ }
+
+ return super.defaultValue()
+ }
+
override fun duplicate(targetContainingClass: ClassItem): PsiMethodItem {
val duplicated = create(codebase, targetContainingClass as PsiClassItem, psiMethod)
diff --git a/src/main/java/com/android/tools/metalava/model/text/TextMethodItem.kt b/src/main/java/com/android/tools/metalava/model/text/TextMethodItem.kt
index e94db81..ec76fff 100644
--- a/src/main/java/com/android/tools/metalava/model/text/TextMethodItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/text/TextMethodItem.kt
@@ -190,4 +190,14 @@
"${if (isConstructor()) "Constructor" else "Method"} ${containingClass().qualifiedName()}.${name()}(${parameters().joinToString {
it.type().toSimpleType()
}})"
+
+ private var annotationDefault = ""
+
+ fun setAnnotationDefault(default: String) {
+ annotationDefault = default
+ }
+
+ override fun defaultValue(): String {
+ return annotationDefault
+ }
}
diff --git a/src/test/java/com/android/tools/metalava/ApiFromTextTest.kt b/src/test/java/com/android/tools/metalava/ApiFromTextTest.kt
index f167979..70d8020 100644
--- a/src/test/java/com/android/tools/metalava/ApiFromTextTest.kt
+++ b/src/test/java/com/android/tools/metalava/ApiFromTextTest.kt
@@ -459,4 +459,25 @@
api = source
)
}
+
+ @Test
+ fun `Signatures with default annotation method values`() {
+ val source = """
+ package libcore.util {
+ public @interface NonNull {
+ method public abstract int from() default java.lang.Integer.MIN_VALUE;
+ method public abstract double fromWithCast() default (double)java.lang.Float.NEGATIVE_INFINITY;
+ method public abstract String! myString() default "This is a \"string\"";
+ method public abstract int to() default java.lang.Integer.MAX_VALUE;
+ }
+ }
+ """
+
+ check(
+ inputKotlinStyleNulls = true,
+ compatibilityMode = false,
+ signatureSource = source,
+ api = source
+ )
+ }
}
\ No newline at end of file
diff --git a/src/test/java/com/android/tools/metalava/NullnessMigrationTest.kt b/src/test/java/com/android/tools/metalava/NullnessMigrationTest.kt
index ed328ae..6c1e2ef 100644
--- a/src/test/java/com/android/tools/metalava/NullnessMigrationTest.kt
+++ b/src/test/java/com/android/tools/metalava/NullnessMigrationTest.kt
@@ -238,8 +238,8 @@
api = """
package libcore.util {
public @interface NonNull {
- method public abstract int from();
- method public abstract int to();
+ method public abstract int from() default java.lang.Integer.MIN_VALUE;
+ method public abstract int to() default java.lang.Integer.MAX_VALUE;
}
}
package test.pkg {
diff --git a/src/test/java/com/android/tools/metalava/StubsTest.kt b/src/test/java/com/android/tools/metalava/StubsTest.kt
index f42d4bb..e1f47b4 100644
--- a/src/test/java/com/android/tools/metalava/StubsTest.kt
+++ b/src/test/java/com/android/tools/metalava/StubsTest.kt
@@ -3018,6 +3018,150 @@
)
}
+ @Test
+ fun `Annotation default values`() {
+
+ checkStubs(
+ compatibilityMode = false,
+ sourceFiles =
+ *arrayOf(
+ java(
+ """
+ package test.pkg;
+
+ import java.lang.annotation.ElementType;
+ import java.lang.annotation.Retention;
+ import java.lang.annotation.RetentionPolicy;
+ import java.lang.annotation.Target;
+
+ import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+ /**
+ * This annotation can be used to mark fields and methods to be dumped by
+ * the view server. Only non-void methods with no arguments can be annotated
+ * by this annotation.
+ */
+ @Target({ElementType.FIELD, ElementType.METHOD})
+ @Retention(RetentionPolicy.RUNTIME)
+ public @interface ExportedProperty {
+ /**
+ * When resolveId is true, and if the annotated field/method return value
+ * is an int, the value is converted to an Android's resource name.
+ *
+ * @return true if the property's value must be transformed into an Android
+ * resource name, false otherwise
+ */
+ boolean resolveId() default false;
+ String prefix() default "";
+ String category() default "";
+ boolean formatToHexString() default false;
+ boolean hasAdjacentMapping() default false;
+ Class<? extends Number> myCls() default Integer.class;
+ char[] letters1() default {};
+ char[] letters2() default {'a', 'b', 'c'};
+ double from() default Double.NEGATIVE_INFINITY;
+ double fromWithCast() default (double)Float.NEGATIVE_INFINITY;
+ InnerAnnotation value() default @InnerAnnotation;
+ char letter() default 'a';
+ int integer() default 1;
+ long large_integer() default 1L;
+ float floating() default 1.0f;
+ double large_floating() default 1.0;
+ byte small() default 1;
+ short medium() default 1;
+ int math() default 1+2*3;
+ @InnerAnnotation
+ int unit() default PX;
+ int DP = 0;
+ int PX = 1;
+ int SP = 2;
+ @Retention(SOURCE)
+ @interface InnerAnnotation {
+ }
+ }
+ """
+ )
+ ),
+ warnings = "",
+ // TODO: Put default values into signature files if not compatibiility mode
+ api = """
+ package test.pkg {
+ public @interface ExportedProperty {
+ method public abstract String! category() default "";
+ method public abstract float floating() default 1.0f;
+ method public abstract boolean formatToHexString() default false;
+ method public abstract double from() default java.lang.Double.NEGATIVE_INFINITY;
+ method public abstract double fromWithCast() default (double)java.lang.Float.NEGATIVE_INFINITY;
+ method public abstract boolean hasAdjacentMapping() default false;
+ method public abstract int integer() default 1;
+ method public abstract double large_floating() default 1.0;
+ method public abstract long large_integer() default 1;
+ method public abstract char letter() default 'a';
+ method public abstract char[]! letters1() default {};
+ method public abstract char[]! letters2() default {'a', 'b', 'c'};
+ method public abstract int math() default 7;
+ method public abstract short medium() default 1;
+ method public abstract Class<? extends java.lang.Number>! myCls() default java.lang.Integer.class;
+ method public abstract String! prefix() default "";
+ method public abstract boolean resolveId() default false;
+ method public abstract byte small() default 1;
+ method public abstract int unit() default test.pkg.ExportedProperty.PX;
+ method public abstract test.pkg.ExportedProperty.InnerAnnotation! value() default @test.pkg.ExportedProperty.InnerAnnotation;
+ field public static final int DP = 0; // 0x0
+ field public static final int PX = 1; // 0x1
+ field public static final int SP = 2; // 0x2
+ }
+ public static @interface ExportedProperty.InnerAnnotation {
+ }
+ }
+ """,
+ source = """
+ package test.pkg;
+ /**
+ * This annotation can be used to mark fields and methods to be dumped by
+ * the view server. Only non-void methods with no arguments can be annotated
+ * by this annotation.
+ */
+ @SuppressWarnings({"unchecked", "deprecation", "all"})
+ public @interface ExportedProperty {
+ /**
+ * When resolveId is true, and if the annotated field/method return value
+ * is an int, the value is converted to an Android's resource name.
+ *
+ * @return true if the property's value must be transformed into an Android
+ * resource name, false otherwise
+ */
+ public boolean resolveId() default false;
+ public java.lang.String prefix() default "";
+ public java.lang.String category() default "";
+ public boolean formatToHexString() default false;
+ public boolean hasAdjacentMapping() default false;
+ public java.lang.Class<? extends java.lang.Number> myCls() default java.lang.Integer.class;
+ public char[] letters1() default {};
+ public char[] letters2() default {'a', 'b', 'c'};
+ public double from() default java.lang.Double.NEGATIVE_INFINITY;
+ public double fromWithCast() default (double)java.lang.Float.NEGATIVE_INFINITY;
+ public test.pkg.ExportedProperty.InnerAnnotation value() default @test.pkg.ExportedProperty.InnerAnnotation;
+ public char letter() default 'a';
+ public int integer() default 1;
+ public long large_integer() default 1;
+ public float floating() default 1.0f;
+ public double large_floating() default 1.0;
+ public byte small() default 1;
+ public short medium() default 1;
+ public int math() default 7;
+ public int unit() default test.pkg.ExportedProperty.PX;
+ public static final int DP = 0; // 0x0
+ public static final int PX = 1; // 0x1
+ public static final int SP = 2; // 0x2
+ @SuppressWarnings({"unchecked", "deprecation", "all"})
+ public static @interface InnerAnnotation {
+ }
+ }
+ """
+ )
+ }
+
@Test
fun `Check writing package info file`() {
checkStubs(