FIR IDE: Ensure all fixes provided by HLDiagnosticFixFactory are of type

QuickFixActionBase.

This allows us to verify in the quickfix tests that the action comes
from a diagnostic-based quickfix, and not an available intention. We
are adding APIs that wrap IntentionActions as QuickFixActionBase.
This commit is contained in:
Mark Punzalan
2021-06-10 00:24:58 +00:00
committed by TeamCityServer
parent 43a8299f48
commit 783e5972d0
9 changed files with 86 additions and 62 deletions
@@ -12,7 +12,7 @@ import com.intellij.psi.PsiFile
import org.jetbrains.kotlin.descriptors.Modality
import org.jetbrains.kotlin.idea.core.overrideImplement.KtImplementMembersHandler.Companion.getUnimplementedMembers
import org.jetbrains.kotlin.idea.core.util.KotlinIdeaCoreBundle
import org.jetbrains.kotlin.idea.fir.api.fixes.diagnosticFixFactory
import org.jetbrains.kotlin.idea.fir.api.fixes.diagnosticFixFactoryFromIntentionActions
import org.jetbrains.kotlin.idea.frontend.api.KtAnalysisSession
import org.jetbrains.kotlin.idea.frontend.api.analyse
import org.jetbrains.kotlin.idea.frontend.api.fir.diagnostics.KtFirDiagnostic
@@ -120,21 +120,25 @@ internal class KtImplementAsConstructorParameterQuickfix(private val members: Co
object MemberNotImplementedQuickfixFactories {
val abstractMemberNotImplemented = diagnosticFixFactory(KtFirDiagnostic.AbstractMemberNotImplemented::class) { diagnostic ->
getUnimplementedMemberFixes(diagnostic.psi)
}
val abstractMemberNotImplemented =
diagnosticFixFactoryFromIntentionActions(KtFirDiagnostic.AbstractMemberNotImplemented::class) { diagnostic ->
getUnimplementedMemberFixes(diagnostic.psi)
}
val abstractClassMemberNotImplemented = diagnosticFixFactory(KtFirDiagnostic.AbstractClassMemberNotImplemented::class) { diagnostic ->
getUnimplementedMemberFixes(diagnostic.psi)
}
val abstractClassMemberNotImplemented =
diagnosticFixFactoryFromIntentionActions(KtFirDiagnostic.AbstractClassMemberNotImplemented::class) { diagnostic ->
getUnimplementedMemberFixes(diagnostic.psi)
}
val manyInterfacesMemberNotImplemented = diagnosticFixFactory(KtFirDiagnostic.ManyInterfacesMemberNotImplemented::class) { diagnostic ->
getUnimplementedMemberFixes(diagnostic.psi)
}
val manyInterfacesMemberNotImplemented =
diagnosticFixFactoryFromIntentionActions(KtFirDiagnostic.ManyInterfacesMemberNotImplemented::class) { diagnostic ->
getUnimplementedMemberFixes(diagnostic.psi)
}
val manyImplMemberNotImplemented = diagnosticFixFactory(KtFirDiagnostic.ManyImplMemberNotImplemented::class) { diagnostic ->
getUnimplementedMemberFixes(diagnostic.psi, false)
}
val manyImplMemberNotImplemented =
diagnosticFixFactoryFromIntentionActions(KtFirDiagnostic.ManyImplMemberNotImplemented::class) { diagnostic ->
getUnimplementedMemberFixes(diagnostic.psi, false)
}
@OptIn(ExperimentalStdlibApi::class)
private fun KtAnalysisSession.getUnimplementedMemberFixes(
@@ -20,7 +20,8 @@ import kotlin.reflect.KClass
abstract class AbstractHLIntention<PSI : KtElement, INPUT : HLApplicatorInput>(
elementType: KClass<PSI>,
) : SelfTargetingIntention<PSI>(elementType.java, { "" }, { "" }) {
val applicator: HLApplicator<PSI, INPUT>
) : SelfTargetingIntention<PSI>(elementType.java, applicator::getFamilyName) {
final override fun isApplicableTo(element: PSI, caretOffset: Int): Boolean {
val project = element.project// TODO expensive operation, may require traversing the tree up to containing PsiFile
if (!applicator.isApplicableByPsi(element, project)) return false
@@ -61,6 +62,5 @@ abstract class AbstractHLIntention<PSI : KtElement, INPUT : HLApplicatorInput>(
}
abstract val applicabilityRange: HLApplicabilityRange<PSI>
abstract val applicator: HLApplicator<PSI, INPUT>
abstract val inputProvider: HLApplicatorInputProvider<PSI, INPUT>
}
@@ -6,15 +6,19 @@
package org.jetbrains.kotlin.idea.fir.api.fixes
import com.intellij.codeInsight.intention.IntentionAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import org.jetbrains.kotlin.idea.api.applicator.HLApplicator
import org.jetbrains.kotlin.idea.api.applicator.HLApplicatorInput
import org.jetbrains.kotlin.idea.frontend.api.KtAnalysisSession
import org.jetbrains.kotlin.idea.frontend.api.diagnostics.KtDiagnosticWithPsi
import org.jetbrains.kotlin.idea.quickfix.QuickFixActionBase
import kotlin.reflect.KClass
sealed class HLDiagnosticFixFactory<DIAGNOSTIC : KtDiagnosticWithPsi<*>> {
abstract fun KtAnalysisSession.createQuickFixes(diagnostic: DIAGNOSTIC): List<IntentionAction>
abstract fun KtAnalysisSession.createQuickFixes(diagnostic: DIAGNOSTIC): List<QuickFixActionBase<*>>
abstract val diagnosticClass: KClass<DIAGNOSTIC>
}
@@ -27,11 +31,11 @@ private class HLDiagnosticFixFactoryWithFixedApplicator<DIAGNOSTIC : KtDiagnosti
createTargets.invoke(this, diagnostic).map { (target, input) -> HLQuickFix(target, input, applicator) }
}
private class HLDiagnosticFixFactoryWithVariableApplicator<DIAGNOSTIC : KtDiagnosticWithPsi<*>>(
private class HLDiagnosticFixFactoryUsingQuickFixActionBase<DIAGNOSTIC : KtDiagnosticWithPsi<*>>(
override val diagnosticClass: KClass<DIAGNOSTIC>,
private val createQuickFixes: KtAnalysisSession.(DIAGNOSTIC) -> List<IntentionAction>
private val createQuickFixes: KtAnalysisSession.(DIAGNOSTIC) -> List<QuickFixActionBase<*>>
) : HLDiagnosticFixFactory<DIAGNOSTIC>() {
override fun KtAnalysisSession.createQuickFixes(diagnostic: DIAGNOSTIC): List<IntentionAction> =
override fun KtAnalysisSession.createQuickFixes(diagnostic: DIAGNOSTIC): List<QuickFixActionBase<*>> =
createQuickFixes.invoke(this, diagnostic)
}
@@ -52,19 +56,42 @@ fun <DIAGNOSTIC : KtDiagnosticWithPsi<*>, TARGET_PSI : PsiElement, INPUT : HLApp
HLDiagnosticFixFactoryWithFixedApplicator(diagnosticClass, applicator, createTargets)
/**
* Returns a [HLDiagnosticFixFactory] that creates [HLQuickFix]es from a diagnostic.
* Returns a [HLDiagnosticFixFactory] that creates [QuickFixActionBase]s from a diagnostic.
*/
fun <DIAGNOSTIC : KtDiagnosticWithPsi<*>> diagnosticFixFactory(
diagnosticClass: KClass<DIAGNOSTIC>,
createQuickFixes: KtAnalysisSession.(DIAGNOSTIC) -> List<IntentionAction>
createQuickFixes: KtAnalysisSession.(DIAGNOSTIC) -> List<QuickFixActionBase<*>>
): HLDiagnosticFixFactory<DIAGNOSTIC> =
HLDiagnosticFixFactoryWithVariableApplicator(diagnosticClass, createQuickFixes)
HLDiagnosticFixFactoryUsingQuickFixActionBase(diagnosticClass, createQuickFixes)
/**
* Returns a [Collection] of [HLDiagnosticFixFactory] that creates [HLQuickFix]es from diagnostics that have the same type of [PsiElement].
* Returns a [Collection] of [HLDiagnosticFixFactory] that creates [QuickFixActionBase]s from diagnostics that have the same type of
* [PsiElement].
*/
fun <DIAGNOSTIC_PSI : PsiElement, DIAGNOSTIC : KtDiagnosticWithPsi<DIAGNOSTIC_PSI>> diagnosticFixFactories(
vararg diagnosticClasses: KClass<out DIAGNOSTIC>,
createQuickFixes: KtAnalysisSession.(DIAGNOSTIC) -> List<IntentionAction>
createQuickFixes: KtAnalysisSession.(DIAGNOSTIC) -> List<QuickFixActionBase<*>>
): Collection<HLDiagnosticFixFactory<out DIAGNOSTIC>> =
diagnosticClasses.map { HLDiagnosticFixFactoryWithVariableApplicator(it, createQuickFixes) }
diagnosticClasses.map { HLDiagnosticFixFactoryUsingQuickFixActionBase(it, createQuickFixes) }
/**
* Returns a [HLDiagnosticFixFactory] that creates [IntentionAction]s from a diagnostic.
*/
fun <DIAGNOSTIC : KtDiagnosticWithPsi<*>> diagnosticFixFactoryFromIntentionActions(
diagnosticClass: KClass<DIAGNOSTIC>,
createIntentionActions: KtAnalysisSession.(DIAGNOSTIC) -> List<IntentionAction>
): HLDiagnosticFixFactory<DIAGNOSTIC> {
// Wrap the IntentionActions as QuickFixActionBase. This ensures all fixes are of type QuickFixActionBase.
val createQuickFixes: KtAnalysisSession.(DIAGNOSTIC) -> List<QuickFixActionBase<*>> = { diagnostic ->
val intentionActions = createIntentionActions.invoke(this, diagnostic)
intentionActions.map { IntentionActionAsQuickFixWrapper(it, diagnostic.psi) }
}
return HLDiagnosticFixFactoryUsingQuickFixActionBase(diagnosticClass, createQuickFixes)
}
private class IntentionActionAsQuickFixWrapper<T : PsiElement>(val intentionAction: IntentionAction, element: T) :
QuickFixActionBase<T>(element) {
override fun getText(): String = intentionAction.text
override fun getFamilyName(): String = intentionAction.familyName
override fun invoke(project: Project, editor: Editor?, file: PsiFile?) = intentionAction.invoke(project, editor, file)
}
@@ -25,18 +25,11 @@ import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
import org.jetbrains.kotlin.psi.psiUtil.hasExpectModifier
abstract class HLAddAccessorIntention(private val addGetter: Boolean, private val addSetter: Boolean) :
AbstractHLIntention<KtProperty, HLApplicatorInput.Empty>(KtProperty::class) {
AbstractHLIntention<KtProperty, HLApplicatorInput.Empty>(KtProperty::class, applicator(addGetter, addSetter)) {
override val applicabilityRange: HLApplicabilityRange<KtProperty> = applicabilityTarget { ktProperty ->
if (ktProperty.hasInitializer()) ktProperty.nameIdentifier else ktProperty
}
override val applicator: HLApplicator<KtProperty, HLApplicatorInput.Empty> = applicator {
applyTo { ktProperty, _, _, editor ->
AbstractAddAccessorsIntention.applyTo(ktProperty, editor, addGetter, addSetter)
}
familyAndActionName(AbstractAddAccessorsIntention.createFamilyName(addGetter, addSetter))
}
override val inputProvider: HLApplicatorInputProvider<KtProperty, HLApplicatorInput.Empty> = inputProvider { ktProperty ->
val symbol = ktProperty.getVariableSymbol() as? KtPropertySymbol ?: return@inputProvider null
if (symbol.containsAnnotation(JVM_FIELD_CLASS_ID)) return@inputProvider null
@@ -6,8 +6,8 @@
package org.jetbrains.kotlin.idea.fir.intentions.declarations
import org.jetbrains.kotlin.idea.KotlinBundle
import org.jetbrains.kotlin.idea.fir.api.*
import org.jetbrains.kotlin.idea.api.applicator.with
import org.jetbrains.kotlin.idea.fir.api.*
import org.jetbrains.kotlin.idea.fir.api.applicator.HLApplicabilityRange
import org.jetbrains.kotlin.idea.fir.api.applicator.inputProvider
import org.jetbrains.kotlin.idea.fir.applicators.ApplicabilityRanges
@@ -15,16 +15,7 @@ import org.jetbrains.kotlin.idea.fir.applicators.CallableReturnTypeUpdaterApplic
import org.jetbrains.kotlin.psi.*
class HLSpecifyExplicitTypeForCallableDeclarationIntention :
AbstractHLIntention<KtCallableDeclaration, CallableReturnTypeUpdaterApplicator.Type>(
KtCallableDeclaration::class
) {
override val applicator = CallableReturnTypeUpdaterApplicator.applicator.with {
isApplicableByPsi { declaration: KtCallableDeclaration ->
if (declaration is KtConstructor<*> || declaration is KtFunctionLiteral) return@isApplicableByPsi false
declaration.typeReference == null && (declaration as? KtNamedFunction)?.hasBlockBody() != true
}
familyAndActionName(KotlinBundle.lazyMessage("specify.return.type.explicitly"))
}
AbstractHLIntention<KtCallableDeclaration, CallableReturnTypeUpdaterApplicator.Type>(KtCallableDeclaration::class, applicator) {
override val applicabilityRange: HLApplicabilityRange<KtCallableDeclaration> = ApplicabilityRanges.SELF
override val inputProvider = inputProvider<KtCallableDeclaration, CallableReturnTypeUpdaterApplicator.Type> { declaration ->
@@ -32,4 +23,14 @@ class HLSpecifyExplicitTypeForCallableDeclarationIntention :
val denotableType = returnType.approximateToSuperPublicDenotable() ?: returnType
with(CallableReturnTypeUpdaterApplicator.Type) { createByKtType(denotableType) }
}
companion object {
private val applicator = CallableReturnTypeUpdaterApplicator.applicator.with {
isApplicableByPsi { declaration: KtCallableDeclaration ->
if (declaration is KtConstructor<*> || declaration is KtFunctionLiteral) return@isApplicableByPsi false
declaration.typeReference == null && (declaration as? KtNamedFunction)?.hasBlockBody() != true
}
familyAndActionName(KotlinBundle.lazyMessage("specify.return.type.explicitly"))
}
}
}
@@ -5,7 +5,7 @@
package org.jetbrains.kotlin.idea.quickfix.fixes
import org.jetbrains.kotlin.idea.fir.api.fixes.diagnosticFixFactory
import org.jetbrains.kotlin.idea.fir.api.fixes.diagnosticFixFactoryFromIntentionActions
import org.jetbrains.kotlin.idea.fir.intentions.HLAddGetterAndSetterIntention
import org.jetbrains.kotlin.idea.fir.intentions.HLAddGetterIntention
import org.jetbrains.kotlin.idea.fir.intentions.HLAddSetterIntention
@@ -13,15 +13,16 @@ import org.jetbrains.kotlin.idea.frontend.api.fir.diagnostics.KtFirDiagnostic
import org.jetbrains.kotlin.psi.KtProperty
object AddAccessorsFactories {
val addAccessorsToUninitializedProperty = diagnosticFixFactory(KtFirDiagnostic.MustBeInitializedOrBeAbstract::class) { diagnostic ->
val property: KtProperty = diagnostic.psi
val addGetter = property.getter == null
val addSetter = property.isVar && property.setter == null
when {
addGetter && addSetter -> listOf(HLAddGetterAndSetterIntention())
addGetter -> listOf(HLAddGetterIntention())
addSetter -> listOf(HLAddSetterIntention())
else -> emptyList()
val addAccessorsToUninitializedProperty =
diagnosticFixFactoryFromIntentionActions(KtFirDiagnostic.MustBeInitializedOrBeAbstract::class) { diagnostic ->
val property: KtProperty = diagnostic.psi
val addGetter = property.getter == null
val addSetter = property.isVar && property.setter == null
when {
addGetter && addSetter -> listOf(HLAddGetterAndSetterIntention())
addGetter -> listOf(HLAddGetterIntention())
addSetter -> listOf(HLAddSetterIntention())
else -> emptyList()
}
}
}
}
@@ -5,7 +5,6 @@
package org.jetbrains.kotlin.idea.quickfix.fixes
import com.intellij.codeInsight.intention.IntentionAction
import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.idea.fir.api.fixes.diagnosticFixFactory
import org.jetbrains.kotlin.idea.frontend.api.KtAnalysisSession
@@ -30,7 +29,7 @@ object AddExclExclCallFixFactories {
getFixForUnsafeCall(diagnostic.psi)
}
private fun KtAnalysisSession.getFixForUnsafeCall(psi: PsiElement): List<IntentionAction> {
private fun KtAnalysisSession.getFixForUnsafeCall(psi: PsiElement): List<AddExclExclCallFix> {
val (target, hasImplicitReceiver) = when (val unwrapped = psi.unwrapParenthesesLabelsAndAnnotations()) {
// `foo.bar` -> `foo!!.bar`
is KtDotQualifiedExpression -> unwrapped.receiverExpression to false
@@ -5,13 +5,13 @@
package org.jetbrains.kotlin.idea.quickfix.fixes
import com.intellij.codeInsight.intention.IntentionAction
import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.idea.fir.api.fixes.diagnosticFixFactory
import org.jetbrains.kotlin.idea.frontend.api.KtAnalysisSession
import org.jetbrains.kotlin.idea.frontend.api.fir.diagnostics.KtFirDiagnostic
import org.jetbrains.kotlin.idea.frontend.api.types.KtType
import org.jetbrains.kotlin.idea.frontend.api.types.KtTypeNullability
import org.jetbrains.kotlin.idea.quickfix.AddExclExclCallFix
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
@@ -36,7 +36,7 @@ object TypeMismatchFactories {
psi: PsiElement,
expectedType: KtType,
actualType: KtType
): List<IntentionAction> {
): List<AddExclExclCallFix> {
// TODO: Add more fixes than just AddExclExclCallFix when available.
if (!expectedType.canBeNull && actualType.canBeNull) {
// We don't want to offer AddExclExclCallFix if we know the expression is definitely null, e.g.:
@@ -5,7 +5,6 @@
package org.jetbrains.kotlin.idea.quickfix.fixes
import com.intellij.codeInsight.intention.IntentionAction
import com.intellij.openapi.diagnostic.Logger
import com.intellij.psi.PsiElement
import com.intellij.psi.SmartPsiElementPointer
@@ -180,7 +179,7 @@ object WrapWithSafeLetCallFixFactories {
private fun KtAnalysisSession.createWrapWithSafeLetCallInputForNullableExpressionIfMoreThanImmediateParentIsWrapped(
nullableExpression: KtExpression?,
isImplicitInvokeCallToMemberProperty: Boolean = false,
): List<IntentionAction> {
): List<HLQuickFix<KtExpression, Input>> {
val surroundingExpression = nullableExpression?.surroundingExpression
if (
surroundingExpression == null ||
@@ -205,7 +204,7 @@ object WrapWithSafeLetCallFixFactories {
isImplicitInvokeCallToMemberProperty: Boolean = false,
surroundingExpression: KtExpression? = findParentExpressionAtNullablePosition(nullableExpression)
?: nullableExpression?.surroundingExpression
): List<IntentionAction> {
): List<HLQuickFix<KtExpression, Input>> {
if (nullableExpression == null || surroundingExpression == null) return emptyList()
val existingNames =
nullableExpression.containingKtFile.getScopeContextForPosition(nullableExpression).scopes.getPossibleCallableNames()