[kotlin] New Inspection API: API improvements

- Require `ApplicabilityRanges.SELF` to be specified manually with every intention/inspection so that the developer is forced to think about the applicability range.
- For inspections, rename `getFamilyName` to `getActionFamilyName` as this is the family name of the quick fix. The inspection's family name is configured via XML.
- Add `getProblemRanges` to inspections, which allows selecting a subset of applicability ranges to register the inspection's problems. The default implementation which selects all ranges should suffice for most inspections.

GitOrigin-RevId: 3762bc0c16878cabd229e6533dc0bf70313395bb
This commit is contained in:
Marco Pennekamp
2022-11-14 20:43:46 +00:00
committed by intellij-monorepo-bot
parent ea36dd85a3
commit ea7c3cfdad
25 changed files with 79 additions and 46 deletions
@@ -1,28 +1,19 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.codeinsight.api.applicable
import com.intellij.codeInspection.util.IntentionFamilyName
import org.jetbrains.kotlin.idea.codeinsight.api.applicators.KotlinApplicabilityRange
import org.jetbrains.kotlin.idea.codeinsight.api.applicators.applicabilityTarget
import org.jetbrains.kotlin.psi.KtElement
/**
* A common base interface for [KotlinApplicableIntentionBase] and [KotlinApplicableInspectionBase].
*/
interface KotlinApplicableToolBase<ELEMENT : KtElement> {
/**
* @see com.intellij.codeInsight.intention.IntentionAction.getFamilyName
* @see com.intellij.codeInspection.QuickFix.getFamilyName
*/
fun getFamilyName(): @IntentionFamilyName String
/**
* The [KotlinApplicabilityRange] determines whether the tool is available in a range *after* [isApplicableByPsi] has been checked.
*
* The default applicability range is equivalent to `ApplicabilityRanges.SELF`. Configuration of the applicability range might be as
* simple as choosing an existing one from `ApplicabilityRanges`.
* Configuration of the applicability range might be as simple as choosing an existing one from `ApplicabilityRanges`.
*/
fun getApplicabilityRange(): KotlinApplicabilityRange<ELEMENT> = applicabilityTarget { it }
fun getApplicabilityRange(): KotlinApplicabilityRange<ELEMENT>
/**
* Whether this tool is applicable to [element] by PSI only. May not use the Analysis API due to performance concerns.
@@ -28,7 +28,7 @@ abstract class AbstractKotlinApplicableInspection<ELEMENT : KtElement>(
*
* @see com.intellij.codeInspection.CommonProblemDescriptor.getDescriptionTemplate
*/
open fun getProblemDescription(element: ELEMENT): @InspectionMessage String = getFamilyName()
open fun getProblemDescription(element: ELEMENT): @InspectionMessage String = getActionFamilyName()
/**
* Returns the [ProblemHighlightType] for the inspection's registered problem.
@@ -45,7 +45,7 @@ abstract class AbstractKotlinApplicableInspection<ELEMENT : KtElement>(
apply(element, element.project, element.findExistingEditor())
}
override fun getFamilyName(): String = this@KotlinApplicableInspection.getFamilyName()
override fun getFamilyName(): String = this@AbstractKotlinApplicableInspection.getActionFamilyName()
override fun getName(): String = elementPointer.element?.let { getActionName(element) } ?: familyName
}
@@ -3,7 +3,9 @@ package org.jetbrains.kotlin.idea.codeinsight.api.applicable.inspections
import com.intellij.codeInspection.*
import com.intellij.codeInspection.util.InspectionMessage
import com.intellij.codeInspection.util.IntentionFamilyName
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiFile
import org.jetbrains.kotlin.idea.codeinsight.api.applicable.KotlinApplicableToolBase
import org.jetbrains.kotlin.idea.codeinsight.api.applicators.*
@@ -19,6 +21,17 @@ import kotlin.reflect.KClass
sealed class KotlinApplicableInspectionBase<ELEMENT : KtElement>(
elementType: KClass<ELEMENT>,
) : KotlinSingleElementInspection<ELEMENT>(elementType), KotlinApplicableToolBase<ELEMENT> {
/**
* @see com.intellij.codeInspection.QuickFix.getFamilyName
*/
abstract fun getActionFamilyName(): @IntentionFamilyName String
/**
* By default, a problem is registered for every [TextRange] produced by [getApplicabilityRange]. [getProblemRanges] can be overridden
* to customize this behavior, e.g. to register a problem only for the first [TextRange].
*/
open fun getProblemRanges(ranges: List<TextRange>): List<TextRange> = ranges
internal class ProblemInfo(
val description: @InspectionMessage String,
val highlightType: ProblemHighlightType,
@@ -37,19 +50,14 @@ sealed class KotlinApplicableInspectionBase<ELEMENT : KtElement>(
if (ranges.isEmpty()) return
val problemInfo = buildProblemInfo(element) ?: return
ranges.forEach { range ->
with(holder) {
registerProblem(
manager.createProblemDescriptor(
element,
range,
problemInfo.description,
problemInfo.highlightType,
isOnTheFly,
problemInfo.quickFix
)
)
}
getProblemRanges(ranges).forEach { range ->
holder.registerProblem(
element,
problemInfo.description,
problemInfo.highlightType,
range,
problemInfo.quickFix,
)
}
}
}
@@ -28,7 +28,7 @@ abstract class AbstractKotlinApplicableInspectionWithContext<ELEMENT : KtElement
*
* @see com.intellij.codeInspection.CommonProblemDescriptor.getDescriptionTemplate
*/
open fun getProblemDescription(element: ELEMENT, context: CONTEXT): @InspectionMessage String = getFamilyName()
open fun getProblemDescription(element: ELEMENT, context: CONTEXT): @InspectionMessage String = getActionFamilyName()
/**
* Returns the [ProblemHighlightType] for the inspection's registered problem.
@@ -45,7 +45,7 @@ abstract class AbstractKotlinApplicableInspectionWithContext<ELEMENT : KtElement
apply(element, context, element.project, element.findExistingEditor())
}
override fun getFamilyName(): String = this@KotlinApplicableInspectionWithContext.getFamilyName()
override fun getFamilyName(): String = this@AbstractKotlinApplicableInspectionWithContext.getActionFamilyName()
override fun getName(): String = elementPointer.element?.let { getActionName(element, context) } ?: familyName
}
@@ -20,6 +20,9 @@ import kotlin.reflect.KClass
sealed class AbstractKotlinApplicableIntentionBase<ELEMENT : KtElement>(
elementType: KClass<ELEMENT>,
) : SelfTargetingIntention<ELEMENT>(elementType.java, { "" }), KotlinApplicableToolBase<ELEMENT> {
/**
* @see com.intellij.codeInsight.intention.IntentionAction.getFamilyName
*/
abstract override fun getFamilyName(): @IntentionFamilyName String
/**
@@ -24,10 +24,12 @@ internal class ImplicitThisInspection :
val isUnambiguousLabel: Boolean
)
override fun getFamilyName(): String = KotlinBundle.message("inspection.implicit.this.display.name")
override fun getActionFamilyName(): String = KotlinBundle.message("inspection.implicit.this.display.name")
override fun getActionName(element: KtExpression, context: ImplicitReceiverInfo): String =
KotlinBundle.message("inspection.implicit.this.action.name")
override fun getApplicabilityRange(): KotlinApplicabilityRange<KtExpression> = ApplicabilityRanges.SELF
override fun isApplicableByPsi(element: KtExpression): Boolean {
return when (element) {
is KtSimpleNameExpression -> {
@@ -62,7 +64,8 @@ internal class ImplicitThisInspection :
}
}
private fun KtAnalysisSession.getAssociatedClass(symbol: KtSymbol): KtClassOrObjectSymbol? {
context(KtAnalysisSession)
private fun getAssociatedClass(symbol: KtSymbol): KtClassOrObjectSymbol? {
// both variables and functions are callable and only they can be referenced by "this"
if (symbol !is KtCallableSymbol) return null
return when (symbol) {
@@ -76,7 +79,8 @@ private fun KtAnalysisSession.getAssociatedClass(symbol: KtSymbol): KtClassOrObj
}
}
private fun KtAnalysisSession.getImplicitReceiverInfoOfClass(
context(KtAnalysisSession)
private fun getImplicitReceiverInfoOfClass(
implicitReceivers: List<KtImplicitReceiver>, associatedClass: KtClassOrObjectSymbol
): ImplicitThisInspection.ImplicitReceiverInfo? {
// We can't use "this" with label if the label is already taken
@@ -100,7 +104,8 @@ private fun KtAnalysisSession.getImplicitReceiverInfoOfClass(
return null
}
private fun KtAnalysisSession.getImplicitReceiverClassAndTag(receiver: KtImplicitReceiver): Pair<KtClassOrObjectSymbol, Name?>? {
context(KtAnalysisSession)
private fun getImplicitReceiverClassAndTag(receiver: KtImplicitReceiver): Pair<KtClassOrObjectSymbol, Name?>? {
val associatedClass = receiver.type.expandedClassSymbol ?: return null
val associatedTag: Name? = when (val receiverSymbol = receiver.ownerSymbol) {
is KtClassOrObjectSymbol -> receiverSymbol.name
@@ -21,10 +21,12 @@ import org.jetbrains.kotlin.psi.*
* See plugins/kotlin/code-insight/descriptions/resources-en/inspectionDescriptions/NullableBooleanElvis.html for details.
*/
class NullableBooleanElvisInspection : AbstractKotlinApplicableInspection<KtBinaryExpression>(KtBinaryExpression::class) {
override fun getFamilyName(): String = KotlinBundle.message("inspection.nullable.boolean.elvis.display.name")
override fun getActionFamilyName(): String = KotlinBundle.message("inspection.nullable.boolean.elvis.display.name")
override fun getActionName(element: KtBinaryExpression): String =
KotlinBundle.message("inspection.nullable.boolean.elvis.action.name")
override fun getApplicabilityRange(): KotlinApplicabilityRange<KtExpression> = ApplicabilityRanges.SELF
override fun isApplicableByPsi(element: KtBinaryExpression): Boolean = element.isTargetOfNullableBooleanElvisInspection()
context(KtAnalysisSession)
@@ -19,7 +19,7 @@ import org.jetbrains.kotlin.psi.psiUtil.startOffset
import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes
internal class RedundantElvisReturnNullInspection : AbstractKotlinApplicableInspection<KtBinaryExpression>(KtBinaryExpression::class) {
override fun getFamilyName(): String = KotlinBundle.message("inspection.redundant.elvis.return.null.descriptor")
override fun getActionFamilyName(): String = KotlinBundle.message("inspection.redundant.elvis.return.null.descriptor")
override fun getActionName(element: KtBinaryExpression): String = KotlinBundle.message("remove.redundant.elvis.return.null.text")
override fun getApplicabilityRange() = applicabilityRanges { binaryExpression: KtBinaryExpression ->
@@ -20,8 +20,10 @@ internal class RemoveSingleExpressionStringTemplateInspection :
class Context(val isString: Boolean)
override fun getFamilyName(): String = KotlinBundle.message("remove.single.expression.string.template")
override fun getActionName(element: KtStringTemplateExpression, context: Context): String = getFamilyName()
override fun getActionFamilyName(): String = KotlinBundle.message("remove.single.expression.string.template")
override fun getActionName(element: KtStringTemplateExpression, context: Context): String = getActionFamilyName()
override fun getApplicabilityRange(): KotlinApplicabilityRange<KtStringTemplateExpression> = ApplicabilityRanges.SELF
override fun isApplicableByPsi(element: KtStringTemplateExpression): Boolean = element.singleExpressionOrNull() != null
@@ -22,8 +22,8 @@ internal class RemoveToStringInStringTemplateInspection :
AbstractKotlinApplicableInspection<KtDotQualifiedExpression>(KtDotQualifiedExpression::class),
CleanupLocalInspectionTool {
override fun getFamilyName(): String = KotlinBundle.message("remove.to.string.fix.text")
override fun getActionName(element: KtDotQualifiedExpression): String = getFamilyName()
override fun getActionFamilyName(): String = KotlinBundle.message("remove.to.string.fix.text")
override fun getActionName(element: KtDotQualifiedExpression): String = getActionFamilyName()
override fun getApplicabilityRange(): KotlinApplicabilityRange<KtDotQualifiedExpression> =
applicabilityRanges { dotQualifiedExpression: KtDotQualifiedExpression ->
@@ -26,7 +26,7 @@ internal class ReplaceGetOrSetInspection :
class Context(val calleeName: Name)
override fun getFamilyName(): String = KotlinBundle.message("inspection.replace.get.or.set.display.name")
override fun getActionFamilyName(): String = KotlinBundle.message("inspection.replace.get.or.set.display.name")
override fun getProblemDescription(element: KtDotQualifiedExpression, context: Context): String =
KotlinBundle.message("explicit.0.call", context.calleeName)
override fun getActionName(element: KtDotQualifiedExpression, context: Context): String =
@@ -17,7 +17,7 @@ internal class RedundantUnitReturnTypeInspection :
AbstractKotlinApplicableInspectionWithContext<KtNamedFunction, TypeInfo>(KtNamedFunction::class),
CleanupLocalInspectionTool {
override fun getFamilyName(): String = KotlinBundle.message("inspection.redundant.unit.return.type.display.name")
override fun getActionFamilyName(): String = KotlinBundle.message("inspection.redundant.unit.return.type.display.name")
override fun getActionName(element: KtNamedFunction, context: TypeInfo): String =
KotlinBundle.message("inspection.redundant.unit.return.type.action.name")
@@ -14,7 +14,7 @@ import org.jetbrains.kotlin.psi.psiUtil.modalityModifierType
class RedundantModalityModifierInspection :
RedundantModifierInspectionBase<KtFirDiagnostic.RedundantModalityModifier>(KtTokens.MODALITY_MODIFIERS) {
override fun getFamilyName(): String = KotlinBundle.message("redundant.modality.modifier")
override fun getActionFamilyName(): String = KotlinBundle.message("redundant.modality.modifier")
override fun getDiagnosticType() = KtFirDiagnostic.RedundantModalityModifier::class
@@ -14,7 +14,7 @@ import org.jetbrains.kotlin.psi.psiUtil.visibilityModifierType
internal class RedundantVisibilityModifierInspection :
RedundantModifierInspectionBase<KtFirDiagnostic.RedundantVisibilityModifier>(KtTokens.VISIBILITY_MODIFIERS) {
override fun getFamilyName(): String = KotlinBundle.message("redundant.visibility.modifier")
override fun getActionFamilyName(): String = KotlinBundle.message("redundant.visibility.modifier")
override fun getDiagnosticType() = KtFirDiagnostic.RedundantVisibilityModifier::class
@@ -19,7 +19,7 @@ internal class UnusedVariableInspection :
KtNamedDeclaration::class,
) {
override fun getFamilyName(): String = KotlinBundle.message("inspection.kotlin.unused.variable.display.name")
override fun getActionFamilyName(): String = KotlinBundle.message("inspection.kotlin.unused.variable.display.name")
override fun getActionName(element: KtNamedDeclaration): String =
KotlinBundle.message("remove.variable.0", element.name.toString())
@@ -14,10 +14,12 @@ import org.jetbrains.kotlin.psi.KtParenthesizedExpression
import org.jetbrains.kotlin.psi.KtPrefixExpression
internal class KotlinDoubleNegationInspection : AbstractKotlinApplicableInspection<KtPrefixExpression>(KtPrefixExpression::class) {
override fun getFamilyName(): String = KotlinBundle.message("inspection.kotlin.double.negation.display.name")
override fun getActionFamilyName(): String = KotlinBundle.message("inspection.kotlin.double.negation.display.name")
override fun getActionName(element: KtPrefixExpression): String =
KotlinBundle.message("inspection.kotlin.double.negation.action.name")
override fun getApplicabilityRange(): KotlinApplicabilityRange<KtPrefixExpression> = ApplicabilityRanges.SELF
override fun isApplicableByPsi(element: KtPrefixExpression): Boolean =
element.operationToken == KtTokens.EXCL
&& (element.parentThroughParenthesis as? KtPrefixExpression)?.operationToken == KtTokens.EXCL
@@ -23,10 +23,12 @@ private val COLLECTION_CLASS_IDS = setOf(StandardClassIds.Collection, StandardCl
StandardClassIds.elementTypeByPrimitiveArrayType.keys + StandardClassIds.unsignedArrayTypeByElementType.keys
internal class ReplaceCollectionCountWithSizeInspection : AbstractKotlinApplicableInspection<KtCallExpression>(KtCallExpression::class) {
override fun getFamilyName(): String = KotlinBundle.message("inspection.replace.collection.count.with.size.display.name")
override fun getActionFamilyName(): String = KotlinBundle.message("inspection.replace.collection.count.with.size.display.name")
override fun getActionName(element: KtCallExpression): String =
KotlinBundle.message("replace.collection.count.with.size.quick.fix.text")
override fun getApplicabilityRange(): KotlinApplicabilityRange<KtCallExpression> = ApplicabilityRanges.SELF
override fun isApplicableByPsi(element: KtCallExpression): Boolean =
element.calleeExpression?.text == "count" && element.valueArguments.isEmpty()
@@ -8,13 +8,16 @@ import org.jetbrains.kotlin.idea.codeinsight.api.applicable.inspections.Abstract
import org.jetbrains.kotlin.idea.codeinsight.api.applicators.KotlinApplicabilityRange
import org.jetbrains.kotlin.idea.codeinsights.impl.base.RemoveEmptyParenthesesFromLambdaCallUtils.canRemoveByPsi
import org.jetbrains.kotlin.idea.codeinsights.impl.base.RemoveEmptyParenthesesFromLambdaCallUtils.removeArgumentList
import org.jetbrains.kotlin.idea.codeinsights.impl.base.applicators.ApplicabilityRanges
import org.jetbrains.kotlin.psi.KtValueArgumentList
class RemoveEmptyParenthesesFromLambdaCallInspection : AbstractKotlinApplicableInspection<KtValueArgumentList>(KtValueArgumentList::class) {
override fun getFamilyName(): String = KotlinBundle.message("inspection.remove.empty.parentheses.from.lambda.call.display.name")
override fun getActionFamilyName(): String = KotlinBundle.message("inspection.remove.empty.parentheses.from.lambda.call.display.name")
override fun getActionName(element: KtValueArgumentList): String =
KotlinBundle.message("inspection.remove.empty.parentheses.from.lambda.call.action.name")
override fun getApplicabilityRange(): KotlinApplicabilityRange<KtValueArgumentList> = ApplicabilityRanges.SELF
override fun isApplicableByPsi(element: KtValueArgumentList): Boolean = canRemoveByPsi(element)
override fun apply(element: KtValueArgumentList, project: Project, editor: Editor?) {
@@ -24,6 +24,8 @@ internal class AddOpenModifierIntention :
override fun getFamilyName(): String = KotlinBundle.message("make.open")
override fun getActionName(element: KtCallableDeclaration): String = familyName
override fun getApplicabilityRange(): KotlinApplicabilityRange<KtCallableDeclaration> = ApplicabilityRanges.SELF
override fun isApplicableByPsi(element: KtCallableDeclaration): Boolean =
(element is KtProperty || element is KtNamedFunction)
&& !element.hasModifier(KtTokens.OPEN_KEYWORD)
@@ -19,6 +19,8 @@ internal class AddWhenRemainingBranchesIntention
override fun getFamilyName(): String = AddRemainingWhenBranchesUtils.familyAndActionName(false)
override fun getActionName(element: KtWhenExpression, context: AddRemainingWhenBranchesUtils.Context): String = familyName
override fun getApplicabilityRange(): KotlinApplicabilityRange<KtWhenExpression> = ApplicabilityRanges.SELF
override fun isApplicableByPsi(element: KtWhenExpression): Boolean = true
context(KtAnalysisSession)
@@ -18,6 +18,8 @@ internal class ConvertConcatenationToBuildStringIntention : AbstractKotlinApplic
override fun getFamilyName(): String = KotlinBundle.message("convert.concatenation.to.build.string")
override fun getActionName(element: KtBinaryExpression): String = familyName
override fun getApplicabilityRange(): KotlinApplicabilityRange<KtBinaryExpression> = ApplicabilityRanges.SELF
override fun isApplicableByPsi(element: KtBinaryExpression): Boolean =
element.operationToken == KtTokens.PLUS && !element.isAnnotationArgument()
@@ -19,6 +19,8 @@ internal class ConvertStringTemplateToBuildStringIntention : AbstractKotlinAppli
override fun getFamilyName(): String = KotlinBundle.message("convert.string.template.to.build.string")
override fun getActionName(element: KtStringTemplateExpression): String = familyName
override fun getApplicabilityRange(): KotlinApplicabilityRange<KtStringTemplateExpression> = ApplicabilityRanges.SELF
override fun isApplicableByPsi(element: KtStringTemplateExpression): Boolean =
!element.text.startsWith("\"\"\"") && !element.isAnnotationArgument()
@@ -12,6 +12,7 @@ import org.jetbrains.kotlin.idea.base.resources.KotlinBundle
import org.jetbrains.kotlin.idea.codeinsight.api.applicable.intentions.AbstractKotlinApplicableIntentionWithContext
import org.jetbrains.kotlin.idea.codeinsight.api.applicators.KotlinApplicabilityRange
import org.jetbrains.kotlin.idea.codeinsight.utils.adjustLineIndent
import org.jetbrains.kotlin.idea.codeinsights.impl.base.applicators.ApplicabilityRanges
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.endOffset
@@ -33,6 +34,8 @@ internal class ConvertToBlockBodyIntention :
override fun getFamilyName(): String = KotlinBundle.message("convert.to.block.body")
override fun getActionName(element: KtDeclarationWithBody, context: Context): String = familyName
override fun getApplicabilityRange(): KotlinApplicabilityRange<KtDeclarationWithBody> = ApplicabilityRanges.SELF
override fun isApplicableByPsi(element: KtDeclarationWithBody): Boolean =
(element is KtNamedFunction || element is KtPropertyAccessor) && !element.hasBlockBody() && element.hasBody()
@@ -39,6 +39,8 @@ internal class ImportAllMembersIntention :
override fun getActionName(element: KtExpression, context: Context): String =
KotlinBundle.message("import.members.from.0", context.fqName.asString())
override fun getApplicabilityRange(): KotlinApplicabilityRange<KtExpression> = ApplicabilityRanges.SELF
override fun isApplicableByPsi(element: KtExpression): Boolean =
element.isOnTheLeftOfQualificationDot && !element.isInImportDirective()
@@ -35,6 +35,8 @@ internal class ImportMemberIntention :
override fun getActionName(element: KtNameReferenceExpression, context: Context): String =
KotlinBundle.message("add.import.for.0", context.fqName.asString())
override fun getApplicabilityRange(): KotlinApplicabilityRange<KtNameReferenceExpression> = ApplicabilityRanges.SELF
override fun isApplicableByPsi(element: KtNameReferenceExpression): Boolean =
// Ignore simple name expressions or already imported names.
element.getQualifiedElement() != element && !element.isInImportDirective()