From b5a4e4c4094b66488cf6c3908e58622cb90eaac9 Mon Sep 17 00:00:00 2001 From: Ilya Kirillov Date: Wed, 12 Aug 2020 17:30:22 +0300 Subject: [PATCH] FIR IDE: introduce applicable computation & -based inspection --- .../AbstractHighLevelApiBasedInspection.kt | 91 +++++++++++++++++++ .../AbstractHighLevelApiBasedIntention.kt | 55 +++++++++++ .../AddFunctionReturnTypeIntention.kt | 43 +++++++++ .../api/computation/ApplicableComputation.kt | 83 +++++++++++++++++ .../idea/frontend/api/types/ktTypeUtils.kt | 20 ++++ idea/resources-fir/META-INF/plugin.xml | 6 ++ .../AddFunctionReturnTypeIntention.html | 5 + 7 files changed, 303 insertions(+) create mode 100644 idea/idea-fir/src/org/jetbrains/kotlin/idea/fir/inspections/AbstractHighLevelApiBasedInspection.kt create mode 100644 idea/idea-fir/src/org/jetbrains/kotlin/idea/fir/inspections/AbstractHighLevelApiBasedIntention.kt create mode 100644 idea/idea-fir/src/org/jetbrains/kotlin/idea/fir/inspections/AddFunctionReturnTypeIntention.kt create mode 100644 idea/idea-frontend-api/src/org/jetbrains/kotlin/idea/frontend/api/computation/ApplicableComputation.kt create mode 100644 idea/idea-frontend-api/src/org/jetbrains/kotlin/idea/frontend/api/types/ktTypeUtils.kt create mode 100644 idea/resources-fir/inspectionDescriptions/AddFunctionReturnTypeIntention.html diff --git a/idea/idea-fir/src/org/jetbrains/kotlin/idea/fir/inspections/AbstractHighLevelApiBasedInspection.kt b/idea/idea-fir/src/org/jetbrains/kotlin/idea/fir/inspections/AbstractHighLevelApiBasedInspection.kt new file mode 100644 index 00000000000..072a989ec9c --- /dev/null +++ b/idea/idea-fir/src/org/jetbrains/kotlin/idea/fir/inspections/AbstractHighLevelApiBasedInspection.kt @@ -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( + val elementType: Class +) : 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 + } +} \ No newline at end of file diff --git a/idea/idea-fir/src/org/jetbrains/kotlin/idea/fir/inspections/AbstractHighLevelApiBasedIntention.kt b/idea/idea-fir/src/org/jetbrains/kotlin/idea/fir/inspections/AbstractHighLevelApiBasedIntention.kt new file mode 100644 index 00000000000..558e57ba725 --- /dev/null +++ b/idea/idea-fir/src/org/jetbrains/kotlin/idea/fir/inspections/AbstractHighLevelApiBasedIntention.kt @@ -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( + elementType: Class, + @Nls private val textGetter: () -> String, + @Nls familyNameGetter: () -> String = textGetter, +) : SelfTargetingIntention(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 { + 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) + } + } +} \ No newline at end of file diff --git a/idea/idea-fir/src/org/jetbrains/kotlin/idea/fir/inspections/AddFunctionReturnTypeIntention.kt b/idea/idea-fir/src/org/jetbrains/kotlin/idea/fir/inspections/AddFunctionReturnTypeIntention.kt new file mode 100644 index 00000000000..9205b8a7893 --- /dev/null +++ b/idea/idea-fir/src/org/jetbrains/kotlin/idea/fir/inspections/AddFunctionReturnTypeIntention.kt @@ -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::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) \ No newline at end of file diff --git a/idea/idea-frontend-api/src/org/jetbrains/kotlin/idea/frontend/api/computation/ApplicableComputation.kt b/idea/idea-frontend-api/src/org/jetbrains/kotlin/idea/frontend/api/computation/ApplicableComputation.kt new file mode 100644 index 00000000000..e25d2613f38 --- /dev/null +++ b/idea/idea-frontend-api/src/org/jetbrains/kotlin/idea/frontend/api/computation/ApplicableComputation.kt @@ -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( + 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() +} \ No newline at end of file diff --git a/idea/idea-frontend-api/src/org/jetbrains/kotlin/idea/frontend/api/types/ktTypeUtils.kt b/idea/idea-frontend-api/src/org/jetbrains/kotlin/idea/frontend/api/types/ktTypeUtils.kt new file mode 100644 index 00000000000..58fcc33aae8 --- /dev/null +++ b/idea/idea-frontend-api/src/org/jetbrains/kotlin/idea/frontend/api/types/ktTypeUtils.kt @@ -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()) +} \ No newline at end of file diff --git a/idea/resources-fir/META-INF/plugin.xml b/idea/resources-fir/META-INF/plugin.xml index a9d9fb852ed..db9e08c851e 100644 --- a/idea/resources-fir/META-INF/plugin.xml +++ b/idea/resources-fir/META-INF/plugin.xml @@ -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"/> + + + org.jetbrains.kotlin.idea.fir.inspections.AddFunctionReturnTypeIntention + Kotlin + + diff --git a/idea/resources-fir/inspectionDescriptions/AddFunctionReturnTypeIntention.html b/idea/resources-fir/inspectionDescriptions/AddFunctionReturnTypeIntention.html new file mode 100644 index 00000000000..f29f4353573 --- /dev/null +++ b/idea/resources-fir/inspectionDescriptions/AddFunctionReturnTypeIntention.html @@ -0,0 +1,5 @@ + + +This intention adds an explicit type specification for functions. + + \ No newline at end of file