blob: cc0809a4a3b9c37077d7b1f36ee452feab791071 [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.client.api.AnnotationLookup
import com.android.tools.lint.client.api.JavaEvaluator
import com.android.tools.lint.detector.api.AnnotationUsageType
import com.android.tools.lint.detector.api.AnnotationUsageType.FIELD_REFERENCE
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.JavaPsiFacade
import com.intellij.psi.PsiAnnotation
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiClassType
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiField
import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiModifier
import com.intellij.psi.PsiModifierListOwner
import com.intellij.psi.impl.source.PsiClassReferenceType
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.util.PsiTypesUtil
import org.jetbrains.kotlin.util.capitalizeDecapitalize.capitalizeAsciiOnly
import org.jetbrains.kotlin.util.capitalizeDecapitalize.toLowerCaseAsciiOnly
import org.jetbrains.uast.UAnnotated
import org.jetbrains.uast.UAnnotation
import org.jetbrains.uast.UArrayAccessExpression
import org.jetbrains.uast.UBinaryExpression
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UCallableReferenceExpression
import org.jetbrains.uast.UClass
import org.jetbrains.uast.UClassLiteralExpression
import org.jetbrains.uast.UElement
import org.jetbrains.uast.UEnumConstant
import org.jetbrains.uast.UExpression
import org.jetbrains.uast.ULambdaExpression
import org.jetbrains.uast.UMethod
import org.jetbrains.uast.UReferenceExpression
import org.jetbrains.uast.USimpleNameReferenceExpression
import org.jetbrains.uast.UVariable
import org.jetbrains.uast.java.JavaUAnnotation
import org.jetbrains.uast.toUElement
import org.jetbrains.uast.tryResolve
import java.util.ArrayList
class ExperimentalDetector : Detector(), SourceCodeScanner {
private val visitedUsages: MutableMap<UElement, MutableSet<String>> = mutableMapOf()
override fun applicableAnnotations(): List<String> = listOf(
JAVA_EXPERIMENTAL_ANNOTATION,
KOTLIN_EXPERIMENTAL_ANNOTATION,
JAVA_REQUIRES_OPT_IN_ANNOTATION,
KOTLIN_REQUIRES_OPT_IN_ANNOTATION
)
override fun applicableSuperClasses(): List<String> = listOf(
"java.lang.Object"
)
override fun visitClass(
context: JavaContext,
lambda: ULambdaExpression,
) {
// Infer the overridden method by taking the first (and only) abstract method from the
// functional interface being implemented.
val superClass = (lambda.functionalInterfaceType as? PsiClassReferenceType)?.resolve()
val superMethod = superClass?.allMethods
?.first { method -> method.isAbstract() }
?.toUElement()
if (superMethod is UMethod) {
checkMethodOverride(context, lambda, superMethod)
}
}
override fun visitClass(
context: JavaContext,
declaration: UClass,
) {
declaration.methods.forEach { method ->
val eval = context.evaluator
if (eval.isOverride(method, true)) {
method.findSuperMethods().forEach { superMethod ->
checkMethodOverride(context, method, superMethod)
}
}
}
}
/**
* Extract the relevant annotations from the method override and run the checks
* on the annotations.
*
* Based on Lint's `AnnotationHandler.checkCall)()`.
*/
private fun checkMethodOverride(
context: JavaContext,
usage: UElement,
superMethod: PsiMethod,
) {
val evaluator = context.evaluator
val allAnnotations = evaluator.getAllAnnotations(superMethod, inHierarchy = true)
val methodAnnotations = filterRelevantAnnotations(
evaluator, allAnnotations, usage,
)
// Look for annotations on the class as well: these trickle
// down to all the methods in the class
val containingClass: PsiClass? = superMethod.containingClass
val (classAnnotations, pkgAnnotations) = getClassAndPkgAnnotations(
containingClass, evaluator, usage
)
doCheckMethodOverride(
context,
superMethod,
methodAnnotations,
classAnnotations,
pkgAnnotations,
usage,
containingClass,
)
}
/**
* Do the checks of a method override based on the method, class, and package
* annotations given.
*
* Based on Lint's `AnnotationHandler.doCheckCall()`.
*/
private fun doCheckMethodOverride(
context: JavaContext,
superMethod: PsiMethod?,
methodAnnotations: List<UAnnotation>,
classAnnotations: List<UAnnotation>,
pkgAnnotations: List<UAnnotation>,
usage: UElement,
containingClass: PsiClass?,
) {
if (methodAnnotations.isNotEmpty()) {
checkAnnotations(
context,
argument = usage,
type = AnnotationUsageType.METHOD_CALL,
method = superMethod,
referenced = superMethod,
annotations = methodAnnotations,
allMethodAnnotations = methodAnnotations,
allClassAnnotations = classAnnotations,
packageAnnotations = pkgAnnotations,
annotated = superMethod,
)
}
if (containingClass != null && classAnnotations.isNotEmpty()) {
checkAnnotations(
context,
argument = usage,
type = AnnotationUsageType.METHOD_CALL_CLASS,
method = superMethod,
referenced = superMethod,
annotations = classAnnotations,
allMethodAnnotations = methodAnnotations,
allClassAnnotations = classAnnotations,
packageAnnotations = pkgAnnotations,
annotated = containingClass,
)
}
if (pkgAnnotations.isNotEmpty()) {
checkAnnotations(
context,
argument = usage,
type = AnnotationUsageType.METHOD_CALL_PACKAGE,
method = superMethod,
referenced = superMethod,
annotations = pkgAnnotations,
allMethodAnnotations = methodAnnotations,
allClassAnnotations = classAnnotations,
packageAnnotations = pkgAnnotations,
annotated = null,
)
}
}
/**
* Copied from Lint's `AnnotationHandler.checkAnnotations()` with modifications to operate on
* this detector only, rather than a list of scanners.
*/
private fun checkAnnotations(
context: JavaContext,
argument: UElement,
type: AnnotationUsageType,
method: PsiMethod?,
referenced: PsiElement?,
annotations: List<UAnnotation>,
allMethodAnnotations: List<UAnnotation> = emptyList(),
allClassAnnotations: List<UAnnotation> = emptyList(),
packageAnnotations: List<UAnnotation> = emptyList(),
annotated: PsiElement?
) {
for (annotation in annotations) {
val signature = annotation.qualifiedName ?: continue
var uAnnotations: List<UAnnotation>? = null
var psiAnnotations: Array<out PsiAnnotation>? = null
// Modification: Removed loop over uastScanners list.
if (isApplicableAnnotationUsage(type)) {
// Some annotations should not be treated as inherited though
// the hierarchy: if that's the case for this annotation in
// this scanner, check whether it's inherited and if so, skip it
if (annotated != null && !inheritAnnotation(signature)) {
// First try to look by directly checking the owner element of
// the annotation.
val annotationOwner = (annotation.sourcePsi as? PsiAnnotation)?.owner
val owner =
if (annotationOwner is PsiElement) {
PsiTreeUtil.getParentOfType(
annotationOwner,
PsiModifierListOwner::class.java
)
} else {
null
}
if (owner != null) {
val annotatedPsi = (annotated as? UElement)?.sourcePsi ?: annotated
if (owner != annotatedPsi) {
continue
}
} else {
// Figure out if this is an inherited annotation: it would be
// if it's not annotated on the element
if (annotated is UAnnotated) {
var found = false
for (
uAnnotation in uAnnotations ?: run {
val list = context.evaluator.getAllAnnotations(
annotated,
inHierarchy = false
)
uAnnotations = list
list
}
) {
val qualifiedName = uAnnotation.qualifiedName
if (qualifiedName == signature) {
found = true
break
}
}
if (!found) {
continue
}
}
if (annotated is PsiModifierListOwner) {
var found = false
for (
psiAnnotation in psiAnnotations ?: run {
val array =
context.evaluator.getAllAnnotations(annotated, false)
psiAnnotations = array
array
}
) {
val qualifiedName = psiAnnotation.qualifiedName
if (qualifiedName == signature) {
found = true
break
}
}
if (!found) {
continue
}
}
}
}
visitAnnotationUsage(
context, argument, type, annotation,
signature, method, referenced, annotations, allMethodAnnotations,
allClassAnnotations, packageAnnotations
)
}
}
}
/**
* Copied from Lint's `AnnotationHandler`.
*/
private fun getClassAndPkgAnnotations(
containingClass: PsiClass?,
evaluator: JavaEvaluator,
context: UElement,
): Pair<List<UAnnotation>, List<UAnnotation>> {
// Yes, returning a pair is ugly. But we are initializing two lists, and splitting this
// into two methods takes more lines of code then it saves over copying this block into
// two methods.
// Plus, destructuring assignment makes using the results less verbose.
val classAnnotations: List<UAnnotation>
val pkgAnnotations: List<UAnnotation>
if (containingClass != null) {
val annotations = evaluator.getAllAnnotations(containingClass, inHierarchy = true)
classAnnotations = filterRelevantAnnotations(evaluator, annotations, context)
val pkg = evaluator.getPackage(containingClass)
pkgAnnotations = if (pkg != null) {
val annotations2 = evaluator.getAllAnnotations(pkg, inHierarchy = false)
filterRelevantAnnotations(evaluator, annotations2)
} else {
emptyList()
}
} else {
classAnnotations = emptyList()
pkgAnnotations = emptyList()
}
return Pair(classAnnotations, pkgAnnotations)
}
/**
* Copied from Lint's `AnnotationHandler`.
*/
private fun filterRelevantAnnotations(
evaluator: JavaEvaluator,
annotations: Array<PsiAnnotation>,
context: UElement? = null
): List<UAnnotation> {
var result: MutableList<UAnnotation>? = null
val length = annotations.size
if (length == 0) {
return emptyList()
}
for (annotation in annotations) {
val signature = annotation.qualifiedName
if (signature == null ||
(
signature.startsWith("kotlin.") ||
signature.startsWith("java.")
) && !relevantAnnotations.contains(signature)
) {
// @Override, @SuppressWarnings etc. Ignore
continue
}
if (relevantAnnotations.contains(signature)) {
val uAnnotation = JavaUAnnotation.wrap(annotation)
// Common case: there's just one annotation; no need to create a list copy
if (length == 1) {
return listOf(uAnnotation)
}
if (result == null) {
result = ArrayList(2)
}
result.add(uAnnotation)
continue
}
// Special case @IntDef and @StringDef: These are used on annotations
// themselves. For example, you create a new annotation named @foo.bar.Baz,
// annotate it with @IntDef, and then use @foo.bar.Baz in your signatures.
// Here we want to map from @foo.bar.Baz to the corresponding int def.
// Don't need to compute this if performing @IntDef or @StringDef lookup
val cls = annotation.nameReferenceElement?.resolve() ?: run {
val project = annotation.project
JavaPsiFacade.getInstance(project).findClass(
signature,
GlobalSearchScope.projectScope(project)
)
} ?: continue
if (cls !is PsiClass || !cls.isAnnotationType) {
continue
}
val innerAnnotations = evaluator.getAllAnnotations(cls, inHierarchy = false)
for (j in innerAnnotations.indices) {
val inner = innerAnnotations[j]
val a = inner.qualifiedName
if (a != null && relevantAnnotations.contains(a)) {
if (result == null) {
result = ArrayList(2)
}
val innerU = annotationLookup.findRealAnnotation(inner, cls, context)
result.add(innerU)
}
}
}
return result ?: emptyList()
}
// Used for drop-in compatibility with code from Lint's `AnnotationHandler`.
private val relevantAnnotations: List<String>
get() = applicableAnnotations()
private val annotationLookup = AnnotationLookup()
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>
) {
// Are we visiting a Kotlin property as a field reference when it's actually a method?
// Ignore it, since we'll also visit it as a method.
if (isKotlin(usage.sourcePsi) && type == FIELD_REFERENCE && referenced is PsiMethod) {
return
}
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,
referenced,
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,
referenced,
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 optInFqNames fully-qualified class name for experimental opt-in annotation
*/
private fun checkExperimentalUsage(
context: JavaContext,
annotation: UAnnotation,
referenced: PsiElement?,
usage: UElement,
optInFqNames: List<String>
) {
val annotationFqName = (annotation.uastParent as? UClass)?.qualifiedName ?: return
// This method may get called multiple times when there is more than one instance of the
// annotation in the hierarchy. We don't care which one we're looking at, but we shouldn't
// report the same usage and annotation pair multiple times.
val visitedAnnotations = visitedUsages.getOrPut(usage, { mutableSetOf() })
if (!visitedAnnotations.add(annotationFqName)) {
return
}
// Check whether the usage actually considered experimental.
val decl = referenced.toUElement() ?: usage.getReferencedElement() ?: return
if (!decl.isExperimentalityRequired(context, annotationFqName)) {
return
}
// Check whether the usage is acceptable, either due to opt-in or propagation.
if (usage.isExperimentalityAccepted(context, annotationFqName, optInFqNames)) {
return
}
// For some reason we can't read the explicit default level from the compiled version
// of `kotlin.Experimental` (but we can from `kotlin.RequiresOptIn`... go figure). It's
// possible that we'll fail to read the level for other reasons, but the safest
// fallback is `ERROR` either way.
val level = annotation.extractAttribute(context, "level", "ERROR")
if (level != null) {
report(
context,
usage,
annotationFqName,
"This declaration is opt-in and its usage should be marked with " +
"`@$annotationFqName` or `@OptIn(markerClass = $annotationFqName.class)`",
level
)
} else {
// This is a more serious failure where we obtained a representation that we
// couldn't understand.
report(
context,
usage,
annotationFqName,
"Failed to read `level` from `@$annotationFqName` -- assuming `ERROR`. " +
"This declaration is opt-in and its usage should be marked with " +
"`@$annotationFqName` or `@OptIn(markerClass = $annotationFqName.class)`",
"ERROR"
)
}
}
/**
* Determines if the element is within scope of the experimental marker identified by
* [annotationFqName], thus whether it requires either opt-in or propagation of the marker.
*
* This is functionally equivalent to a containment check for [annotationFqName] on the result
* of the Kotlin compiler's implementation of `DeclarationDescriptor.loadExperimentalities()`
* within `ExperimentalUsageChecker`.
*/
private fun UElement.isExperimentalityRequired(
context: JavaContext,
annotationFqName: String,
): Boolean {
// Is the element itself experimental?
if (isDeclarationAnnotatedWith(annotationFqName)) {
return true
}
// Is a parent of the element experimental? Kotlin's implementation skips this check if
// the current element is a constructor method, but it's required when we're looking at
// the syntax tree through UAST. Unclear why.
if ((uastParent as? UClass)?.isExperimentalityRequired(context, annotationFqName) == true) {
return true
}
// Is the containing package experimental?
if (context.evaluator.getPackage(this)?.getAnnotation(annotationFqName) != null) {
return true
}
return false
}
/**
* Returns whether the element has accepted the scope of the experimental marker identified by
* [annotationFqName], either by opting-in via an annotation in [optInFqNames] or propagating
* the marker.
*
* This is functionally equivalent to the Kotlin compiler's implementation of
* `PsiElement.isExperimentalityAccepted()` within `ExperimentalUsageChecker`.
*/
private fun UElement.isExperimentalityAccepted(
context: JavaContext,
annotationFqName: String,
optInFqNames: List<String>,
): Boolean {
val config = context.configuration
return config.getOption(ISSUE_ERROR, "opt-in")?.contains(annotationFqName) == true ||
config.getOption(ISSUE_WARNING, "opt-in")?.contains(annotationFqName) == true ||
anyParentMatches({ element ->
element.isDeclarationAnnotatedWith(annotationFqName) ||
element.isDeclarationAnnotatedWithOptInOf(annotationFqName, optInFqNames)
})
}
/**
* Reports an issue and trims indentation on the [message].
*/
private fun report(
context: JavaContext,
usage: UElement,
annotation: String,
message: String,
level: String,
) {
val issue = when (level) {
ENUM_ERROR -> ISSUE_ERROR
ENUM_WARNING -> ISSUE_WARNING
else -> throw IllegalArgumentException(
"Level was \"$level\" but must be one of: $ENUM_ERROR, $ENUM_WARNING"
)
}
try {
if (context.configuration.getOption(issue, "opt-in")?.contains(annotation) != true) {
context.report(issue, usage, context.getNameLocation(usage), message.trimIndent())
}
} catch (e: UnsupportedOperationException) {
if ("Method not implemented" == e.message) {
// Workaround for b/191286558 where lint attempts to read annotations from a
// compiled UAST parent of `usage`. Swallow the exception and don't report anything.
} else {
throw e
}
}
}
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"
const val ENUM_ERROR = "ERROR"
const val ENUM_WARNING = "WARNING"
private fun issueForLevel(
levelEnum: String,
severity: Severity,
): Issue {
val levelText = levelEnum.toLowerCaseAsciiOnly()
val issueId = "UnsafeOptInUsage${levelText.capitalizeAsciiOnly()}"
return Issue.create(
id = issueId,
briefDescription = "Unsafe opt-in usage intended to be $levelText-level severity",
explanation = """
This API has been flagged as opt-in with $levelText-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 by using the `@OptIn` annotation, using the marker annotation --
effectively causing further propagation of the opt-in aspect -- or configuring
the `$issueId` check's options for project-wide opt-in.
To configure project-wide opt-in, specify the `opt-in` option value in `lint.xml`
as a comma-delimited list of opted-in annotations:
```
<lint>
<issue id="$issueId">
<option name="opt-in" value="com.foo.ExperimentalBarAnnotation" />
</issue>
</lint>
```
""",
category = Category.CORRECTNESS,
priority = 4,
severity = severity,
implementation = IMPLEMENTATION,
)
}
val ISSUE_ERROR = issueForLevel(ENUM_ERROR, Severity.ERROR)
val ISSUE_WARNING = issueForLevel(ENUM_WARNING, Severity.WARNING)
val ISSUES = listOf(
ISSUE_ERROR,
ISSUE_WARNING,
)
}
}
private fun UAnnotation.hasMatchingAttributeValueClass(
attributeName: String,
className: String
): Boolean {
val attributeValue = findDeclaredAttributeValue(attributeName)
if (attributeValue.getFullyQualifiedName() == className) {
return true
}
if (attributeValue is UCallExpression) {
return attributeValue.valueArguments.any { attrValue ->
attrValue.getFullyQualifiedName() == className
}
}
return false
}
/**
* Returns the fully-qualified class name for a given attribute value, if any.
*/
private fun UExpression?.getFullyQualifiedName(): String? {
val type = if (this is UClassLiteralExpression) this.type else this?.evaluate()
return (type as? PsiClassType)?.canonicalText
}
private fun UElement?.getReferencedElement(): UElement? =
when (this) {
is UBinaryExpression ->
leftOperand.tryResolve() // or referenced
is UMethod ->
this // or referenced
is UClass ->
uastSuperTypes.firstNotNullOfOrNull {
PsiTypesUtil.getPsiClass(it.type)
} // or referenced
is USimpleNameReferenceExpression ->
resolve().let { field -> field as? PsiField ?: field as? PsiMethod } // or referenced
is UCallExpression ->
resolve() ?: classReference?.resolve() // referenced is empty for constructor
is UCallableReferenceExpression ->
resolve() as? PsiMethod // or referenced
is UAnnotation ->
null
is UEnumConstant ->
resolveMethod() // or referenced
is UArrayAccessExpression ->
(receiver as? UReferenceExpression)?.resolve() // or referenced
is UVariable ->
this
else ->
null
}.toUElement()
/**
* Tests each parent in the elements hierarchy including the element itself. Returns `true` for the
* first element where [positivePredicate] matches or `false` for the first element where
* [negativePredicate] returns matches. If neither predicate is matched, returns [defaultValue].
*/
private inline fun UElement.anyParentMatches(
positivePredicate: (element: UElement) -> Boolean,
negativePredicate: (element: UElement) -> Boolean = { false },
defaultValue: Boolean = false
): Boolean {
var element = this
while (true) {
if (positivePredicate(element)) return true
if (negativePredicate(element)) return false
element = element.uastParent ?: return defaultValue
}
}
/**
* Returns whether the element declaration is annotated with the specified annotation.
*/
private fun UElement.isDeclarationAnnotatedWith(
annotationFqName: String,
) = (this as? UAnnotated)?.findAnnotation(annotationFqName) != null
/**
* Returns whether the element declaration is annotated with any of the specified opt-in
* annotations where the value of `markerClass` contains the specified annotation.
*/
private fun UElement.isDeclarationAnnotatedWithOptInOf(
annotationFqName: String,
optInFqNames: List<String>,
) = (this as? UAnnotated)?.let { annotated ->
optInFqNames.any { optInFqName ->
annotated.findAnnotation(optInFqName)?.hasMatchingAttributeValueClass(
"markerClass",
annotationFqName,
) == true
}
} == true
private fun PsiModifierListOwner.isAbstract(): Boolean =
modifierList?.hasModifierProperty(PsiModifier.ABSTRACT) == true