Lint check to verify @Deprecated and @deprecated match

This check exists in metalava only for public API surface, so the lint tries to not check any declarations that aren't public APIs.

The check in metalava wasn't complete (requiring @Deprecated when @deprecated is present is a TODO), so there are a couple @Deprecated tags added to existing violations.

Bug: 201630817
Test: Added unit tests

Change-Id: I964a67c506eb5454431347fd05f4a584bd905196
diff --git a/browser/browser/src/main/java/androidx/browser/trusted/TrustedWebActivityIntentBuilder.java b/browser/browser/src/main/java/androidx/browser/trusted/TrustedWebActivityIntentBuilder.java
index d8f3b5e..bd923f4 100644
--- a/browser/browser/src/main/java/androidx/browser/trusted/TrustedWebActivityIntentBuilder.java
+++ b/browser/browser/src/main/java/androidx/browser/trusted/TrustedWebActivityIntentBuilder.java
@@ -117,6 +117,7 @@
      *
      * @deprecated Use {@link #setDefaultColorSchemeParams} instead.
      */
+    @Deprecated
     @NonNull
     public TrustedWebActivityIntentBuilder setToolbarColor(@ColorInt int color) {
         mIntentBuilder.setToolbarColor(color);
@@ -128,6 +129,7 @@
      *
      * @deprecated Use {@link #setDefaultColorSchemeParams} instead.
      */
+    @Deprecated
     @NonNull
     public TrustedWebActivityIntentBuilder setNavigationBarColor(@ColorInt int color) {
         mIntentBuilder.setNavigationBarColor(color);
@@ -140,6 +142,7 @@
      *
      * @deprecated Use {@link #setDefaultColorSchemeParams} instead.
      */
+    @Deprecated
     @NonNull
     public TrustedWebActivityIntentBuilder setNavigationBarDividerColor(@ColorInt int color) {
         mIntentBuilder.setNavigationBarDividerColor(color);
diff --git a/core/core-appdigest/src/main/java/androidx/core/appdigest/Checksum.java b/core/core-appdigest/src/main/java/androidx/core/appdigest/Checksum.java
index 69e3629..47fa0ca 100644
--- a/core/core-appdigest/src/main/java/androidx/core/appdigest/Checksum.java
+++ b/core/core-appdigest/src/main/java/androidx/core/appdigest/Checksum.java
@@ -83,6 +83,7 @@
      *
      * @see Checksums#getChecksums
      */
+    @Deprecated
     public static final int TYPE_WHOLE_SHA256 = 0x00000008;
 
     /**
@@ -93,6 +94,7 @@
      *
      * @see Checksums#getChecksums
      */
+    @Deprecated
     public static final int TYPE_WHOLE_SHA512 = 0x00000010;
 
     /**
diff --git a/lint-checks/src/main/java/androidx/build/lint/AndroidXIssueRegistry.kt b/lint-checks/src/main/java/androidx/build/lint/AndroidXIssueRegistry.kt
index 9af2442..4bcf319 100644
--- a/lint-checks/src/main/java/androidx/build/lint/AndroidXIssueRegistry.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/AndroidXIssueRegistry.kt
@@ -76,7 +76,8 @@
                 // MissingJvmDefaultWithCompatibilityDetector is intentionally left out of the
                 // registry, see comments on the class for more details.
                 BanVisibleForTestingParams.ISSUE,
-                PrereleaseSdkCoreDependencyDetector.ISSUE
+                PrereleaseSdkCoreDependencyDetector.ISSUE,
+                DeprecationMismatchDetector.ISSUE,
             )
         }
     }
diff --git a/lint-checks/src/main/java/androidx/build/lint/DeprecationMismatchDetector.kt b/lint-checks/src/main/java/androidx/build/lint/DeprecationMismatchDetector.kt
new file mode 100644
index 0000000..f6ac92d
--- /dev/null
+++ b/lint-checks/src/main/java/androidx/build/lint/DeprecationMismatchDetector.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2023 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 androidx.build.lint
+
+import com.android.tools.lint.client.api.UElementHandler
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Incident
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.LocationType
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.isKotlin
+import com.intellij.psi.PsiComment
+import org.jetbrains.uast.UAnonymousClass
+import org.jetbrains.uast.UDeclaration
+import org.jetbrains.uast.UMethod
+import org.jetbrains.uast.UastVisibility
+import org.jetbrains.uast.getContainingUClass
+
+class DeprecationMismatchDetector : Detector(), Detector.UastScanner {
+
+    override fun getApplicableUastTypes() = listOf(UDeclaration::class.java)
+
+    override fun createUastHandler(context: JavaContext): UElementHandler {
+        return DeprecationChecker(context)
+    }
+
+    private inner class DeprecationChecker(val context: JavaContext) : UElementHandler() {
+        override fun visitDeclaration(node: UDeclaration) {
+            // This check is only applicable for Java, the Kotlin @Deprecated has a message field
+            if (isKotlin(node.lang)) return
+
+            // This check is for API elements, not anonymous class declarations
+            if (node is UAnonymousClass) return
+            if (node is UMethod && node.name == "<anon-init>") return
+
+            // Not necessary if the element isn't public API
+            if (!applicableVisibilities.contains(node.visibility)) return
+
+            // Check if @deprecated and @Deprecated don't match
+            val hasDeprecatedDocTag = node.comments.any { it.text.contains("@deprecated") }
+            val hasDeprecatedAnnotation = node.hasAnnotation(DEPRECATED_ANNOTATION)
+            if (hasDeprecatedDocTag == hasDeprecatedAnnotation) return
+
+            // Proto-generated files are not part of the public API surface
+            if (node.containingFile.children.filterIsInstance<PsiComment>().any {
+                it.text.contains("Generated by the protocol buffer compiler.  DO NOT EDIT!")
+            }) return
+
+            // Methods that override deprecated methods can inherit docs from the original method
+            if (node is UMethod && node.hasAnnotation(OVERRIDE_ANNOTATION) &&
+                (node.comments.isEmpty() ||
+                    node.comments.any { it.text.contains("@inheritDoc") })) return
+
+            // @RestrictTo elements aren't part of the public API surface
+            if (node.hasAnnotation(RESTRICT_TO_ANNOTATION) ||
+                node.getContainingUClass()?.hasAnnotation(RESTRICT_TO_ANNOTATION) == true) return
+
+            // The mismatch is in a public API, report the error
+            val baseIncident = Incident(context)
+                .issue(ISSUE)
+                .location(context.getLocation(node, LocationType.NAME))
+                .scope(node)
+
+            val incident = if (hasDeprecatedAnnotation) {
+                // No auto-fix for this case since developers should write a comment with details
+                baseIncident
+                    .message("Items annotated with @Deprecated must have a @deprecated doc tag")
+            } else {
+                val fix = fix()
+                    .name("Annotate with @Deprecated")
+                    .annotate(DEPRECATED_ANNOTATION)
+                    .range(context.getLocation(node, LocationType.ALL))
+                    .autoFix()
+                    .build()
+
+                baseIncident
+                    .fix(fix)
+                    .message("Items with a @deprecated doc tag must be annotated with @Deprecated")
+            }
+
+            context.report(incident)
+        }
+    }
+
+    companion object {
+        val ISSUE = Issue.create(
+            "DeprecationMismatch",
+            "@Deprecated (annotation) and @deprecated (doc tag) must go together",
+            "A deprecated API should both be annotated with @Deprecated and have a " +
+                "@deprecated doc tag.",
+            Category.CORRECTNESS, 5, Severity.ERROR,
+            Implementation(DeprecationMismatchDetector::class.java, Scope.JAVA_FILE_SCOPE)
+        )
+
+        private const val DEPRECATED_ANNOTATION = "java.lang.Deprecated"
+        private const val RESTRICT_TO_ANNOTATION = "androidx.annotation.RestrictTo"
+        private const val OVERRIDE_ANNOTATION = "java.lang.Override"
+        private val applicableVisibilities = listOf(UastVisibility.PUBLIC, UastVisibility.PROTECTED)
+    }
+}
diff --git a/lint-checks/src/test/java/androidx/build/lint/DeprecationMismatchDetectorTest.kt b/lint-checks/src/test/java/androidx/build/lint/DeprecationMismatchDetectorTest.kt
new file mode 100644
index 0000000..6c46e9f
--- /dev/null
+++ b/lint-checks/src/test/java/androidx/build/lint/DeprecationMismatchDetectorTest.kt
@@ -0,0 +1,336 @@
+/*
+ * Copyright 2023 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 androidx.build.lint
+
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class DeprecationMismatchDetectorTest : AbstractLintDetectorTest(
+    useDetector = DeprecationMismatchDetector(),
+    useIssues = listOf(DeprecationMismatchDetector.ISSUE),
+) {
+    @Test
+    fun `Test correctly matched @deprecated and @Deprecated`() {
+        val input = arrayOf(
+            java(
+                """
+                    package java.androidx;
+
+                    /**
+                     * @deprecated Foo is deprecated
+                     */
+                    @Deprecated
+                    public class Foo {
+                        /**
+                         * @deprecated foo is deprecated
+                         */
+                        @Deprecated
+                        public void foo() {}
+
+                        /**
+                         * @deprecated FOO is deprecated
+                         */
+                        @Deprecated
+                        public static int FOO = 0;
+
+                        /**
+                         * @deprecated InnerFoo is deprecated
+                         */
+                        @Deprecated
+                        public interface InnerFoo {}
+                    }
+                """.trimIndent()
+            )
+        )
+
+        check(*input).expectClean()
+    }
+
+    @Test
+    fun `Test @deprecated missing @Deprecated`() {
+        val input = arrayOf(
+            java(
+                """
+                    package java.androidx;
+
+                    /**
+                     * @deprecated Foo is deprecated
+                     */
+                    public class Foo {
+                        /**
+                         * @deprecated foo is deprecated
+                         */
+                        public void foo() {}
+
+                        /**
+                         * @deprecated FOO is deprecated
+                         */
+                        public static int FOO = 0;
+
+                        /**
+                         * @deprecated InnerFoo is deprecated
+                         */
+                        public interface InnerFoo {}
+                    }
+                """.trimIndent()
+            )
+        )
+
+        /* ktlint-disable max-line-length */
+        val expected = """
+            src/java/androidx/Foo.java:6: Error: Items with a @deprecated doc tag must be annotated with @Deprecated [DeprecationMismatch]
+            public class Foo {
+                         ~~~
+            src/java/androidx/Foo.java:10: Error: Items with a @deprecated doc tag must be annotated with @Deprecated [DeprecationMismatch]
+                public void foo() {}
+                            ~~~
+            src/java/androidx/Foo.java:15: Error: Items with a @deprecated doc tag must be annotated with @Deprecated [DeprecationMismatch]
+                public static int FOO = 0;
+                                  ~~~
+            src/java/androidx/Foo.java:20: Error: Items with a @deprecated doc tag must be annotated with @Deprecated [DeprecationMismatch]
+                public interface InnerFoo {}
+                                 ~~~~~~~~
+            4 errors, 0 warnings
+        """.trimIndent()
+
+        val expectedFixDiffs = """
+            Autofix for src/java/androidx/Foo.java line 6: Annotate with @Deprecated:
+            @@ -3 +3
+            + @Deprecated
+            Autofix for src/java/androidx/Foo.java line 10: Annotate with @Deprecated:
+            @@ -7 +7
+            +     @Deprecated
+            Autofix for src/java/androidx/Foo.java line 15: Annotate with @Deprecated:
+            @@ -12 +12
+            +     @Deprecated
+            Autofix for src/java/androidx/Foo.java line 20: Annotate with @Deprecated:
+            @@ -17 +17
+            +     @Deprecated
+        """.trimIndent()
+        /* ktlint-enable max-line-length */
+
+       check(*input).expect(expected).expectFixDiffs(expectedFixDiffs)
+    }
+
+    @Test
+    fun `Test @Deprecated missing @deprecated`() {
+        val input = arrayOf(
+            java(
+                """
+                    package java.androidx;
+
+                    @Deprecated
+                    public class Foo {
+                        @Deprecated
+                        public void foo() {}
+
+                        @Deprecated
+                        public static int FOO = 0;
+
+                        @Deprecated
+                        public interface InnerFoo {}
+                    }
+                """.trimIndent()
+            )
+        )
+
+        /* ktlint-disable max-line-length */
+        val expected = """
+            src/java/androidx/Foo.java:4: Error: Items annotated with @Deprecated must have a @deprecated doc tag [DeprecationMismatch]
+            public class Foo {
+                         ~~~
+            src/java/androidx/Foo.java:6: Error: Items annotated with @Deprecated must have a @deprecated doc tag [DeprecationMismatch]
+                public void foo() {}
+                            ~~~
+            src/java/androidx/Foo.java:9: Error: Items annotated with @Deprecated must have a @deprecated doc tag [DeprecationMismatch]
+                public static int FOO = 0;
+                                  ~~~
+            src/java/androidx/Foo.java:12: Error: Items annotated with @Deprecated must have a @deprecated doc tag [DeprecationMismatch]
+                public interface InnerFoo {}
+                                 ~~~~~~~~
+            4 errors, 0 warnings
+        """.trimIndent()
+        /* ktlint-enable max-line-length */
+
+        check(*input).expect(expected)
+    }
+
+    @Test
+    fun `Test @deprecated not required for private APIs`() {
+        val input = arrayOf(
+            java(
+                """
+                    package java.androidx;
+
+                    @Deprecated
+                    private class Foo {
+                        @Deprecated
+                        private void foo() {}
+
+                        @Deprecated
+                        private static int FOO = 0;
+
+                        @Deprecated
+                        private interface InnerFoo {}
+                    }
+                """.trimIndent()
+            )
+        )
+        check(*input).expectClean()
+    }
+
+    @Test
+    fun `Test @deprecated not required for proto-generated APIs`() {
+        val input = arrayOf(
+            java(
+                """
+                    // Generated by the protocol buffer compiler.  DO NOT EDIT!
+                    package java.androidx.proto;
+
+                    @Deprecated
+                    public class Foo {
+                        @Deprecated
+                        public void foo() {}
+
+                        @Deprecated
+                        public static int FOO = 0;
+
+                        @Deprecated
+                        public interface InnerFoo {}
+                    }
+                """.trimIndent()
+            )
+        )
+        check(*input).expectClean()
+    }
+
+    @Test
+    fun `Test anonymous classes don't need @deprecated`() {
+        val input = arrayOf(
+            java(
+                """
+                    package java.androidx;
+
+                    /**
+                     * @deprecated Foo is deprecated
+                     */
+                    @Deprecated
+                    public abstract class Foo<T> {
+                        /**
+                         * @deprecated foo is deprecated
+                         */
+                        @Deprecated
+                        public void foo();
+                    }
+                """.trimIndent()
+            ),
+            java(
+                """
+                    package java.androidx;
+
+                    public class Bar {
+                        public static void bar() {
+                            new Foo<String>() {
+                                @Override
+                                public void foo() {}
+                            }.foo();
+                        }
+                    }
+                """.trimIndent()
+            )
+        )
+
+        check(*input).expectClean()
+    }
+
+    @Test
+    fun `Test @RestrictTo APIs don't need @deprecated`() {
+        val input = arrayOf(
+            java(
+                """
+                    package java.androidx;
+
+                    import androidx.annotation.RestrictTo;
+
+                    @RestrictTo(RestrictTo.Scope.LIBRARY)
+                    @Deprecated
+                    public class Foo {
+                        @Deprecated
+                        private void foo() {}
+
+                        @Deprecated
+                        private static int FOO = 0;
+
+                        @Deprecated
+                        private interface InnerFoo {}
+                    }
+                """.trimIndent()
+            ),
+            Stubs.RestrictTo
+        )
+        check(*input).expectClean()
+    }
+
+    @Test
+    fun `Test overriding methods don't need @deprecated`() {
+        val input = arrayOf(
+            java(
+                """
+                    package java.androidX;
+
+                    public interface MyInterface {
+                        /** @deprecated Use XYZ instead. */
+                        @Deprecated
+                        void inheritedNoComment();
+
+                        /** @deprecated Use XYZ instead. */
+                        @Deprecated
+                        void inheritedWithComment();
+
+                        /** @deprecated Use XYZ instead. */
+                        @Deprecated
+                        void inheritedWithInheritDoc();
+                    }
+                """,
+            ),
+            java(
+                """
+                    package test.pkg;
+
+                    public class MyClass implements MyInterface {
+                        @Deprecated
+                        @Override
+                        public void inheritedNoComment() {}
+
+                        /** @deprecated Use XYZ instead. */
+                        @Deprecated
+                        @Override
+                        public void inheritedWithComment() {}
+
+                        /** {@inheritDoc} */
+                        @Deprecated
+                        @Override
+                        public void inheritedWithInheritDoc() {}
+                    }
+                """
+            )
+        )
+        check(*input).expectClean()
+    }
+}
diff --git a/lint-checks/src/test/java/androidx/build/lint/Stubs.kt b/lint-checks/src/test/java/androidx/build/lint/Stubs.kt
index 108e776..08b9ba1 100644
--- a/lint-checks/src/test/java/androidx/build/lint/Stubs.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/Stubs.kt
@@ -237,6 +237,7 @@
         LIBRARY,
         LIBRARY_GROUP,
         LIBRARY_GROUP_PREFIX,
+        /** @deprecated Use {@link #LIBRARY_GROUP_PREFIX} instead */
         @Deprecated
         GROUP_ID,
         TESTS,