From a128acc83e8ab48cbd377cf6c0f47bf0ab5e120c Mon Sep 17 00:00:00 2001 From: Piotr Tomiak Date: Fri, 20 Feb 2026 13:46:08 +0100 Subject: [PATCH] WEB-76927 PolySymbols: make it easier to provide generic properties in implementations (cherry picked from commit 05381d85eb702bf76e1f25bed8e87e794f43ea7b) IJ-CR-193424 GitOrigin-RevId: 5e7d1cecf23f7e2f6a1d895f4394452255d6a823 --- .../polySymbols/api-dump-experimental.txt | 15 ++- platform/polySymbols/backend/BUILD.bazel | 1 + .../intellij.platform.polySymbols.backend.iml | 1 + .../intellij.platform.polySymbols.backend.xml | 3 + .../com/intellij/polySymbols/PolySymbol.kt | 10 +- .../polySymbols/PolySymbolProperty.kt | 46 +++---- .../impl/PolySymbolPropertyGetter.kt | 121 ++++++++++++++++++ .../polySymbols/utils/PolySymbolDelegate.kt | 2 +- .../query/PolySymbolsDebugOutputPrinter.kt | 2 +- .../CommitMessageReferenceProvider.kt | 2 +- .../src/com/intellij/tasks/core/TaskSymbol.kt | 2 +- 11 files changed, 172 insertions(+), 33 deletions(-) create mode 100644 platform/polySymbols/src/com/intellij/polySymbols/impl/PolySymbolPropertyGetter.kt diff --git a/platform/polySymbols/api-dump-experimental.txt b/platform/polySymbols/api-dump-experimental.txt index 629c6fb3157e..fad915f5a262 100644 --- a/platform/polySymbols/api-dump-experimental.txt +++ b/platform/polySymbols/api-dump-experimental.txt @@ -43,6 +43,9 @@ - a:getValue():D *f:com.intellij.polySymbols.PolySymbol$Priority$Companion - f:custom(D):com.intellij.polySymbols.PolySymbol$Priority +*@:com.intellij.polySymbols.PolySymbol$Property +- java.lang.annotation.Annotation +- a:property():java.lang.Class *:com.intellij.polySymbols.PolySymbolApiStatus - *sf:Companion:com.intellij.polySymbols.PolySymbolApiStatus$Companion - sf:Deprecated:com.intellij.polySymbols.PolySymbolApiStatus$Deprecated @@ -200,11 +203,15 @@ - s:getEntries():kotlin.enums.EnumEntries - s:valueOf(java.lang.String):com.intellij.polySymbols.PolySymbolNameSegment$MatchProblem - s:values():com.intellij.polySymbols.PolySymbolNameSegment$MatchProblem[] -*:com.intellij.polySymbols.PolySymbolProperty +*a:com.intellij.polySymbols.PolySymbolProperty - *sf:Companion:com.intellij.polySymbols.PolySymbolProperty$Companion -- s:get(java.lang.String,java.lang.Class):com.intellij.polySymbols.PolySymbolProperty -- a:getName():java.lang.String -- a:tryCast(java.lang.Object):java.lang.Object +- (java.lang.String,java.lang.Class):V +- f:equals(java.lang.Object):Z +- sf:get(java.lang.String,java.lang.Class):com.intellij.polySymbols.PolySymbolProperty +- f:getName():java.lang.String +- f:getType():java.lang.Class +- f:hashCode():I +- f:tryCast(java.lang.Object):java.lang.Object *f:com.intellij.polySymbols.PolySymbolProperty$Companion - f:get(java.lang.String,java.lang.Class):com.intellij.polySymbols.PolySymbolProperty *:com.intellij.polySymbols.PolySymbolQualifiedName diff --git a/platform/polySymbols/backend/BUILD.bazel b/platform/polySymbols/backend/BUILD.bazel index a46ab0a85194..45b453d86729 100644 --- a/platform/polySymbols/backend/BUILD.bazel +++ b/platform/polySymbols/backend/BUILD.bazel @@ -14,6 +14,7 @@ jvm_library( srcs = glob(["src/**/*.kt", "src/**/*.java", "src/**/*.form"], allow_empty = True), resources = [":backend_resources"], deps = [ + "//libraries/kotlin/reflect", "@lib//:kotlin-stdlib", "//platform/core-api:core", "//platform/projectModel-impl", diff --git a/platform/polySymbols/backend/intellij.platform.polySymbols.backend.iml b/platform/polySymbols/backend/intellij.platform.polySymbols.backend.iml index 60daf3ea31d2..b79d60c9a9f0 100644 --- a/platform/polySymbols/backend/intellij.platform.polySymbols.backend.iml +++ b/platform/polySymbols/backend/intellij.platform.polySymbols.backend.iml @@ -7,6 +7,7 @@ + diff --git a/platform/polySymbols/backend/resources/intellij.platform.polySymbols.backend.xml b/platform/polySymbols/backend/resources/intellij.platform.polySymbols.backend.xml index 94d2fb03bf33..892545d5e3ad 100644 --- a/platform/polySymbols/backend/resources/intellij.platform.polySymbols.backend.xml +++ b/platform/polySymbols/backend/resources/intellij.platform.polySymbols.backend.xml @@ -1,7 +1,10 @@ + + + diff --git a/platform/polySymbols/src/com/intellij/polySymbols/PolySymbol.kt b/platform/polySymbols/src/com/intellij/polySymbols/PolySymbol.kt index 1780d2b75625..d8c0f3052358 100644 --- a/platform/polySymbols/src/com/intellij/polySymbols/PolySymbol.kt +++ b/platform/polySymbols/src/com/intellij/polySymbols/PolySymbol.kt @@ -16,6 +16,7 @@ import com.intellij.platform.backend.presentation.TargetPresentation import com.intellij.polySymbols.PolySymbol.Companion.PROP_DOC_HIDE_ICON import com.intellij.polySymbols.context.PolyContext import com.intellij.polySymbols.documentation.PolySymbolDocumentationCustomizer +import com.intellij.polySymbols.impl.PolySymbolPropertyGetter import com.intellij.polySymbols.query.PolySymbolMatch import com.intellij.polySymbols.query.PolySymbolMatchCustomizer import com.intellij.polySymbols.query.PolySymbolQueryExecutor @@ -38,6 +39,7 @@ import com.intellij.util.concurrency.annotations.RequiresReadLock import org.jetbrains.annotations.ApiStatus import java.util.Locale import javax.swing.Icon +import kotlin.reflect.KClass /** * The core element of the Poly Symbols framework. It is identified through `name` and `kind` properties. @@ -165,7 +167,7 @@ interface PolySymbol : Symbol, NavigatableSymbol, PolySymbolPrioritizedScope { * [PolySymbolProperty.tryCast] method for returned values. */ operator fun get(property: PolySymbolProperty): T? = - null + PolySymbolPropertyGetter.get(this, property) /** * Returns [TargetPresentation] used by [SearchTarget] and [RenameTarget]. @@ -273,7 +275,6 @@ interface PolySymbol : Symbol, NavigatableSymbol, PolySymbolPrioritizedScope { ): String = queryExecutor.namesProvider.adjustRename(oldName, newName, occurence) - sealed interface Priority : Comparable { val value: Double @@ -312,6 +313,11 @@ interface PolySymbol : Symbol, NavigatableSymbol, PolySymbolPrioritizedScope { } } + @Target(AnnotationTarget.PROPERTY, AnnotationTarget.FIELD, AnnotationTarget.PROPERTY_GETTER, + AnnotationTarget.ANNOTATION_CLASS) + @Retention(AnnotationRetention.RUNTIME) + annotation class Property(val property: KClass<*>) + companion object { /** diff --git a/platform/polySymbols/src/com/intellij/polySymbols/PolySymbolProperty.kt b/platform/polySymbols/src/com/intellij/polySymbols/PolySymbolProperty.kt index 603a6f5421a9..673fb28acbd4 100644 --- a/platform/polySymbols/src/com/intellij/polySymbols/PolySymbolProperty.kt +++ b/platform/polySymbols/src/com/intellij/polySymbols/PolySymbolProperty.kt @@ -1,14 +1,30 @@ // Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.polySymbols -import org.jetbrains.annotations.ApiStatus +abstract class PolySymbolProperty( + val name: String, + type: Class, +) { -@ApiStatus.NonExtendable -interface PolySymbolProperty { + val type: Class = if (type.isPrimitive) type.kotlin.javaObjectType else type - val name: String + @Suppress("UNCHECKED_CAST") + fun tryCast(value: Any?): T? = + if (value != null && this.type.isInstance(value)) value as T? else null - fun tryCast(value: Any?): T? + final override fun equals(other: Any?): Boolean = + other === this || + other is PolySymbolPropertyData<*> + && other.name == name + && other.type == type + + final override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + type.hashCode() + return result + } + + override fun toString(): String = name + ": " + type.simpleName + " (" + javaClass.simpleName + ")" companion object { @JvmStatic @@ -21,24 +37,8 @@ interface PolySymbolProperty { } } -private class PolySymbolPropertyData(override val name: String, private val type: Class) : PolySymbolProperty { +private class PolySymbolPropertyData(name: String, type: Class) : PolySymbolProperty(name, type) { - @Suppress("UNCHECKED_CAST") - override fun tryCast(value: Any?): T? = - if (value != null && this.type.isInstance(value)) value as T? else null - - override fun equals(other: Any?): Boolean = - other === this || - other is PolySymbolPropertyData<*> - && other.name == name - && other.type == type - - override fun hashCode(): Int { - var result = name.hashCode() - result = 31 * result + type.hashCode() - return result - } - - override fun toString(): String = name + override fun toString(): String = name + ": " + type.simpleName } \ No newline at end of file diff --git a/platform/polySymbols/src/com/intellij/polySymbols/impl/PolySymbolPropertyGetter.kt b/platform/polySymbols/src/com/intellij/polySymbols/impl/PolySymbolPropertyGetter.kt new file mode 100644 index 000000000000..85fd431be510 --- /dev/null +++ b/platform/polySymbols/src/com/intellij/polySymbols/impl/PolySymbolPropertyGetter.kt @@ -0,0 +1,121 @@ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.polySymbols.impl + +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.util.text.StringUtil +import com.intellij.polySymbols.PolySymbol +import com.intellij.polySymbols.PolySymbolProperty +import com.intellij.util.asSafely +import com.intellij.util.containers.ContainerUtil +import java.lang.reflect.Field +import java.lang.reflect.Method + +object PolySymbolPropertyGetter { + + private val accessorsMap: MutableMap, Map, (PolySymbol) -> Any?>> = + ContainerUtil.createConcurrentWeakMap() + + fun get(symbol: PolySymbol, property: PolySymbolProperty): T? = + accessorsMap.computeIfAbsent(symbol::class.java) { + buildPropertiesMap(it) + }[property]?.let { property.tryCast(it(symbol)) } + + private fun buildPropertiesMap(symbolClass: Class): Map, (PolySymbol) -> Any?> { + val toVisit = ArrayDeque>() + toVisit.add(symbolClass) + val visited = hashSetOf>() + + val result = mutableMapOf, (PolySymbol) -> Any?>() + + while (toVisit.isNotEmpty()) { + val clazz = toVisit.removeFirst() + if (!visited.add(clazz)) continue + + for (field in clazz.declaredFields) { + val property = getProperty(field.getDeclaredAnnotation(PolySymbol.Property::class.java)) + ?: continue + addField(property, field, result, clazz) + } + for (method in clazz.declaredMethods) { + val property = getProperty(method.getDeclaredAnnotation(PolySymbol.Property::class.java)) + ?: continue + addMethod(property, method, result, clazz) + } + clazz.superclass?.let { toVisit.add(it) } + toVisit.addAll(clazz.interfaces) + } + return result + } + + private fun addField( + property: PolySymbolProperty<*>?, + field: Field, + result: MutableMap, (PolySymbol) -> Any?>, + clazz: Class<*>, + ) { + if (property == null || result.containsKey(property)) return + if (property.type.objectType.isAssignableFrom(field.type.objectType)) { + field.isAccessible = true + result[property] = { field.get(it) } + } + else { + thisLogger().error("PolySymbol property ${property.name} of type ${property.type} is not assignable from field type ${field.type} of ${clazz.name}.${field.name}") + } + } + + private fun addMethod( + property: PolySymbolProperty<*>?, + method: Method, + result: MutableMap, (PolySymbol) -> Any?>, + clazz: Class<*>, + ) { + if (property == null || result.containsKey(property)) return + val actualMethod = if (method.name.endsWith($$"$annotations")) + try { + clazz.getDeclaredMethod(method.name.removeSuffix($$"$annotations")) + } + catch (_: NoSuchMethodException) { + if (method.name.startsWith("get")) { + try { + val field = clazz.getDeclaredField( + method.name.removeSuffix($$"$annotations").removePrefix("get").let { StringUtil.decapitalize(it) } + ) + addField(property, field, result, clazz) + return + } + catch (_: NoSuchFieldException) { + } + } + method + } + else + method + if (actualMethod.parameterCount != 0) { + thisLogger().error("Method ${clazz.name}.${actualMethod.name} annotated with @PolySymbol.Property should not have parameters.") + } + else if (property.type.objectType.isAssignableFrom(actualMethod.returnType.objectType)) { + actualMethod.isAccessible = true + result[property] = { actualMethod.invoke(it) } + } + else { + thisLogger().error("PolySymbol property ${property.name} of type ${property.type} is not assignable from return type ${actualMethod.returnType} of ${clazz.name}.${actualMethod.name}()") + } + } + + private val Class<*>.objectType: Class<*> + get() = if (isPrimitive) kotlin.javaObjectType else this + + private fun getProperty(annotation: PolySymbol.Property?): PolySymbolProperty<*>? { + val propertyClass = annotation?.property ?: return null + @Suppress("NO_REFLECTION_IN_CLASS_PATH") + return propertyClass.objectInstance.asSafely>() + ?: try { + propertyClass.java.getDeclaredConstructor() + } + catch (_: NoSuchMethodException) { + null + } + ?.newInstance() + ?.asSafely>() + } +} diff --git a/platform/polySymbols/src/com/intellij/polySymbols/utils/PolySymbolDelegate.kt b/platform/polySymbols/src/com/intellij/polySymbols/utils/PolySymbolDelegate.kt index 3600ff4130e2..445ba77a4c2e 100644 --- a/platform/polySymbols/src/com/intellij/polySymbols/utils/PolySymbolDelegate.kt +++ b/platform/polySymbols/src/com/intellij/polySymbols/utils/PolySymbolDelegate.kt @@ -51,7 +51,7 @@ interface PolySymbolDelegate : PolySymbol, PolySymbolScope { get() = delegate.priority override fun get(property: PolySymbolProperty): T? = - delegate[property] + super.get(property) ?: delegate[property] override fun getDocumentationTarget(location: PsiElement?): DocumentationTarget? = delegate.getDocumentationTarget(location) diff --git a/platform/polySymbols/testFramework/com/intellij/polySymbols/testFramework/query/PolySymbolsDebugOutputPrinter.kt b/platform/polySymbols/testFramework/com/intellij/polySymbols/testFramework/query/PolySymbolsDebugOutputPrinter.kt index c197ac07bdcd..a74d5dd7dc28 100644 --- a/platform/polySymbols/testFramework/com/intellij/polySymbols/testFramework/query/PolySymbolsDebugOutputPrinter.kt +++ b/platform/polySymbols/testFramework/com/intellij/polySymbols/testFramework/query/PolySymbolsDebugOutputPrinter.kt @@ -111,7 +111,7 @@ open class PolySymbolsDebugOutputPrinter : DebugOutputPrinter() { level, "properties", propertiesToPrint .sortedBy { it.name } - .mapNotNull { prop -> source[prop]?.let { Pair(prop, it) } } + .mapNotNull { prop -> source[prop]?.let { Pair(prop.name, it) } } .toMap() .takeIf { it.isNotEmpty() } ) diff --git a/platform/vcs-impl/src/com/intellij/psi/impl/source/resolve/reference/CommitMessageReferenceProvider.kt b/platform/vcs-impl/src/com/intellij/psi/impl/source/resolve/reference/CommitMessageReferenceProvider.kt index 144a7ff8d0a0..9b67c603a354 100644 --- a/platform/vcs-impl/src/com/intellij/psi/impl/source/resolve/reference/CommitMessageReferenceProvider.kt +++ b/platform/vcs-impl/src/com/intellij/psi/impl/source/resolve/reference/CommitMessageReferenceProvider.kt @@ -59,7 +59,7 @@ internal class CommitMessageReferenceProvider : PsiPolySymbolReferenceProvider

get(property: PolySymbolProperty): T? = when (property) { PolySymbol.PROP_IJ_TEXT_ATTRIBUTES_KEY -> property.tryCast(EditorColors.REFERENCE_HYPERLINK_COLOR.externalName) - else -> null + else -> super.get(property) } override val presentation: TargetPresentation diff --git a/plugins/tasks/tasks-core/src/com/intellij/tasks/core/TaskSymbol.kt b/plugins/tasks/tasks-core/src/com/intellij/tasks/core/TaskSymbol.kt index a8171a695575..8da18f4e3e8c 100644 --- a/plugins/tasks/tasks-core/src/com/intellij/tasks/core/TaskSymbol.kt +++ b/plugins/tasks/tasks-core/src/com/intellij/tasks/core/TaskSymbol.kt @@ -159,7 +159,7 @@ sealed class AbstractTaskSymbol : PolySymbol, DocumentationSymbol { when (property) { PolySymbol.PROP_IJ_TEXT_ATTRIBUTES_KEY -> property.tryCast(EditorColors.REFERENCE_HYPERLINK_COLOR.externalName) TASK_PROPERTY -> property.tryCast(task) - else -> null + else -> super.get(property) } override val presentation: TargetPresentation