Process @OptIn's markerClass for lint checks

Evaluate the annotation(s) declared in OptIn's markerClass and report on invalid experimental API usage.

Bug: 201564937
Test: BanInappropriateExperimentalUsageTest
Test: presubmit
Change-Id: Ia6f7fad4ba35fc867e293d8432251602b4027dd5
diff --git a/lint-checks/integration-tests/src/main/java/androidx/sample/consumer/OutsideGroupExperimentalAnnotatedClass.kt b/lint-checks/integration-tests/src/main/java/androidx/sample/consumer/OutsideGroupExperimentalAnnotatedClass.kt
index d6c1fee..278b5eb 100644
--- a/lint-checks/integration-tests/src/main/java/androidx/sample/consumer/OutsideGroupExperimentalAnnotatedClass.kt
+++ b/lint-checks/integration-tests/src/main/java/androidx/sample/consumer/OutsideGroupExperimentalAnnotatedClass.kt
@@ -20,6 +20,7 @@
 
 import sample.annotation.provider.ExperimentalSampleAnnotationJava
 import sample.annotation.provider.RequiresOptInSampleAnnotationJava
+import sample.annotation.provider.RequiresOptInSampleAnnotationJavaDuplicate
 
 class OutsideGroupExperimentalAnnotatedClass {
 
@@ -38,4 +39,24 @@
     fun invalidRequiresOptInAnnotatedMethod() {
         // Nothing to see here.
     }
+
+    @OptIn(RequiresOptInSampleAnnotationJava::class)
+    fun invalidMethodWithSingleOptIn() {
+        // Nothing to see here.
+    }
+
+    @OptIn(
+        RequiresOptInSampleAnnotationJava::class,
+        RequiresOptInSampleAnnotationJavaDuplicate::class
+    )
+    fun invalidMethodWithMultipleOptInsWithLineBreaks() {
+        // Nothing to see here.
+    }
+
+    /* ktlint-disable max-line-length */
+    @OptIn(RequiresOptInSampleAnnotationJava::class, RequiresOptInSampleAnnotationJavaDuplicate::class)
+    fun invalidMethodWithMultipleOptInsWithoutLineBreaks() {
+        // Nothing to see here.
+    }
+    /* ktlint-enable max-line-length */
 }
diff --git a/lint-checks/integration-tests/src/main/java/sample/annotation/provider/RequiresOptInSampleAnnotationJavaDuplicate.java b/lint-checks/integration-tests/src/main/java/sample/annotation/provider/RequiresOptInSampleAnnotationJavaDuplicate.java
new file mode 100644
index 0000000..3d7b8f3
--- /dev/null
+++ b/lint-checks/integration-tests/src/main/java/sample/annotation/provider/RequiresOptInSampleAnnotationJavaDuplicate.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2021 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 sample.annotation.provider;
+
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import kotlin.RequiresOptIn;
+
+// This is essentially a duplicate of RequiresOptInSampleAnnotationJava. Combined, these two are
+// used in @OptIn with multiple @RequiresOptIn declarations.
+@RequiresOptIn
+@Retention(CLASS)
+@Target({ElementType.TYPE, ElementType.METHOD})
+public @interface RequiresOptInSampleAnnotationJavaDuplicate {}
diff --git a/lint-checks/src/main/java/androidx/build/lint/BanInappropriateExperimentalUsage.kt b/lint-checks/src/main/java/androidx/build/lint/BanInappropriateExperimentalUsage.kt
index a09f735..2c9da29 100644
--- a/lint-checks/src/main/java/androidx/build/lint/BanInappropriateExperimentalUsage.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/BanInappropriateExperimentalUsage.kt
@@ -31,8 +31,12 @@
 import org.jetbrains.uast.UAnnotated
 import org.jetbrains.uast.UAnnotation
 import org.jetbrains.uast.UClass
+import org.jetbrains.uast.UClassLiteralExpression
 import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UExpression
+import org.jetbrains.uast.kotlin.KotlinUVarargExpression
 import org.jetbrains.uast.resolveToUElement
+import org.jetbrains.uast.toUElement
 
 /**
  * Prevents usage of experimental annotations outside the groups in which they were defined.
@@ -49,8 +53,10 @@
         val atomicGroupList: List<String> by lazy { loadAtomicLibraryGroupList() }
 
         override fun visitAnnotation(node: UAnnotation) {
+            val signature = node.qualifiedName
+
             if (DEBUG) {
-                if (APPLICABLE_ANNOTATIONS.contains(node.qualifiedName) && node.sourcePsi != null) {
+                if (APPLICABLE_ANNOTATIONS.contains(signature) && node.sourcePsi != null) {
                     (node.uastParent as? UClass)?.let { annotation ->
                         println(
                             "${context.driver.mode}: declared ${annotation.qualifiedName} in " +
@@ -60,8 +66,65 @@
                 }
             }
 
-            // If we find an usage of an experimentally-declared annotation, check it.
-            val annotation = node.resolveToUElement()
+            /**
+             * If the annotation under evaluation is [kotlin.OptIn], extract and evaluate the
+             * annotation(s) referenced by @OptIn - denoted by [kotlin.OptIn.markerClass].
+             */
+            if (signature != null && signature == KOTLIN_OPT_IN_ANNOTATION) {
+                if (DEBUG) {
+                    println("Processing $KOTLIN_OPT_IN_ANNOTATION annotation")
+                }
+
+                val markerClass: UExpression? = node.findAttributeValue("markerClass")
+                if (markerClass != null) {
+                    getUElementsFromOptInMarkerClass(markerClass).forEach { uElement ->
+                        inspectAnnotation(uElement, node)
+                    }
+                }
+
+                /**
+                 * [kotlin.OptIn] has no effect if [kotlin.OptIn.markerClass] isn't provided.
+                 * Similarly, if [getUElementsFromOptInMarkerClass] returns an empty list then
+                 * there isn't anything more to inspect.
+                 *
+                 * In both of these cases we can stop processing here.
+                 */
+                return
+            }
+
+            inspectAnnotation(node.resolveToUElement(), node)
+        }
+
+        private fun getUElementsFromOptInMarkerClass(markerClass: UExpression): List<UElement> {
+            val elements = ArrayList<UElement?>()
+
+            when (markerClass) {
+                is UClassLiteralExpression -> { // opting in to single annotation
+                    elements.add(markerClass.toUElement())
+                }
+                is KotlinUVarargExpression -> { // opting in to multiple annotations
+                    val expressions: List<UExpression> = markerClass.valueArguments
+                    for (expression in expressions) {
+                        val uElement = (expression as UClassLiteralExpression).toUElement()
+                        elements.add(uElement)
+                    }
+                }
+                else -> {
+                    // do nothing
+                }
+            }
+
+            return elements.filterNotNull()
+        }
+
+        private fun UClassLiteralExpression.toUElement(): UElement? {
+            val psiType = this.type
+            val psiClass = context.evaluator.getTypeClass(psiType)
+            return psiClass.toUElement()
+        }
+
+        // If we find an usage of an experimentally-declared annotation, check it.
+        private fun inspectAnnotation(annotation: UElement?, node: UAnnotation) {
             if (annotation is UAnnotated) {
                 val annotations = context.evaluator.getAllAnnotations(annotation, false)
                 if (annotations.any { APPLICABLE_ANNOTATIONS.contains(it.qualifiedName) }) {
@@ -140,6 +203,7 @@
          */
         private const val KOTLIN_EXPERIMENTAL_ANNOTATION = "kotlin.Experimental"
 
+        private const val KOTLIN_OPT_IN_ANNOTATION = "kotlin.OptIn"
         private const val KOTLIN_REQUIRES_OPT_IN_ANNOTATION = "kotlin.RequiresOptIn"
         private const val JAVA_EXPERIMENTAL_ANNOTATION =
             "androidx.annotation.experimental.Experimental"
@@ -171,4 +235,4 @@
             ),
         )
     }
-}
\ No newline at end of file
+}
diff --git a/lint-checks/src/test/java/androidx/build/lint/AbstractLintDetectorTest.kt b/lint-checks/src/test/java/androidx/build/lint/AbstractLintDetectorTest.kt
index da45139..0ed4dd8 100644
--- a/lint-checks/src/test/java/androidx/build/lint/AbstractLintDetectorTest.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/AbstractLintDetectorTest.kt
@@ -61,6 +61,7 @@
         return lint()
             .projects(*projectsWithStubs)
             .testModes(testModes)
+            .allowDuplicates()
             .run()
     }
 
diff --git a/lint-checks/src/test/java/androidx/build/lint/BanInappropriateExperimentalUsageTest.kt b/lint-checks/src/test/java/androidx/build/lint/BanInappropriateExperimentalUsageTest.kt
index caa51b3..59ea06a 100644
--- a/lint-checks/src/test/java/androidx/build/lint/BanInappropriateExperimentalUsageTest.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/BanInappropriateExperimentalUsageTest.kt
@@ -101,6 +101,7 @@
                 ktSample("sample.annotation.provider.ExperimentalSampleAnnotation"),
                 javaSample("sample.annotation.provider.ExperimentalSampleAnnotationJava"),
                 javaSample("sample.annotation.provider.RequiresOptInSampleAnnotationJava"),
+                javaSample("sample.annotation.provider.RequiresOptInSampleAnnotationJavaDuplicate"),
                 gradle(
                     """
                     apply plugin: 'com.android.library'
@@ -125,13 +126,22 @@
 
         /* ktlint-disable max-line-length */
         val expected = """
-../consumer/src/main/kotlin/androidx/sample/consumer/OutsideGroupExperimentalAnnotatedClass.kt:32: Error: Experimental and RequiresOptIn APIs may only be used within the same-version group where they were defined. [IllegalExperimentalApiUsage]
+../consumer/src/main/kotlin/androidx/sample/consumer/OutsideGroupExperimentalAnnotatedClass.kt:33: Error: Experimental and RequiresOptIn APIs may only be used within the same-version group where they were defined. [IllegalExperimentalApiUsage]
     @ExperimentalSampleAnnotationJava
     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-../consumer/src/main/kotlin/androidx/sample/consumer/OutsideGroupExperimentalAnnotatedClass.kt:37: Error: Experimental and RequiresOptIn APIs may only be used within the same-version group where they were defined. [IllegalExperimentalApiUsage]
+../consumer/src/main/kotlin/androidx/sample/consumer/OutsideGroupExperimentalAnnotatedClass.kt:38: Error: Experimental and RequiresOptIn APIs may only be used within the same-version group where they were defined. [IllegalExperimentalApiUsage]
     @RequiresOptInSampleAnnotationJava
     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-2 errors, 0 warnings
+../consumer/src/main/kotlin/androidx/sample/consumer/OutsideGroupExperimentalAnnotatedClass.kt:43: Error: Experimental and RequiresOptIn APIs may only be used within the same-version group where they were defined. [IllegalExperimentalApiUsage]
+    @OptIn(RequiresOptInSampleAnnotationJava::class)
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+../consumer/src/main/kotlin/androidx/sample/consumer/OutsideGroupExperimentalAnnotatedClass.kt:48: Error: Experimental and RequiresOptIn APIs may only be used within the same-version group where they were defined. [IllegalExperimentalApiUsage]
+    @OptIn(
+    ^
+../consumer/src/main/kotlin/androidx/sample/consumer/OutsideGroupExperimentalAnnotatedClass.kt:57: Error: Experimental and RequiresOptIn APIs may only be used within the same-version group where they were defined. [IllegalExperimentalApiUsage]
+    @OptIn(RequiresOptInSampleAnnotationJava::class, RequiresOptInSampleAnnotationJavaDuplicate::class)
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+5 errors, 0 warnings
         """.trimIndent()
         /* ktlint-enable max-line-length */