blob: 37e2930dfa8b78349302e742e7c6eaa8ceb589a8 [file] [log] [blame]
/*
* Copyright 2020 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.
*/
@file:Suppress("UnstableApiUsage")
package androidx.build.lint
import com.android.tools.lint.client.api.Configuration
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.Scope
import com.android.tools.lint.detector.api.Severity
import java.io.File
import java.io.FileNotFoundException
import org.jetbrains.uast.UAnnotated
import org.jetbrains.uast.UAnnotation
import org.jetbrains.uast.UClass
import org.jetbrains.uast.UElement
import org.jetbrains.uast.resolveToUElement
/**
* Prevents usage of experimental annotations outside the groups in which they were defined.
*/
class BanInappropriateExperimentalUsage : Detector(), Detector.UastScanner {
override fun getApplicableUastTypes() = listOf(UAnnotation::class.java)
override fun createUastHandler(context: JavaContext): UElementHandler {
return AnnotationChecker(context)
}
private inner class AnnotationChecker(val context: JavaContext) : UElementHandler() {
val atomicGroupList: List<String> by lazy {
loadAtomicLibraryGroupData(context.configuration, ISSUE)
}
override fun visitAnnotation(node: UAnnotation) {
if (DEBUG) {
if (APPLICABLE_ANNOTATIONS.contains(node.qualifiedName) && node.sourcePsi != null) {
(node.uastParent as? UClass)?.let { annotation ->
println(
"${context.driver.mode}: declared ${annotation.qualifiedName} in " +
"${context.project}"
)
}
}
}
// If we find an usage of an experimentally-declared annotation, check it.
val annotation = node.resolveToUElement()
if (annotation is UAnnotated) {
val annotations = context.evaluator.getAllAnnotations(annotation, false)
if (annotations.any { APPLICABLE_ANNOTATIONS.contains(it.qualifiedName) }) {
if (DEBUG) {
println(
"${context.driver.mode}: used ${node.qualifiedName} in " +
"${context.project}"
)
}
verifyUsageOfElementIsWithinSameGroup(
context,
node,
annotation,
ISSUE,
atomicGroupList
)
}
}
}
private fun loadAtomicLibraryGroupData(
configuration: Configuration,
issue: Issue
): List<String> {
val filename = configuration.getOption(issue, ATOMIC_LIBGROUP_FILE_PROPERTY, null)
?: throw RuntimeException(
"Property $ATOMIC_LIBGROUP_FILE_PROPERTY is not set in lint.xml.")
val libGroupFile = if (filename.contains(OUT_DIR_PLACEHOLDER)) {
val fileLocation = filename.replace(OUT_DIR_PLACEHOLDER, System.getenv("OUT_DIR"))
val file = File(fileLocation)
if (!file.exists()) {
throw FileNotFoundException("Couldn't find atomic library group file $filename")
}
file
} else {
configuration.getOptionAsFile(issue, ATOMIC_LIBGROUP_FILE_PROPERTY, null)
?: throw FileNotFoundException(
"Couldn't find atomic library group file $filename")
}
val atomicLibraryGroups = libGroupFile.readLines()
if (atomicLibraryGroups.isEmpty()) {
throw RuntimeException("Atomic library group file should not be empty")
}
return atomicLibraryGroups
}
}
@Suppress("UNUSED_PARAMETER") // TODO: write logic + tests in future CL that uses groupList
fun verifyUsageOfElementIsWithinSameGroup(
context: JavaContext,
usage: UElement,
annotation: UElement,
issue: Issue,
atomicGroupList: List<String>,
) {
val evaluator = context.evaluator
val usageCoordinates = evaluator.getLibrary(usage) ?: context.project.mavenCoordinate
val usageGroupId = usageCoordinates?.groupId
val annotationGroupId = evaluator.getLibrary(annotation)?.groupId
if (annotationGroupId != usageGroupId && annotationGroupId != null) {
if (DEBUG) {
println(
"${context.driver.mode}: report usage of $annotationGroupId in $usageGroupId"
)
}
Incident(context)
.issue(issue)
.at(usage)
.message(
"`Experimental` and `RequiresOptIn` APIs may only be used within the " +
"same-version group where they were defined."
)
.report()
}
}
companion object {
private const val DEBUG = false
/**
* This string must match the value defined in buildSrc/lint.xml
*
* This is needed as we need to define the location of the atomic library group file for
* non-test usage. For tests, we can directly use the value defined in test lint.xml files.
*/
private const val OUT_DIR_PLACEHOLDER = "USE_SYSTEM_OUT_DIR"
// This must match the setting in buildSrc/lint.xml
private const val ATOMIC_LIBGROUP_FILE_PROPERTY = "atomicLibraryGroupFilename"
/**
* Even though Kotlin's [Experimental] annotation is deprecated in favor of [RequiresOptIn],
* we still want to check for its use in Lint.
*/
private const val KOTLIN_EXPERIMENTAL_ANNOTATION = "kotlin.Experimental"
private const val KOTLIN_REQUIRES_OPT_IN_ANNOTATION = "kotlin.RequiresOptIn"
private const val JAVA_EXPERIMENTAL_ANNOTATION =
"androidx.annotation.experimental.Experimental"
private const val JAVA_REQUIRES_OPT_IN_ANNOTATION =
"androidx.annotation.RequiresOptIn"
private val APPLICABLE_ANNOTATIONS = listOf(
JAVA_EXPERIMENTAL_ANNOTATION,
KOTLIN_EXPERIMENTAL_ANNOTATION,
JAVA_REQUIRES_OPT_IN_ANNOTATION,
KOTLIN_REQUIRES_OPT_IN_ANNOTATION,
)
val ISSUE = Issue.create(
id = "IllegalExperimentalApiUsage",
briefDescription = "Using experimental API from separately versioned library",
explanation = "Annotations meta-annotated with `@RequiresOptIn` or `@Experimental` " +
"may only be referenced from within the same-version group in which they were " +
"defined.",
category = Category.CORRECTNESS,
priority = 5,
severity = Severity.ERROR,
implementation = Implementation(
BanInappropriateExperimentalUsage::class.java,
Scope.JAVA_FILE_SCOPE,
),
)
}
}