diff --git a/src/main/java/com/android/tools/metalava/Options.kt b/src/main/java/com/android/tools/metalava/Options.kt index 07be60664f65e4f718ed68068937560f6557fdf8..33afd52f752236219a0f224ee625da70e10dcb8a 100644 --- a/src/main/java/com/android/tools/metalava/Options.kt +++ b/src/main/java/com/android/tools/metalava/Options.kt @@ -148,6 +148,7 @@ const val ARG_STUB_PACKAGES = "--stub-packages" const val ARG_STUB_IMPORT_PACKAGES = "--stub-import-packages" const val ARG_DELETE_EMPTY_BASELINES = "--delete-empty-baselines" const val ARG_SUBTRACT_API = "--subtract-api" +const val ARG_TYPEDEFS_IN_SIGNATURES = "--typedefs-in-signatures" class Options( private val args: Array<String>, @@ -556,6 +557,15 @@ class Options( /** List of signature files to export as JDiff files */ val convertToXmlFiles: List<ConvertFile> = mutableConvertToXmlFiles + enum class TypedefMode { + NONE, + REFERENCE, + INLINE + } + + /** How to handle typedef annotations in signature files; corresponds to $ARG_TYPEDEFS_IN_SIGNATURES */ + var typedefMode = TypedefMode.NONE + /** File conversion tasks */ data class ConvertFile( val fromApiFile: File, @@ -769,6 +779,17 @@ class Options( mutableSkipEmitPackages += packages.split(File.pathSeparatorChar) } + ARG_TYPEDEFS_IN_SIGNATURES -> { + val type = getValue(args, ++index) + typedefMode = when (type) { + "ref" -> TypedefMode.REFERENCE + "inline" -> TypedefMode.INLINE + "none" -> TypedefMode.NONE + else -> throw DriverException( + stderr = "$ARG_TYPEDEFS_IN_SIGNATURES must be one of ref, inline, none; was $type") + } + } + ARG_BASELINE -> { val relative = getValue(args, ++index) assert(baselineFile == null) { "Only one baseline is allowed; found both $baselineFile and $relative" } @@ -1922,6 +1943,12 @@ class Options( "$ARG_SUBTRACT_API <api file>", "Subtracts the API in the given signature or jar file from the " + "current API being emitted via $ARG_API, $ARG_STUBS, $ARG_DOC_STUBS, etc. " + "Note that the subtraction only applies to classes; it does not subtract members.", + "$ARG_TYPEDEFS_IN_SIGNATURES <ref|inline>", "Whether to include typedef annotations in signature " + + "files. `$ARG_TYPEDEFS_IN_SIGNATURES ref` will include just a reference to the typedef class, " + + "which is not itself part of the API and is not included as a class, and " + + "`$ARG_TYPEDEFS_IN_SIGNATURES inline` will include the constants themselves into each usage " + + "site. You can also supply `$ARG_TYPEDEFS_IN_SIGNATURES none` to explicitly turn it off, if the " + + "default ever changes.", "", "\nDocumentation:", ARG_PUBLIC, "Only include elements that are public", diff --git a/src/main/java/com/android/tools/metalava/model/AnnotationItem.kt b/src/main/java/com/android/tools/metalava/model/AnnotationItem.kt index 0ce737c54c84116a9c2745eed7423e4870a419ef..9b237b655191978ae3f074c55d13412f8e9c5d53 100644 --- a/src/main/java/com/android/tools/metalava/model/AnnotationItem.kt +++ b/src/main/java/com/android/tools/metalava/model/AnnotationItem.kt @@ -32,6 +32,7 @@ import com.android.tools.metalava.ANDROID_NULLABLE import com.android.tools.metalava.ANDROID_SUPPORT_ANNOTATION_PREFIX import com.android.tools.metalava.Compatibility import com.android.tools.metalava.JAVA_LANG_PREFIX +import com.android.tools.metalava.Options import com.android.tools.metalava.RECENTLY_NONNULL import com.android.tools.metalava.RECENTLY_NULLABLE import com.android.tools.metalava.doclava1.ApiPredicate @@ -85,6 +86,9 @@ interface AnnotationItem { /** True if this annotation represents @IntDef, @LongDef or @StringDef */ fun isTypeDefAnnotation(): Boolean { val name = qualifiedName() ?: return false + if (!(name.endsWith("Def"))) { + return false + } return (INT_DEF_ANNOTATION.isEquals(name) || STRING_DEF_ANNOTATION.isEquals(name) || LONG_DEF_ANNOTATION.isEquals(name) || @@ -120,6 +124,12 @@ interface AnnotationItem { return codebase.findClass(qualifiedName() ?: return null) } + /** If this annotation has a typedef annotation associated with it, return it */ + fun findTypedefAnnotation(): AnnotationItem? { + val className = originalName() ?: return null + return codebase.findClass(className)?.modifiers?.annotations()?.firstOrNull { it.isTypeDefAnnotation() } + } + /** Returns the retention of this annotation */ val retention: AnnotationRetention get() { @@ -466,6 +476,12 @@ interface AnnotationItem { // See if the annotation is pointing to an annotation class that is part of the API; if not, skip it. val cls = codebase.findClass(qualifiedName) ?: return NO_ANNOTATION_TARGETS if (!ApiPredicate().test(cls)) { + if (options.typedefMode != Options.TypedefMode.NONE) { + if (cls.modifiers.annotations().any { it.isTypeDefAnnotation() }) { + return ANNOTATION_SIGNATURE_ONLY + } + } + return NO_ANNOTATION_TARGETS } diff --git a/src/main/java/com/android/tools/metalava/model/AnnotationTarget.kt b/src/main/java/com/android/tools/metalava/model/AnnotationTarget.kt index 82671f035f802d179f1d270a7c1e7672699a8ffe..a4d2c72f74039044261295d545dbd7a6eecc6933 100644 --- a/src/main/java/com/android/tools/metalava/model/AnnotationTarget.kt +++ b/src/main/java/com/android/tools/metalava/model/AnnotationTarget.kt @@ -16,6 +16,9 @@ package com.android.tools.metalava.model +import com.android.tools.metalava.Options +import com.android.tools.metalava.options + /** Various places where a given annotation can be written */ enum class AnnotationTarget { /** Write the annotation into the signature file */ @@ -80,4 +83,11 @@ val ANNOTATION_IN_DOC_STUBS_AND_EXTERNAL = setOf( val ANNOTATION_EXTERNAL = setOf(AnnotationTarget.SIGNATURE_FILE, AnnotationTarget.EXTERNAL_ANNOTATIONS_FILE) /** Write it only into the external annotations file, not the signature file */ -val ANNOTATION_EXTERNAL_ONLY = setOf(AnnotationTarget.SIGNATURE_FILE, AnnotationTarget.EXTERNAL_ANNOTATIONS_FILE) +val ANNOTATION_EXTERNAL_ONLY = if (options.typedefMode == Options.TypedefMode.INLINE || + options.typedefMode == Options.TypedefMode.NONE) // just here for compatibility purposes + setOf(AnnotationTarget.SIGNATURE_FILE, AnnotationTarget.EXTERNAL_ANNOTATIONS_FILE) +else + setOf(AnnotationTarget.EXTERNAL_ANNOTATIONS_FILE) + +/** Write it only into the he signature file */ +val ANNOTATION_SIGNATURE_ONLY = setOf(AnnotationTarget.SIGNATURE_FILE) diff --git a/src/main/java/com/android/tools/metalava/model/ModifierList.kt b/src/main/java/com/android/tools/metalava/model/ModifierList.kt index d645edf3d9744a95b5560d0ff113883384b266cd..e46e1ef579bf09f6d52007ef284e415df8118be1 100644 --- a/src/main/java/com/android/tools/metalava/model/ModifierList.kt +++ b/src/main/java/com/android/tools/metalava/model/ModifierList.kt @@ -493,6 +493,7 @@ interface ModifierList { continue } + var printAnnotation = annotation if (!annotation.targets().contains(target)) { continue } else if ((annotation.isNullnessAnnotation())) { @@ -502,6 +503,23 @@ interface ModifierList { } else if (annotation.qualifiedName() == "java.lang.Deprecated") { // Special cased in stubs and signature files: emitted first continue + } else if (options.typedefMode == Options.TypedefMode.INLINE) { + val typedef = annotation.findTypedefAnnotation() + if (typedef != null) { + printAnnotation = typedef + } + } else if (options.typedefMode == Options.TypedefMode.REFERENCE && + annotation.targets() === ANNOTATION_SIGNATURE_ONLY && + annotation.findTypedefAnnotation() != null) { + // For annotation references, only include the simple name + writer.write("@") + writer.write(annotation.resolve()?.simpleName() ?: annotation.qualifiedName()!!) + if (separateLines) { + writer.write("\n") + } else { + writer.write(" ") + } + continue } // Optionally filter out duplicates @@ -520,7 +538,8 @@ interface ModifierList { } } - val source = annotation.toSource(target) + val source = printAnnotation.toSource(target) + if (omitCommonPackages) { writer.write(AnnotationItem.shortenAnnotation(source)) } else { diff --git a/src/test/java/com/android/tools/metalava/ExtractAnnotationsTest.kt b/src/test/java/com/android/tools/metalava/ExtractAnnotationsTest.kt index 2d9269567e19aedc5a3a8c28d1bb2be07db43fbc..02cb3a81afe1cbe93f3fc545e635062d0e83229b 100644 --- a/src/test/java/com/android/tools/metalava/ExtractAnnotationsTest.kt +++ b/src/test/java/com/android/tools/metalava/ExtractAnnotationsTest.kt @@ -21,13 +21,9 @@ import org.junit.Test @SuppressWarnings("ALL") // Sample code class ExtractAnnotationsTest : DriverTest() { - @Test - fun `Check java typedef extraction and warning about non-source retention of typedefs`() { - check( - includeSourceRetentionAnnotations = false, - sourceFiles = *arrayOf( - java( - """ + private val sourceFiles1 = arrayOf( + java( + """ package test.pkg; import android.annotation.IntDef; @@ -70,10 +66,17 @@ class ExtractAnnotationsTest : DriverTest() { } } """ - ).indented(), - intDefAnnotationSource, - intRangeAnnotationSource - ), + ).indented(), + intDefAnnotationSource, + intRangeAnnotationSource + ) + + @Test + fun `Check java typedef extraction and warning about non-source retention of typedefs`() { + check( + includeSourceRetentionAnnotations = false, + format = FileFormat.V2, + sourceFiles = *sourceFiles1, warnings = "src/test/pkg/IntDefTest.java:11: error: This typedef annotation class should have @Retention(RetentionPolicy.SOURCE) [AnnotationExtraction]", extractAnnotations = mapOf( "test.pkg" to """ @@ -509,4 +512,112 @@ class ExtractAnnotationsTest : DriverTest() { ) ) } + + @Test + fun `No typedef signatures in api files`() { + check( + includeSourceRetentionAnnotations = false, + extraArguments = arrayOf( + ARG_HIDE_PACKAGE, "android.annotation", + ARG_TYPEDEFS_IN_SIGNATURES, "none" + ), + format = FileFormat.V2, + sourceFiles = *sourceFiles1, + api = """ + // Signature format: 2.0 + package test.pkg { + public class IntDefTest { + ctor public IntDefTest(); + method public void setFlags(Object, int); + method public void setStyle(int, int); + method public void testIntDef(int); + field public static final int STYLE_NORMAL = 0; // 0x0 + field public static final int STYLE_NO_FRAME = 2; // 0x2 + field public static final int STYLE_NO_INPUT = 3; // 0x3 + field public static final int STYLE_NO_TITLE = 1; // 0x1 + field public static final String TYPE_1 = "type1"; + field public static final String TYPE_2 = "type2"; + field public static final int UNRELATED = 3; // 0x3 + field public static final String UNRELATED_TYPE = "other"; + } + public static class IntDefTest.Inner { + ctor public IntDefTest.Inner(); + method public void setInner(int); + } + } + """ + ) + } + + @Test + fun `Inlining typedef signatures in api files`() { + check( + includeSourceRetentionAnnotations = false, + extraArguments = arrayOf( + ARG_HIDE_PACKAGE, "android.annotation", + ARG_TYPEDEFS_IN_SIGNATURES, "inline" + ), + format = FileFormat.V2, + sourceFiles = *sourceFiles1, + api = """ + // Signature format: 2.0 + package test.pkg { + public class IntDefTest { + ctor public IntDefTest(); + method public void setFlags(Object, @IntDef(value={test.pkg.IntDefTest.STYLE_NORMAL, test.pkg.IntDefTest.STYLE_NO_TITLE, test.pkg.IntDefTest.STYLE_NO_FRAME, test.pkg.IntDefTest.STYLE_NO_INPUT, 3, 3 + 1}, flag=true) int); + method public void setStyle(@IntDef({test.pkg.IntDefTest.STYLE_NORMAL, test.pkg.IntDefTest.STYLE_NO_TITLE, test.pkg.IntDefTest.STYLE_NO_FRAME, test.pkg.IntDefTest.STYLE_NO_INPUT}) int, int); + method public void testIntDef(int); + field public static final int STYLE_NORMAL = 0; // 0x0 + field public static final int STYLE_NO_FRAME = 2; // 0x2 + field public static final int STYLE_NO_INPUT = 3; // 0x3 + field public static final int STYLE_NO_TITLE = 1; // 0x1 + field public static final String TYPE_1 = "type1"; + field public static final String TYPE_2 = "type2"; + field public static final int UNRELATED = 3; // 0x3 + field public static final String UNRELATED_TYPE = "other"; + } + public static class IntDefTest.Inner { + ctor public IntDefTest.Inner(); + method public void setInner(@IntDef(value={test.pkg.IntDefTest.STYLE_NORMAL, test.pkg.IntDefTest.STYLE_NO_TITLE, test.pkg.IntDefTest.STYLE_NO_FRAME, test.pkg.IntDefTest.STYLE_NO_INPUT, 3, 3 + 1}, flag=true) int); + } + } + """ + ) + } + + @Test + fun `Referencing typedef signatures in api files`() { + check( + includeSourceRetentionAnnotations = false, + extraArguments = arrayOf( + ARG_HIDE_PACKAGE, "android.annotation", + ARG_TYPEDEFS_IN_SIGNATURES, "ref" + ), + format = FileFormat.V2, + sourceFiles = *sourceFiles1, + api = """ + // Signature format: 2.0 + package test.pkg { + public class IntDefTest { + ctor public IntDefTest(); + method public void setFlags(Object, @DialogFlags int); + method public void setStyle(@DialogStyle int, int); + method public void testIntDef(int); + field public static final int STYLE_NORMAL = 0; // 0x0 + field public static final int STYLE_NO_FRAME = 2; // 0x2 + field public static final int STYLE_NO_INPUT = 3; // 0x3 + field public static final int STYLE_NO_TITLE = 1; // 0x1 + field public static final String TYPE_1 = "type1"; + field public static final String TYPE_2 = "type2"; + field public static final int UNRELATED = 3; // 0x3 + field public static final String UNRELATED_TYPE = "other"; + } + public static class IntDefTest.Inner { + ctor public IntDefTest.Inner(); + method public void setInner(@DialogFlags int); + } + } + """ + ) + } } \ No newline at end of file diff --git a/src/test/java/com/android/tools/metalava/OptionsTest.kt b/src/test/java/com/android/tools/metalava/OptionsTest.kt index d625725a9be67f692a8e360a1fa0e1ee76f89f53..c6af7606961ea96b4bbd1ee1c9ac02eed1e9afd5 100644 --- a/src/test/java/com/android/tools/metalava/OptionsTest.kt +++ b/src/test/java/com/android/tools/metalava/OptionsTest.kt @@ -121,6 +121,16 @@ API sources: --api, --stubs, --doc-stubs, etc. Note that the subtraction only applies to classes; it does not subtract members. +--typedefs-in-signatures <ref|inline> Whether to include typedef annotations in + signature files. `--typedefs-in-signatures ref` + will include just a reference to the typedef + class, which is not itself part of the API and + is not included as a class, and + `--typedefs-in-signatures inline` will include + the constants themselves into each usage site. + You can also supply `--typedefs-in-signatures + none` to explicitly turn it off, if the default + ever changes. Documentation: --public Only include elements that are public