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 */