UnstableApiUsageInspection: check if signature of the referenced API contains an unstable type.

For example, if a method's parameter is marked with @ApiStatus.Experimental annotation, the method is effectively experimental. Invoking such a method from plugin code must be highlighted even if the method itself is not marked with the same annotation.

GitOrigin-RevId: 5ebd52b3a525a0d51aa2cbed5cb2603157e7f292
This commit is contained in:
Sergey Patrikeev
2019-06-25 19:41:31 +03:00
committed by intellij-monorepo-bot
parent 3d3e5ed2bc
commit ddc5ccd24c
8 changed files with 393 additions and 139 deletions

View File

@@ -7,6 +7,7 @@ jvm.inspections.unstable.api.usage.api.is.marked.unstable.itself=''{0}'' is mark
jvm.inspections.unstable.api.usage.api.is.declared.in.unstable.api=''{0}'' is declared in unstable {1} ''{2}''
jvm.inspections.unstable.api.usage.overridden.method.is.marked.unstable.itself=Overridden method ''{0}'' is marked unstable
jvm.inspections.unstable.api.usage.overridden.method.is.declared.in.unstable.api=Overridden method ''{0}'' is declared in unstable {1} ''{2}''
jvm.inspections.unstable.api.usage.unstable.type.is.used.in.signature.of.referenced.api=''{0}'' is unstable because its signature references unstable {1} ''{2}''
jvm.inspections.scheduled.for.removal.future.version=a future version
jvm.inspections.scheduled.for.removal.predefined.version=version {0}
@@ -14,6 +15,7 @@ jvm.inspections.scheduled.for.removal.api.is.marked.itself=''{0}'' is scheduled
jvm.inspections.scheduled.for.removal.api.is.declared.in.marked.api=''{0}'' is declared in {1} ''{2}'' scheduled for removal in {3}
jvm.inspections.scheduled.for.removal.method.overridden.marked.itself=Overridden method ''{0}'' is scheduled for removal in {1}
jvm.inspections.scheduled.for.removal.method.overridden.declared.in.marked.api=Overridden method ''{0}'' is declared in {1} ''{2}'' scheduled for removal in {3}
jvm.inspections.scheduled.for.removal.scheduled.for.removal.type.is.used.in.signature.of.referenced.api=''{0}'' is scheduled for removal because its signature references {1} ''{2}'' scheduled for removal in {3}
jvm.inspections.unstable.type.used.in.signature.display.name=Unstable type is used in signature
jvm.inspections.unstable.type.used.in.class.signature.description=Class must be marked with ''@{0}'' annotation because its declaration references unstable type ''{1}''

View File

@@ -0,0 +1,161 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInspection
import com.intellij.codeInsight.AnnotationUtil
import com.intellij.codeInspection.deprecation.DeprecationInspection
import com.intellij.lang.findUsages.LanguageFindUsages
import com.intellij.psi.*
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.uast.UClass
import org.jetbrains.uast.UDeclaration
import org.jetbrains.uast.UField
import org.jetbrains.uast.UMethod
/**
* Container of several values representing annotation search result.
*
* - [target] — starting API element of which annotation was searched.
* - [containingDeclaration] — annotated declaration containing [target]. May be equal [target] if the [target] is annotated itself.
* Otherwise it is the [target]'s containing class or package.
* - [psiAnnotation] — annotation with which [containingDeclaration] is marked.
*/
@ApiStatus.Internal
data class AnnotatedContainingDeclaration(
val target: PsiModifierListOwner,
val containingDeclaration: PsiModifierListOwner,
val psiAnnotation: PsiAnnotation
) {
val isOwnAnnotation: Boolean
get() = target == containingDeclaration
val targetName: String
get() = DeprecationInspection.getPresentableName(target)
val targetType: String
get() = LanguageFindUsages.getType(target)
val containingDeclarationName: String
get() = DeprecationInspection.getPresentableName(containingDeclaration)
val containingDeclarationType: String
get() = LanguageFindUsages.getType(containingDeclaration)
}
/**
* Utility functions for [UnstableApiUsageInspection] and [UnstableTypeUsedInSignatureInspection].
*/
@ApiStatus.Internal
internal object AnnotatedApiUsageUtil {
/**
* Searches for an annotation on a [target] or its enclosing declaration (containing class or package).
*
* If a [target] is marked with annotation, it is returned immediately.
* If containing class of [target] is marked with annotation, the class and its annotation is returned.
* If the package, to which the [target] belongs, is marked with annotation, the package and its annotation is returned.
*/
fun findAnnotatedContainingDeclaration(
target: PsiModifierListOwner,
annotationNames: Collection<String>,
includeExternalAnnotations: Boolean,
containingDeclaration: PsiModifierListOwner = target
): AnnotatedContainingDeclaration? {
val annotation = AnnotationUtil.findAnnotation(containingDeclaration, annotationNames, !includeExternalAnnotations)
if (annotation != null) {
return AnnotatedContainingDeclaration(target, containingDeclaration, annotation)
}
if (containingDeclaration is PsiMember) {
val containingClass = containingDeclaration.containingClass
if (containingClass != null) {
return findAnnotatedContainingDeclaration(target, annotationNames, includeExternalAnnotations, containingClass)
}
}
val packageName = (containingDeclaration.containingFile as? PsiClassOwner)?.packageName ?: return null
val psiPackage = JavaPsiFacade.getInstance(containingDeclaration.project).findPackage(packageName) ?: return null
return findAnnotatedContainingDeclaration(target, annotationNames, includeExternalAnnotations, psiPackage)
}
/**
* Searches for an annotated type that is part of the signature of the given declaration.
*
* * For classes, annotated type parameter is returned:
* * `class Foo<T extends Bar>` -> `Bar` is returned if it is annotated.
*
* * For methods, annotated return type, parameter type or type parameter is returned:
* * `public <T extends Baz> Foo method(Bar bar)` -> `Baz`, `Foo` or `Bar` is returned, whichever is annotated.
*
* * For fields, annotated field type is returned:
* * `public Foo field` -> `Foo` is returned if it is annotated.
*/
fun findAnnotatedTypeUsedInDeclarationSignature(
declaration: UDeclaration,
annotations: Collection<String>
): AnnotatedContainingDeclaration? {
when (declaration) {
is UClass -> {
return findAnnotatedTypeParameter(declaration.javaPsi, annotations)
}
is UMethod -> {
for (uastParameter in declaration.uastParameters) {
val annotatedParamType = findAnnotatedTypePart(uastParameter.type.deepComponentType, annotations)
if (annotatedParamType != null) {
return annotatedParamType
}
}
val returnType = declaration.returnType
if (returnType != null) {
val annotatedReturnType = findAnnotatedTypePart(returnType.deepComponentType, annotations)
if (annotatedReturnType != null) {
return annotatedReturnType
}
}
return findAnnotatedTypeParameter(declaration.javaPsi, annotations)
}
is UField -> {
return findAnnotatedTypePart(declaration.type.deepComponentType, annotations)
}
else -> return null
}
}
private fun findAnnotatedTypeParameter(
typeParameterListOwner: PsiTypeParameterListOwner,
annotations: Collection<String>
): AnnotatedContainingDeclaration? {
for (typeParameter in typeParameterListOwner.typeParameters) {
for (referencedType in typeParameter.extendsList.referencedTypes) {
val annotatedContainingDeclaration = findAnnotatedTypePart(referencedType.deepComponentType, annotations)
if (annotatedContainingDeclaration != null) {
return annotatedContainingDeclaration
}
}
}
return null
}
private fun findAnnotatedTypePart(
psiType: PsiType,
annotations: Collection<String>
): AnnotatedContainingDeclaration? {
if (psiType is PsiClassType) {
val psiClass = psiType.resolve()
if (psiClass != null) {
val containingDeclaration = findAnnotatedContainingDeclaration(psiClass, annotations, false)
if (containingDeclaration != null) {
return containingDeclaration
}
}
for (parameterType in psiType.parameters) {
val parameterResult = findAnnotatedTypePart(parameterType, annotations)
if (parameterResult != null) {
return parameterResult
}
}
}
if (psiType is PsiWildcardType) {
return findAnnotatedTypePart(psiType.extendsBound, annotations) ?: findAnnotatedTypePart(psiType.superBound, annotations)
}
return null
}
}

View File

@@ -3,12 +3,13 @@ package com.intellij.codeInspection
import com.intellij.analysis.JvmAnalysisBundle
import com.intellij.codeInsight.AnnotationUtil
import com.intellij.codeInspection.AnnotatedApiUsageUtil.findAnnotatedContainingDeclaration
import com.intellij.codeInspection.AnnotatedApiUsageUtil.findAnnotatedTypeUsedInDeclarationSignature
import com.intellij.codeInspection.apiUsage.ApiUsageProcessor
import com.intellij.codeInspection.apiUsage.ApiUsageUastVisitor
import com.intellij.codeInspection.deprecation.DeprecationInspection
import com.intellij.codeInspection.ui.SingleCheckboxOptionsPanel
import com.intellij.codeInspection.util.SpecialAnnotationsUtil
import com.intellij.lang.findUsages.LanguageFindUsages
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.roots.ProjectFileIndex
import com.intellij.psi.*
@@ -119,85 +120,64 @@ private class UnstableApiUsageProcessor(
checkUnstableApiUsage(overriddenMethod, method, true)
}
private fun checkUnstableApiUsage(target: PsiModifierListOwner, sourceNode: UElement, isMethodOverriding: Boolean) {
private fun getMessageProvider(psiAnnotation: PsiAnnotation): UnstableApiUsageMessageProvider? {
val annotationName = psiAnnotation.qualifiedName ?: return null
return knownAnnotationMessageProviders[annotationName] ?: DefaultUnstableApiUsageMessageProvider
}
private fun getElementToHighlight(sourceNode: UElement): PsiElement? =
(sourceNode as? UDeclaration)?.uastAnchor.sourcePsiElement ?: sourceNode.sourcePsi
private fun checkUnstableApiUsage(
target: PsiModifierListOwner,
sourceNode: UElement,
isMethodOverriding: Boolean
) {
if (!isLibraryElement(target)) {
return
}
val annotatedContainingDeclaration = findAnnotatedContainingDeclaration(target, unstableApiAnnotations, true)
if (annotatedContainingDeclaration == null) {
if (annotatedContainingDeclaration != null) {
val messageProvider = getMessageProvider(annotatedContainingDeclaration.psiAnnotation) ?: return
val message = if (isMethodOverriding) {
messageProvider.buildMessageUnstableMethodOverridden(annotatedContainingDeclaration)
}
else {
messageProvider.buildMessage(annotatedContainingDeclaration)
}
val elementToHighlight = getElementToHighlight(sourceNode) ?: return
problemsHolder.registerProblem(elementToHighlight, message, ProblemHighlightType.GENERIC_ERROR_OR_WARNING)
return
}
val annotationName = annotatedContainingDeclaration.psiAnnotation.qualifiedName ?: return
val messageProvider = knownAnnotationMessageProviders[annotationName] ?: DefaultUnstableApiUsageMessageProvider
val message = if (isMethodOverriding) {
messageProvider.buildUnstableMethodOverriddenMessage(annotatedContainingDeclaration)
}
else {
messageProvider.buildMessage(annotatedContainingDeclaration)
}
val elementToHighlight = (sourceNode as? UDeclaration)?.uastAnchor.sourcePsiElement ?: sourceNode.sourcePsi
if (elementToHighlight != null) {
problemsHolder.registerProblem(elementToHighlight, message, ProblemHighlightType.GENERIC_ERROR_OR_WARNING)
if (!isMethodOverriding) {
val declaration = target.toUElement(UDeclaration::class.java) ?: return
val unstableTypeUsedInSignature = findAnnotatedTypeUsedInDeclarationSignature(declaration, unstableApiAnnotations)
if (unstableTypeUsedInSignature != null) {
val messageProvider = getMessageProvider(unstableTypeUsedInSignature.psiAnnotation) ?: return
val message = messageProvider.buildMessageUnstableTypeIsUsedInSignatureOfReferencedApi(target, unstableTypeUsedInSignature)
val elementToHighlight = getElementToHighlight(sourceNode) ?: return
problemsHolder.registerProblem(elementToHighlight, message, ProblemHighlightType.GENERIC_ERROR_OR_WARNING)
}
}
}
}
data class AnnotatedContainingDeclaration(
val target: PsiModifierListOwner,
val containingDeclaration: PsiModifierListOwner,
val psiAnnotation: PsiAnnotation
) {
val targetName: String
get() = DeprecationInspection.getPresentableName(target)
val containingDeclarationName: String
get() = DeprecationInspection.getPresentableName(containingDeclaration)
val containingDeclarationType: String
get() = LanguageFindUsages.getType(containingDeclaration)
val isOwnAnnotation: Boolean
get() = target == containingDeclaration
}
fun findAnnotatedContainingDeclaration(
target: PsiModifierListOwner,
annotationNames: Collection<String>,
includeExternalAnnotations: Boolean
): AnnotatedContainingDeclaration? =
findAnnotatedContainingDeclaration(target, target, annotationNames, includeExternalAnnotations)
private fun findAnnotatedContainingDeclaration(
target: PsiModifierListOwner,
listOwner: PsiModifierListOwner,
annotationNames: Collection<String>,
includeExternalAnnotations: Boolean
): AnnotatedContainingDeclaration? {
val annotation = AnnotationUtil.findAnnotation(listOwner, annotationNames, !includeExternalAnnotations)
if (annotation != null) {
return AnnotatedContainingDeclaration(target, listOwner, annotation)
}
if (listOwner is PsiMember) {
val containingClass = listOwner.containingClass
if (containingClass != null) {
return findAnnotatedContainingDeclaration(target, containingClass, annotationNames, includeExternalAnnotations)
}
}
val packageName = (listOwner.containingFile as? PsiClassOwner)?.packageName ?: return null
val psiPackage = JavaPsiFacade.getInstance(listOwner.project).findPackage(packageName) ?: return null
return findAnnotatedContainingDeclaration(target, psiPackage, annotationNames, includeExternalAnnotations)
}
private interface UnstableApiUsageMessageProvider {
fun buildMessage(annotatedContainingDeclaration: AnnotatedContainingDeclaration): String
fun buildUnstableMethodOverriddenMessage(annotatedContainingDeclaration: AnnotatedContainingDeclaration): String
fun buildMessageUnstableMethodOverridden(annotatedContainingDeclaration: AnnotatedContainingDeclaration): String
fun buildMessageUnstableTypeIsUsedInSignatureOfReferencedApi(
referencedApi: PsiModifierListOwner,
annotatedTypeUsedInSignature: AnnotatedContainingDeclaration
): String
}
private object DefaultUnstableApiUsageMessageProvider : UnstableApiUsageMessageProvider {
override fun buildUnstableMethodOverriddenMessage(annotatedContainingDeclaration: AnnotatedContainingDeclaration): String =
override fun buildMessageUnstableMethodOverridden(annotatedContainingDeclaration: AnnotatedContainingDeclaration): String =
with(annotatedContainingDeclaration) {
if (isOwnAnnotation) {
JvmAnalysisBundle.message("jvm.inspections.unstable.api.usage.overridden.method.is.marked.unstable.itself", targetName)
@@ -226,10 +206,20 @@ private object DefaultUnstableApiUsageMessageProvider : UnstableApiUsageMessageP
)
}
}
override fun buildMessageUnstableTypeIsUsedInSignatureOfReferencedApi(
referencedApi: PsiModifierListOwner,
annotatedTypeUsedInSignature: AnnotatedContainingDeclaration
): String = JvmAnalysisBundle.message(
"jvm.inspections.unstable.api.usage.unstable.type.is.used.in.signature.of.referenced.api",
DeprecationInspection.getPresentableName(referencedApi),
annotatedTypeUsedInSignature.targetType,
annotatedTypeUsedInSignature.targetName
)
}
private class ScheduledForRemovalMessageProvider : UnstableApiUsageMessageProvider {
override fun buildUnstableMethodOverriddenMessage(annotatedContainingDeclaration: AnnotatedContainingDeclaration): String {
override fun buildMessageUnstableMethodOverridden(annotatedContainingDeclaration: AnnotatedContainingDeclaration): String {
val versionMessage = getVersionMessage(annotatedContainingDeclaration)
return with(annotatedContainingDeclaration) {
if (isOwnAnnotation) {
@@ -271,6 +261,20 @@ private class ScheduledForRemovalMessageProvider : UnstableApiUsageMessageProvid
}
}
override fun buildMessageUnstableTypeIsUsedInSignatureOfReferencedApi(
referencedApi: PsiModifierListOwner,
annotatedTypeUsedInSignature: AnnotatedContainingDeclaration
): String {
val versionMessage = getVersionMessage(annotatedTypeUsedInSignature)
return JvmAnalysisBundle.message(
"jvm.inspections.scheduled.for.removal.scheduled.for.removal.type.is.used.in.signature.of.referenced.api",
DeprecationInspection.getPresentableName(referencedApi),
annotatedTypeUsedInSignature.targetType,
annotatedTypeUsedInSignature.targetName,
versionMessage
)
}
private fun getVersionMessage(annotatedContainingDeclaration: AnnotatedContainingDeclaration): String {
val versionValue = AnnotationUtil.getDeclaredStringAttributeValue(annotatedContainingDeclaration.psiAnnotation, "inVersion")
return if (versionValue.isNullOrEmpty()) {

View File

@@ -2,6 +2,7 @@
package com.intellij.codeInspection
import com.intellij.analysis.JvmAnalysisBundle
import com.intellij.codeInspection.AnnotatedApiUsageUtil.findAnnotatedTypeUsedInDeclarationSignature
import com.intellij.codeInspection.UnstableApiUsageInspection.Companion.DEFAULT_UNSTABLE_API_ANNOTATIONS
import com.intellij.codeInspection.util.SpecialAnnotationsUtil
import com.intellij.lang.jvm.JvmModifier
@@ -50,50 +51,34 @@ private class UnstableTypeUsedInSignatureVisitor(
private val unstableApiAnnotations: List<String>
) : AbstractUastNonRecursiveVisitor() {
override fun visitClass(node: UClass): Boolean {
override fun visitDeclaration(node: UDeclaration): Boolean {
if (node !is UClass && node !is UMethod && node !is UField) {
return false
}
if (!isAccessibleDeclaration(node) || isInsideUnstableDeclaration(node)) {
return true
}
checkTypeParameters(node.javaPsi, node)
val annotatedTypeUsedInSignature = findAnnotatedTypeUsedInDeclarationSignature(node, unstableApiAnnotations) ?: return true
val elementToHighlight = node.uastAnchor.sourcePsiElement ?: return true
val typeName = (annotatedTypeUsedInSignature.target as? PsiClass)?.qualifiedName ?: return true
val annotationName = annotatedTypeUsedInSignature.psiAnnotation.qualifiedName ?: return true
val message = when (node) {
is UMethod -> JvmAnalysisBundle.message(
"jvm.inspections.unstable.type.used.in.method.signature.description", annotationName, typeName
)
is UField -> JvmAnalysisBundle.message(
"jvm.inspections.unstable.type.used.in.field.signature.description", annotationName, typeName
)
else -> JvmAnalysisBundle.message(
"jvm.inspections.unstable.type.used.in.class.signature.description", annotationName, typeName
)
}
problemsHolder.registerProblem(elementToHighlight, message, ProblemHighlightType.GENERIC_ERROR_OR_WARNING)
return true
}
override fun visitMethod(node: UMethod): Boolean {
if (!isAccessibleDeclaration(node) || isInsideUnstableDeclaration(node)) {
return true
}
for (uastParameter in node.uastParameters) {
if (checkReferencesUnstableType(uastParameter.type.deepComponentType, node)) {
return true
}
}
val returnType = node.returnType ?: return true
checkReferencesUnstableType(returnType, node)
checkTypeParameters(node.javaPsi, node)
return true
}
override fun visitField(node: UField): Boolean {
if (!isAccessibleDeclaration(node) || isInsideUnstableDeclaration(node)) {
return true
}
checkReferencesUnstableType(node.type, node)
return true
}
private fun checkTypeParameters(typeParameterListOwner: PsiTypeParameterListOwner, declaration: UDeclaration): Boolean {
for (typeParameter in typeParameterListOwner.typeParameters) {
val referencedTypes = typeParameter.extendsList.referencedTypes
for (referencedType in referencedTypes) {
if (checkReferencesUnstableType(referencedType, declaration)) {
return true
}
}
}
return false
}
private fun isAccessibleDeclaration(node: UDeclaration): Boolean {
if (node.visibility == UastVisibility.PRIVATE) {
if (node is UField) {
@@ -133,41 +118,4 @@ private class UnstableTypeUsedInSignatureVisitor(
return false
}
private fun checkReferencesUnstableType(psiType: PsiType, declaration: UDeclaration): Boolean {
val annotatedContainingDeclaration = findReferencedUnstableType(psiType.deepComponentType) ?: return false
val unstableClass = annotatedContainingDeclaration.target as? PsiClass ?: return false
val className = unstableClass.qualifiedName ?: return false
val annotationName = annotatedContainingDeclaration.psiAnnotation.qualifiedName ?: return false
val message = when (declaration) {
is UMethod -> JvmAnalysisBundle.message("jvm.inspections.unstable.type.used.in.method.signature.description", annotationName, className)
is UField -> JvmAnalysisBundle.message("jvm.inspections.unstable.type.used.in.field.signature.description", annotationName, className)
else -> JvmAnalysisBundle.message("jvm.inspections.unstable.type.used.in.class.signature.description", annotationName, className)
}
val elementToHighlight = declaration.uastAnchor.sourcePsiElement ?: return false
problemsHolder.registerProblem(elementToHighlight, message, ProblemHighlightType.GENERIC_ERROR_OR_WARNING)
return true
}
private fun findReferencedUnstableType(psiType: PsiType): AnnotatedContainingDeclaration? {
if (psiType is PsiClassType) {
val psiClass = psiType.resolve()
if (psiClass != null) {
val unstableContainingDeclaration = findAnnotatedContainingDeclaration(psiClass, unstableApiAnnotations, false)
if (unstableContainingDeclaration != null) {
return unstableContainingDeclaration
}
}
for (parameterType in psiType.parameters) {
val parameterResult = findReferencedUnstableType(parameterType)
if (parameterResult != null) {
return parameterResult
}
}
}
if (psiType is PsiWildcardType) {
return findReferencedUnstableType(psiType.extendsBound) ?: findReferencedUnstableType(psiType.superBound)
}
return null
}
}