[go: nahoru, domu]

blob: dc4996c60b7c91d186122aacb61913d91bccbe0d [file] [log] [blame]
/*
* Copyright 2019 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", "SyntheticAccessor")
package androidx.annotation.experimental.lint
import com.android.tools.lint.detector.api.AnnotationUsageType
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.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 com.android.tools.lint.detector.api.SourceCodeScanner
import com.android.tools.lint.detector.api.isKotlin
import com.intellij.psi.PsiClassType
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiField
import com.intellij.psi.PsiMethod
import org.jetbrains.uast.UAnnotated
import org.jetbrains.uast.UAnnotation
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UClass
import org.jetbrains.uast.UClassLiteralExpression
import org.jetbrains.uast.UElement
import org.jetbrains.uast.UExpression
import org.jetbrains.uast.UReferenceExpression
import org.jetbrains.uast.getParentOfType
import java.util.Locale
class ExperimentalDetector : Detector(), SourceCodeScanner {
override fun applicableAnnotations(): List<String> = listOf(
JAVA_EXPERIMENTAL_ANNOTATION,
KOTLIN_EXPERIMENTAL_ANNOTATION,
JAVA_REQUIRES_OPT_IN_ANNOTATION,
KOTLIN_REQUIRES_OPT_IN_ANNOTATION
)
override fun visitAnnotationUsage(
context: JavaContext,
usage: UElement,
type: AnnotationUsageType,
annotation: UAnnotation,
qualifiedName: String,
method: PsiMethod?,
referenced: PsiElement?,
annotations: List<UAnnotation>,
allMemberAnnotations: List<UAnnotation>,
allClassAnnotations: List<UAnnotation>,
allPackageAnnotations: List<UAnnotation>
) {
when (qualifiedName) {
JAVA_EXPERIMENTAL_ANNOTATION, JAVA_REQUIRES_OPT_IN_ANNOTATION -> {
// Only allow Java annotations, since the Kotlin compiler doesn't understand our
// annotations and could get confused when it's trying to opt-in to some random
// annotation that it doesn't understand.
checkExperimentalUsage(
context, annotation, usage,
listOf(
JAVA_USE_EXPERIMENTAL_ANNOTATION,
JAVA_OPT_IN_ANNOTATION
)
)
}
KOTLIN_EXPERIMENTAL_ANNOTATION, KOTLIN_REQUIRES_OPT_IN_ANNOTATION -> {
// Don't check usages of Kotlin annotations from Kotlin sources, since the Kotlin
// compiler handles that already. Allow either Java or Kotlin annotations, since
// we can enforce both and it's possible that a Kotlin-sourced experimental library
// is being used from Java without the Kotlin stdlib in the classpath.
if (!isKotlin(usage.sourcePsi)) {
checkExperimentalUsage(
context, annotation, usage,
listOf(
KOTLIN_USE_EXPERIMENTAL_ANNOTATION,
KOTLIN_OPT_IN_ANNOTATION,
JAVA_USE_EXPERIMENTAL_ANNOTATION,
JAVA_OPT_IN_ANNOTATION
)
)
}
}
}
}
/**
* Check whether the given experimental API [annotation] can be referenced from [usage] call
* site.
*
* @param context the lint scanning context
* @param annotation the experimental opt-in annotation detected on the referenced element
* @param usage the element whose usage should be checked
* @param useAnnotationNames fully-qualified class name for experimental opt-in annotation
*/
private fun checkExperimentalUsage(
context: JavaContext,
annotation: UAnnotation,
usage: UElement,
useAnnotationNames: List<String>
) {
val useAnnotation = (annotation.uastParent as? UClass)?.qualifiedName ?: return
if (!hasOrUsesAnnotation(context, usage, useAnnotation, useAnnotationNames)) {
val level = extractAttribute(annotation, "level")
if (level != null) {
report(
context, usage,
"""
This declaration is opt-in and its usage should be marked with
'@$useAnnotation' or '@OptIn(markerClass = $useAnnotation.class)'
""",
level
)
} else {
report(
context, annotation,
"""
Failed to extract attribute "level" from annotation
""",
"ERROR"
)
}
}
}
@Suppress("SameParameterValue")
private fun extractAttribute(annotation: UAnnotation, name: String): String? {
// Using findAttributeValue instead of findDeclaredAttributeValue allows default values.
return annotation.findAttributeValue(name)?.let { expression ->
((expression as? UReferenceExpression)?.resolve() as? PsiField)?.name
}
}
/**
* Check whether the specified [usage] is either within the scope of [annotationName] or an
* explicit opt-in via a [useAnnotationNames] annotation.
*/
private fun hasOrUsesAnnotation(
context: JavaContext,
usage: UElement,
annotationName: String,
useAnnotationNames: List<String>
): Boolean {
var element: UAnnotated? = if (usage is UAnnotated) {
usage
} else {
usage.getParentOfType(UAnnotated::class.java)
}
while (element != null) {
val annotations = context.evaluator.getAllAnnotations(element, false)
val matchName = annotations.any { annotationName == it.qualifiedName }
if (matchName) {
return true
}
val matchUse = annotations.any { annotation ->
val qualifiedName = annotation.qualifiedName
if (qualifiedName != null && useAnnotationNames.contains(qualifiedName)) {
// Kotlin uses the same attribute for single- and multiple-marker usages.
if (annotation.hasMatchingAttributeValueClass(
context, "markerClass", annotationName
)
) {
return@any true
}
}
return@any false
}
if (matchUse) {
return true
}
element = element.getParentOfType(UAnnotated::class.java)
}
return false
}
/**
* Reports an issue and trims indentation on the [message].
*/
private fun report(
context: JavaContext,
usage: UElement,
message: String,
level: String
) {
val issue = when (level) {
"ERROR" -> ISSUE_ERROR
"WARNING" -> ISSUE_WARNING
else -> throw IllegalArgumentException(
"Level was \"" + level + "\" but must be one " +
"of: ERROR, WARNING"
)
}
context.report(issue, usage, context.getNameLocation(usage), message.trimIndent())
}
companion object {
private val IMPLEMENTATION = Implementation(
ExperimentalDetector::class.java,
Scope.JAVA_FILE_SCOPE
)
const val KOTLIN_EXPERIMENTAL_ANNOTATION = "kotlin.Experimental"
const val KOTLIN_USE_EXPERIMENTAL_ANNOTATION = "kotlin.UseExperimental"
const val KOTLIN_OPT_IN_ANNOTATION = "kotlin.OptIn"
const val KOTLIN_REQUIRES_OPT_IN_ANNOTATION = "kotlin.RequiresOptIn"
const val JAVA_EXPERIMENTAL_ANNOTATION =
"androidx.annotation.experimental.Experimental"
const val JAVA_USE_EXPERIMENTAL_ANNOTATION =
"androidx.annotation.experimental.UseExperimental"
const val JAVA_REQUIRES_OPT_IN_ANNOTATION =
"androidx.annotation.RequiresOptIn"
const val JAVA_OPT_IN_ANNOTATION =
"androidx.annotation.OptIn"
@Suppress("DefaultLocale")
private fun issueForLevel(level: String, severity: Severity): Issue = Issue.create(
id = "UnsafeOptInUsage${level.replaceFirstChar {
if (it.isLowerCase()) it.titlecase(
Locale.getDefault()
) else it.toString()
}}",
briefDescription = "Unsafe opt-in usage intended to be $level-level severity",
explanation = """
This API has been flagged as opt-in with $level-level severity.
Any declaration annotated with this marker is considered part of an unstable or
otherwise non-standard API surface and its call sites should accept the opt-in
aspect of it either by using `@OptIn` or by being annotated with that marker
themselves, effectively causing further propagation of the opt-in aspect.
""",
category = Category.CORRECTNESS,
priority = 4,
severity = severity,
implementation = IMPLEMENTATION
)
val ISSUE_ERROR =
issueForLevel(
"error",
Severity.ERROR
)
val ISSUE_WARNING =
issueForLevel(
"warning",
Severity.WARNING
)
val ISSUES = listOf(
ISSUE_ERROR,
ISSUE_WARNING
)
}
}
private fun UAnnotation.hasMatchingAttributeValueClass(
context: JavaContext,
attributeName: String,
className: String
): Boolean {
val attributeValue = findDeclaredAttributeValue(attributeName)
if (attributeValue.getFullyQualifiedName(context) == className) {
return true
}
if (attributeValue is UCallExpression) {
return attributeValue.valueArguments.any { attrValue ->
attrValue.getFullyQualifiedName(context) == className
}
}
return false
}
/**
* Returns the fully-qualified class name for a given attribute value, if any.
*/
private fun UExpression?.getFullyQualifiedName(context: JavaContext): String? {
val type = if (this is UClassLiteralExpression) this.type else this?.evaluate()
return (type as? PsiClassType)?.let { context.evaluator.getQualifiedName(it) }
}