diff --git a/src/main/java/com/android/tools/metalava/DocAnalyzer.kt b/src/main/java/com/android/tools/metalava/DocAnalyzer.kt index c2550929bc43457eb40cbbd2c278e24a37ee64de..77ff1050916853fb6c37a5a14ff48f09b0313808 100644 --- a/src/main/java/com/android/tools/metalava/DocAnalyzer.kt +++ b/src/main/java/com/android/tools/metalava/DocAnalyzer.kt @@ -206,6 +206,7 @@ class DocAnalyzer( "androidx.annotation.StringDef" -> handleTypeDef(annotation, item) "android.annotation.RequiresFeature" -> handleRequiresFeature(annotation, item) "androidx.annotation.RequiresApi" -> handleRequiresApi(annotation, item) + "android.provider.Column" -> handleColumn(annotation, item) "kotlin.Deprecated" -> handleKotlinDeprecation(annotation, item) } @@ -484,6 +485,48 @@ class DocAnalyzer( addApiLevelDocumentation(level, item) } } + + private fun handleColumn( + annotation: AnnotationItem, + item: Item + ) { + val value = annotation.findAttribute("value")?.leafValues()?.firstOrNull() ?: return + val readOnly = annotation.findAttribute("readOnly")?.leafValues()?.firstOrNull()?.value() == true + val sb = StringBuilder(100) + val resolved = value.resolve() + val field = resolved as? FieldItem + sb.append("This constant represents a column name that can be used with a ") + sb.append("{@link android.content.ContentProvider}") + sb.append(" through a ") + sb.append("{@link android.content.ContentValues}") + sb.append(" or ") + sb.append("{@link android.database.Cursor}") + sb.append(" object. The values stored in this column are ") + sb.append("") + if (field == null) { + reporter.report( + Errors.MISSING_COLUMN, item, + "Cannot find feature field for $value required by $item (may be hidden or removed)" + ) + sb.append("{@link ${value.toSource()}}") + } else { + if (filterReference.test(field)) { + sb.append("{@link ${field.containingClass().qualifiedName()}#${field.name()} ${field.containingClass().simpleName()}#${field.name()}} ") + } else { + reporter.report( + Errors.MISSING_COLUMN, item, + "Feature field $value required by $item is hidden or removed" + ) + sb.append("${field.containingClass().simpleName()}#${field.name()} ") + } + } + + if (readOnly) { + sb.append(", and are read-only and cannot be mutated") + } + sb.append(".") + appendDocumentation(sb.toString(), item, false) + } }) } 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 8621c5c7c9fa0f0ec217efc7a9cf7911d7c1e023..dfb78b91aff932d7e050c8f276ab1322edb5f9fc 100644 --- a/src/main/java/com/android/tools/metalava/doclava1/Errors.java +++ b/src/main/java/com/android/tools/metalava/doclava1/Errors.java @@ -258,6 +258,7 @@ public class Errors { // and (2) the principle is adopted by the API council public static final Error EXTENDS_DEPRECATED = new Error(161, HIDDEN); public static final Error FORBIDDEN_TAG = new Error(162, ERROR); + public static final Error MISSING_COLUMN = new Error(163, WARNING, Category.DOCUMENTATION); // API lint public static final Error START_WITH_LOWER = new Error(300, ERROR, Category.API_LINT, "S1"); diff --git a/src/test/java/com/android/tools/metalava/DocAnalyzerTest.kt b/src/test/java/com/android/tools/metalava/DocAnalyzerTest.kt index 5811331309cef36243f9023e081aa05181696efa..5b4efcff8da647960bb2c4a6b52097eeb4717622 100644 --- a/src/test/java/com/android/tools/metalava/DocAnalyzerTest.kt +++ b/src/test/java/com/android/tools/metalava/DocAnalyzerTest.kt @@ -1869,4 +1869,75 @@ class DocAnalyzerTest : DriverTest() { dir.deleteRecursively() } + + @Test + fun `Test Column annotation`() { + // Bug: 120429729 + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + import android.provider.Column; + import android.database.Cursor; + @SuppressWarnings("WeakerAccess") + public class ColumnTest { + @Column(Cursor.FIELD_TYPE_STRING) + public static final String DATA = "_data"; + @Column(value = Cursor.FIELD_TYPE_BLOB, readOnly = true) + public static final String HASH = "_hash"; + @Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true) + public static final String TITLE = "title"; + @Column(value = Cursor.NONEXISTENT, readOnly = true) + public static final String BOGUS = "bogus"; + } + """ + ), + java( + """ + package android.database; + public interface Cursor { + int FIELD_TYPE_NULL = 0; + int FIELD_TYPE_INTEGER = 1; + int FIELD_TYPE_FLOAT = 2; + int FIELD_TYPE_STRING = 3; + int FIELD_TYPE_BLOB = 4; + } + """ + ), + columnSource + ), + checkCompilation = true, + checkDoclava1 = false, + warnings = """ + src/test/pkg/ColumnTest.java:12: warning: Cannot find feature field for Cursor.NONEXISTENT required by field ColumnTest.BOGUS (may be hidden or removed) [MissingColumn] + """, + stubs = arrayOf( + """ + package test.pkg; + import android.database.Cursor; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class ColumnTest { + public ColumnTest() { throw new RuntimeException("Stub!"); } + /** + * This constant represents a column name that can be used with a {@link android.content.ContentProvider} through a {@link android.content.ContentValues} or {@link android.database.Cursor} object. The values stored in this column are {@link Cursor.NONEXISTENT}, and are read-only and cannot be mutated. + */ + @android.provider.Column(value=Cursor.NONEXISTENT, readOnly=true) public static final java.lang.String BOGUS = "bogus"; + /** + * This constant represents a column name that can be used with a {@link android.content.ContentProvider} through a {@link android.content.ContentValues} or {@link android.database.Cursor} object. The values stored in this column are {@link android.database.Cursor#FIELD_TYPE_STRING Cursor#FIELD_TYPE_STRING} . + */ + @android.provider.Column(android.database.Cursor.FIELD_TYPE_STRING) public static final java.lang.String DATA = "_data"; + /** + * This constant represents a column name that can be used with a {@link android.content.ContentProvider} through a {@link android.content.ContentValues} or {@link android.database.Cursor} object. The values stored in this column are {@link android.database.Cursor#FIELD_TYPE_BLOB Cursor#FIELD_TYPE_BLOB} , and are read-only and cannot be mutated. + */ + @android.provider.Column(value=android.database.Cursor.FIELD_TYPE_BLOB, readOnly=true) public static final java.lang.String HASH = "_hash"; + /** + * This constant represents a column name that can be used with a {@link android.content.ContentProvider} through a {@link android.content.ContentValues} or {@link android.database.Cursor} object. The values stored in this column are {@link android.database.Cursor#FIELD_TYPE_STRING Cursor#FIELD_TYPE_STRING} , and are read-only and cannot be mutated. + */ + @android.provider.Column(value=android.database.Cursor.FIELD_TYPE_STRING, readOnly=true) public static final java.lang.String TITLE = "title"; + } + """ + ) + ) + } } \ No newline at end of file diff --git a/src/test/java/com/android/tools/metalava/DriverTest.kt b/src/test/java/com/android/tools/metalava/DriverTest.kt index 8db14dc75dc4ac51776b3aa7c18cc3065f144351..5618e94f49cf4a986acbc132818801e7ff8e9634 100644 --- a/src/test/java/com/android/tools/metalava/DriverTest.kt +++ b/src/test/java/com/android/tools/metalava/DriverTest.kt @@ -2453,3 +2453,28 @@ val visibleForTestingSource: TestFile = java( } """ ).indented() + +val columnSource: TestFile = java( + """ + package android.provider; + + import static java.lang.annotation.ElementType.FIELD; + import static java.lang.annotation.RetentionPolicy.RUNTIME; + + import android.content.ContentProvider; + import android.content.ContentValues; + import android.database.Cursor; + + import java.lang.annotation.Documented; + import java.lang.annotation.Retention; + import java.lang.annotation.Target; + + @Documented + @Retention(RUNTIME) + @Target({FIELD}) + public @interface Column { + int value(); + boolean readOnly() default false; + } + """ +).indented() \ No newline at end of file