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(