From e720433c8ee6d4f050ee32cd79618bc7513c3fd5 Mon Sep 17 00:00:00 2001 From: David Herman Date: Mon, 24 Jun 2024 12:06:52 -0700 Subject: [PATCH] Update logic for checking annotations to avoid using internal analyze API Bug #49 --- .../FunctionNameInspectionSuppressor.kt | 17 +------ .../inspections/UnusedInspectionSuppressor.kt | 13 +---- .../intellij/util/psi/KtElementExtensions.kt | 50 +++++++++++++++++++ 3 files changed, 54 insertions(+), 26 deletions(-) create mode 100644 plugin/src/main/kotlin/com/varabyte/kobweb/intellij/util/psi/KtElementExtensions.kt diff --git a/plugin/src/main/kotlin/com/varabyte/kobweb/intellij/inspections/FunctionNameInspectionSuppressor.kt b/plugin/src/main/kotlin/com/varabyte/kobweb/intellij/inspections/FunctionNameInspectionSuppressor.kt index 1d97253..eb6a86e 100644 --- a/plugin/src/main/kotlin/com/varabyte/kobweb/intellij/inspections/FunctionNameInspectionSuppressor.kt +++ b/plugin/src/main/kotlin/com/varabyte/kobweb/intellij/inspections/FunctionNameInspectionSuppressor.kt @@ -5,14 +5,9 @@ import com.intellij.codeInspection.SuppressQuickFix import com.intellij.psi.PsiElement import com.varabyte.kobweb.intellij.util.kobweb.isInKobwebSource import com.varabyte.kobweb.intellij.util.kobweb.isInReadableKobwebProject -import org.jetbrains.kotlin.analysis.api.analyze -import org.jetbrains.kotlin.analysis.api.annotations.annotationClassIds +import com.varabyte.kobweb.intellij.util.psi.hasAnyAnnotation import org.jetbrains.kotlin.psi.KtNamedFunction -private val SUPPRESS_FUNCTION_NAME_WHEN_ANNOTATED_WITH = arrayOf( - "androidx.compose.runtime.Composable", -) - /** * Suppress the "Function name should start with a lowercase letter" inspection. */ @@ -22,15 +17,7 @@ class FunctionNameInspectionSuppressor : InspectionSuppressor { if (!element.isInReadableKobwebProject() && !element.isInKobwebSource()) return false val ktFunction = element.parent as? KtNamedFunction ?: return false - analyze(ktFunction) { - val symbol = ktFunction.getSymbol() - - symbol.annotationClassIds.forEach { - if (it.asFqNameString() in SUPPRESS_FUNCTION_NAME_WHEN_ANNOTATED_WITH) return true - } - } - - return false + return ktFunction.hasAnyAnnotation("androidx.compose.runtime.Composable") } override fun getSuppressActions(element: PsiElement?, toolId: String) = emptyArray() diff --git a/plugin/src/main/kotlin/com/varabyte/kobweb/intellij/inspections/UnusedInspectionSuppressor.kt b/plugin/src/main/kotlin/com/varabyte/kobweb/intellij/inspections/UnusedInspectionSuppressor.kt index 361f7c1..1504d00 100644 --- a/plugin/src/main/kotlin/com/varabyte/kobweb/intellij/inspections/UnusedInspectionSuppressor.kt +++ b/plugin/src/main/kotlin/com/varabyte/kobweb/intellij/inspections/UnusedInspectionSuppressor.kt @@ -4,8 +4,7 @@ import com.intellij.codeInspection.InspectionSuppressor import com.intellij.codeInspection.SuppressQuickFix import com.intellij.psi.PsiElement import com.varabyte.kobweb.intellij.util.kobweb.isInReadableKobwebProject -import org.jetbrains.kotlin.analysis.api.analyze -import org.jetbrains.kotlin.analysis.api.annotations.annotationClassIds +import com.varabyte.kobweb.intellij.util.psi.hasAnyAnnotation import org.jetbrains.kotlin.psi.KtNamedFunction private val SUPPRESS_UNUSED_WHEN_ANNOTATED_WITH = arrayOf( @@ -26,15 +25,7 @@ class UnusedInspectionSuppressor : InspectionSuppressor { if (!element.isInReadableKobwebProject()) return false val ktFunction = element.parent as? KtNamedFunction ?: return false - analyze(ktFunction) { - val symbol = ktFunction.getSymbol() - - symbol.annotationClassIds.forEach { - if (it.asFqNameString() in SUPPRESS_UNUSED_WHEN_ANNOTATED_WITH) return true - } - } - - return false + return ktFunction.hasAnyAnnotation(*SUPPRESS_UNUSED_WHEN_ANNOTATED_WITH) } override fun getSuppressActions(element: PsiElement?, toolId: String) = emptyArray() diff --git a/plugin/src/main/kotlin/com/varabyte/kobweb/intellij/util/psi/KtElementExtensions.kt b/plugin/src/main/kotlin/com/varabyte/kobweb/intellij/util/psi/KtElementExtensions.kt new file mode 100644 index 0000000..134b013 --- /dev/null +++ b/plugin/src/main/kotlin/com/varabyte/kobweb/intellij/util/psi/KtElementExtensions.kt @@ -0,0 +1,50 @@ +package com.varabyte.kobweb.intellij.util.psi + +import com.intellij.openapi.roots.ProjectRootModificationTracker +import com.intellij.psi.util.CachedValueProvider +import com.intellij.psi.util.CachedValuesManager +import org.jetbrains.kotlin.idea.base.psi.kotlinFqName +import org.jetbrains.kotlin.idea.caches.resolve.analyze +import org.jetbrains.kotlin.psi.KtAnnotated +import org.jetbrains.kotlin.psi.KtAnnotationEntry +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode + +/** + * Determines whether this [KtAnnotationEntry] has the specified qualified name. + * Careful: this does *not* currently take into account Kotlin type aliases (https://kotlinlang.org/docs/reference/type-aliases.html). + * Fortunately, type aliases are extremely uncommon for simple annotation types. + */ +private fun KtAnnotationEntry.fqNameMatches(fqName: String): Boolean { + // For inspiration, see IDELightClassGenerationSupport.KtUltraLightSupportImpl.findAnnotation in the Kotlin plugin. + val shortName = shortName?.asString() ?: return false + return fqName.endsWith(".$shortName") && fqName == getQualifiedName() +} + +/** + * Computes the qualified name of this [KtAnnotationEntry]. + * Prefer to use [fqNameMatches], which checks the short name first and thus has better performance. + */ +private fun KtAnnotationEntry.getQualifiedName(): String? = + analyze(BodyResolveMode.PARTIAL).get(BindingContext.ANNOTATION, this)?.fqName?.asString() + +/** + * Returns true if the function is tagged with any one of the given annotations. + * + * The annotation name must be fully-qualified, as in "androidx.compose.runtime.Composable". + */ +internal fun KtAnnotated.hasAnyAnnotation(vararg annotationFqns: String): Boolean { + // Code adapted from https://github.com/JetBrains/compose-multiplatform/blob/b501e0f794aecde9a6ce47cb4b5308939cbc7cc5/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/locationUtils.kt#L135 + return CachedValuesManager.getCachedValue(this) { + CachedValueProvider.Result.create( + run { + this.annotationEntries.any { annotationEntry -> + annotationFqns.any { fqName -> annotationEntry.fqNameMatches(fqName) } + } + }, + this.containingKtFile, + ProjectRootModificationTracker.getInstance(project), + *annotationFqns + ) + } +}