FIR IDE: introduce applicable computation & -based inspection

This commit is contained in:
Ilya Kirillov
2020-08-12 17:30:22 +03:00
parent c8ab0766c9
commit b5a4e4c409
7 changed files with 303 additions and 0 deletions
@@ -0,0 +1,91 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.fir.inspections
import com.intellij.codeInspection.*
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import org.jetbrains.kotlin.idea.frontend.api.KtAnalysisSession
import org.jetbrains.kotlin.idea.frontend.api.analyzeWithReadAction
import org.jetbrains.kotlin.idea.frontend.api.computation.ApplicableComputation
import org.jetbrains.kotlin.idea.inspections.AbstractKotlinInspection
import org.jetbrains.kotlin.idea.inspections.findExistingEditor
import org.jetbrains.kotlin.psi.KtElement
import org.jetbrains.kotlin.psi.KtVisitorVoid
abstract class AbstractHighLevelApiBasedInspection<ELEMENT : KtElement, DATA : Any>(
val elementType: Class<ELEMENT>
) : AbstractKotlinInspection() {
final override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession) =
object : KtVisitorVoid() {
override fun visitKtElement(element: KtElement) {
super.visitKtElement(element)
if (!elementType.isInstance(element) || element.textLength == 0) return
@Suppress("UNCHECKED_CAST")
visitTargetElement(element as ELEMENT, holder, isOnTheFly)
}
}
protected fun visitTargetElement(element: ELEMENT, holder: ProblemsHolder, isOnTheFly: Boolean) {
if (!isApplicableByPsi(element)) return
if (analyzeWithReadAction(element) { analyzeAndGetData(element) == null }) return
holder.registerProblemWithoutOfflineInformation(
element,
inspectionText(element),
isOnTheFly,
inspectionHighlightType(element),
inspectionHighlightRangeInElement(element),
LocalFix(fixText(element))
)
}
open fun inspectionHighlightRangeInElement(element: ELEMENT): TextRange? = null
open fun inspectionHighlightType(element: ELEMENT): ProblemHighlightType = ProblemHighlightType.GENERIC_ERROR_OR_WARNING
abstract fun inspectionText(element: ELEMENT): String
abstract val defaultFixText: String
open fun fixText(element: ELEMENT) = defaultFixText
abstract fun isApplicableByPsi(element: ELEMENT): Boolean
abstract fun KtAnalysisSession.analyzeAndGetData(element: ELEMENT): DATA?
abstract fun applyTo(element: ELEMENT, data: DATA, project: Project = element.project, editor: Editor? = null)
private inner class LocalFix(val text: String) : LocalQuickFix {
override fun startInWriteAction() = false
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
@Suppress("UNCHECKED_CAST")
val element = descriptor.psiElement as ELEMENT
if (!isApplicableByPsi(element)) return
ApplicationManager.getApplication().executeOnPooledThread {
val computation = ApplicableComputation(
computation = { analyzeAndGetData(it) },
application = { element, data ->
applyTo(element, data, project, element.findExistingEditor())
},
psiChecker = ::isApplicableByPsi,
computationTitle = fixText(element)
)
computation.computeAndApply(element)
}
}
override fun getFamilyName() = defaultFixText
override fun getName() = text
}
}
@@ -0,0 +1,55 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.fir.inspections
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.Editor
import org.jetbrains.annotations.Nls
import org.jetbrains.kotlin.idea.frontend.api.KtAnalysisSession
import org.jetbrains.kotlin.idea.frontend.api.analyzeWithReadAction
import org.jetbrains.kotlin.idea.frontend.api.computation.ApplicableComputation
import org.jetbrains.kotlin.idea.intentions.SelfTargetingIntention
import org.jetbrains.kotlin.psi.KtElement
abstract class AbstractHighLevelApiBasedIntention<ELEMENT : KtElement, DATA : Any>(
elementType: Class<ELEMENT>,
@Nls private val textGetter: () -> String,
@Nls familyNameGetter: () -> String = textGetter,
) : SelfTargetingIntention<ELEMENT>(elementType, textGetter, familyNameGetter) {
protected abstract fun isApplicableByPsi(element: ELEMENT): Boolean
protected open fun isApplicableByPsiAtOffset(element: ELEMENT, offset: Int): Boolean =
isApplicableByPsi(element)
protected abstract fun KtAnalysisSession.analyzeAndGetData(element: ELEMENT): DATA?
protected abstract fun applyTo(element: ELEMENT, data: DATA, editor: Editor?)
final override fun isApplicableTo(element: ELEMENT, caretOffset: Int): Boolean {
if (!isApplicableByPsi(element)) return false
if (!isApplicableByPsiAtOffset(element, caretOffset)) return false
val data = ApplicationManager.getApplication().executeOnPooledThread<DATA?> {
analyzeWithReadAction(element) { analyzeAndGetData(element) }
}.get()
return data != null
}
final override fun applyTo(element: ELEMENT, editor: Editor?) {
if (!isApplicableByPsi(element)) return
ApplicationManager.getApplication().executeOnPooledThread {
val computation = ApplicableComputation(
computation = { analyzeAndGetData(it) },
application = { element, data ->
applyTo(element, data, editor)
},
psiChecker = ::isApplicableByPsi,
computationTitle = textGetter()
)
computation.computeAndApply(element)
}
}
}
@@ -0,0 +1,43 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.fir.inspections
import com.intellij.openapi.editor.Editor
import org.jetbrains.kotlin.idea.frontend.api.KtAnalysisSession
import org.jetbrains.kotlin.idea.frontend.api.types.*
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.KtPsiFactory
class AddFunctionReturnTypeIntention :
AbstractHighLevelApiBasedIntention<KtNamedFunction, TypeCandidate>(
KtNamedFunction::class.java,
{ "Specify type explicitly" }
) {
override fun isApplicableByPsi(element: KtNamedFunction): Boolean =
element.typeReference == null && !element.hasBlockBody()
override fun KtAnalysisSession.analyzeAndGetData(element: KtNamedFunction): TypeCandidate? {
val returnType = getReturnTypeForKtDeclaration(element)
val approximated = approximateTypeToUpperDenotable(returnType) ?: return null
return TypeCandidate(KtTypeRenderer.render(approximated))
}
private tailrec fun approximateTypeToUpperDenotable(type: KtType): KtDenotableType? = when (type) {
is KtNonDenotableType -> when (type) {
is KtFlexibleType -> approximateTypeToUpperDenotable(type.upperBound)
is KtIntersectionType -> null
}
is KtDenotableType -> type
else -> null
}
override fun applyTo(element: KtNamedFunction, data: TypeCandidate, editor: Editor?) {
element.typeReference = KtPsiFactory(element).createType(data.candidate)
}
}
data class TypeCandidate(val candidate: String)
@@ -0,0 +1,83 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.frontend.api.computation
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.command.CommandProcessor
import com.intellij.psi.util.PsiModificationTracker
import org.jetbrains.kotlin.idea.frontend.api.KtAnalysisSession
import org.jetbrains.kotlin.idea.frontend.api.analyzeWithReadAction
import org.jetbrains.kotlin.idea.util.application.runWriteAction
import org.jetbrains.kotlin.psi.KtElement
/**
* Computation which computes some value by [computation] in context of [KtAnalysisSession],
* If computations is successful (i.e. it returns non-null value) then it tries to apply result of this computation by [application]
* [application] is ran in EDT & write action and supposed to modify given KtElement somehow
* Application happens only if world has not changed since we called [computation]
* If world changed we [computation] again and again until we success or exceed the number of attempts represented by [tryCount]
* Should be run from non-EDT thread
* [computation] 8 [psiChecker] should be pure functions
*/
class ApplicableComputation<ELEMENT : KtElement, DATA : Any>(
val computation: KtAnalysisSession.(ELEMENT) -> DATA?,
val application: (ELEMENT, DATA) -> Unit,
val psiChecker: (ELEMENT) -> Boolean = { true },
val computationTitle: String,
) {
@Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
fun computeAndApply(element: ELEMENT, tryCount: UInt = UInt.MAX_VALUE): ApplicableComputationResult {
if (!element.isValid) return ApplicableComputationResult.NonApplicable
val ideaApplication = ApplicationManager.getApplication()
if (ideaApplication.isDispatchThread) {
error("ApplicableComputation.apply should be called from non-EDT thread")
}
val project = element.project
val modificationTracker = PsiModificationTracker.SERVICE.getInstance(project)
var completed = false
var exception: Throwable? = null
var tries = 0u
while (!completed && tries < tryCount) {
if (!element.isValid) return ApplicableComputationResult.NonApplicable
if (!psiChecker(element)) return ApplicableComputationResult.NonApplicable
val data = analyzeWithReadAction(element) { computation(element) } ?: return ApplicableComputationResult.NonApplicable
val timestamp = modificationTracker.modificationCount
ideaApplication.invokeAndWait {
CommandProcessor.getInstance().executeCommand(
project,
{
runWriteAction {
tries++
if (modificationTracker.modificationCount == timestamp) {
try {
application(element, data)
} catch (e: Throwable) {
exception = e
} finally {
completed = true
}
}
}
},
computationTitle,
null
)
}
}
return exception?.let(ApplicableComputationResult::WithException) ?: ApplicableComputationResult.Applied
}
}
sealed class ApplicableComputationResult {
@Suppress("SpellCheckingInspection")
object NonApplicable : ApplicableComputationResult()
@Suppress("SpellCheckingInspection")
object Applied : ApplicableComputationResult()
data class WithException(val exception: Throwable) : ApplicableComputationResult()
}
@@ -0,0 +1,20 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.frontend.api.types
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.name.ClassId
val KtType.isUnit: Boolean get() = isClassTypeWithClassId(DefaultTypeClassIds.UNIT)
fun KtType.isClassTypeWithClassId(classId: ClassId): Boolean {
if (this !is KtClassType) return false
return this.classId == classId
}
private object DefaultTypeClassIds {
val UNIT = ClassId.topLevel(KotlinBuiltIns.FQ_NAMES.unit.toSafe())
}
+6
View File
@@ -203,5 +203,11 @@ The Kotlin FIR plugin provides language support in IntelliJ IDEA and Android Stu
implementationClass="org.jetbrains.kotlin.idea.KotlinModuleFileType"
fieldName="INSTANCE"
extensions="kotlin_module"/>
<intentionAction>
<className>org.jetbrains.kotlin.idea.fir.inspections.AddFunctionReturnTypeIntention</className>
<category>Kotlin</category>
</intentionAction>
</extensions>
</idea-plugin>
@@ -0,0 +1,5 @@
<html>
<body>
This intention adds an explicit type specification for functions.
</body>
</html>