WEB-76927 PolySymbols: make it easier to provide generic properties in implementations

(cherry picked from commit 05381d85eb702bf76e1f25bed8e87e794f43ea7b)

IJ-CR-193424

GitOrigin-RevId: 5e7d1cecf23f7e2f6a1d895f4394452255d6a823
This commit is contained in:
Piotr Tomiak
2026-02-20 13:46:08 +01:00
committed by intellij-monorepo-bot
parent 40abaa7658
commit a128acc83e
11 changed files with 172 additions and 33 deletions

View File

@@ -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
- <init>(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

View File

@@ -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",

View File

@@ -7,6 +7,7 @@
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="module" module-name="intellij.libraries.kotlin.reflect" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="kotlin-stdlib" level="project" />
<orderEntry type="module" module-name="intellij.platform.core" />

View File

@@ -1,7 +1,10 @@
<idea-plugin visibility="public">
<dependencies>
<module name="intellij.platform.backend"/>
<!-- region Generated dependencies - run `Generate Product Layouts` to regenerate -->
<module name="intellij.libraries.kotlin.reflect"/>
<module name="intellij.platform.polySymbols"/>
<!-- endregion -->
</dependencies>
<extensionPoints>

View File

@@ -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 <T : Any> get(property: PolySymbolProperty<T>): 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<Priority> {
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 {
/**

View File

@@ -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<T : Any>(
val name: String,
type: Class<T>,
) {
@ApiStatus.NonExtendable
interface PolySymbolProperty<T : Any> {
val type: Class<T> = 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<T : Any> {
}
}
private class PolySymbolPropertyData<T : Any>(override val name: String, private val type: Class<T>) : PolySymbolProperty<T> {
private class PolySymbolPropertyData<T : Any>(name: String, type: Class<T>) : PolySymbolProperty<T>(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
}

View File

@@ -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<Class<out PolySymbol>, Map<PolySymbolProperty<*>, (PolySymbol) -> Any?>> =
ContainerUtil.createConcurrentWeakMap()
fun <T : Any> get(symbol: PolySymbol, property: PolySymbolProperty<T>): T? =
accessorsMap.computeIfAbsent(symbol::class.java) {
buildPropertiesMap(it)
}[property]?.let { property.tryCast(it(symbol)) }
private fun buildPropertiesMap(symbolClass: Class<out PolySymbol>): Map<PolySymbolProperty<*>, (PolySymbol) -> Any?> {
val toVisit = ArrayDeque<Class<*>>()
toVisit.add(symbolClass)
val visited = hashSetOf<Class<*>>()
val result = mutableMapOf<PolySymbolProperty<*>, (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<PolySymbolProperty<*>, (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<PolySymbolProperty<*>, (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<PolySymbolProperty<*>>()
?: try {
propertyClass.java.getDeclaredConstructor()
}
catch (_: NoSuchMethodException) {
null
}
?.newInstance()
?.asSafely<PolySymbolProperty<*>>()
}
}

View File

@@ -51,7 +51,7 @@ interface PolySymbolDelegate<T : PolySymbol> : PolySymbol, PolySymbolScope {
get() = delegate.priority
override fun <T : Any> get(property: PolySymbolProperty<T>): T? =
delegate[property]
super.get(property) ?: delegate[property]
override fun getDocumentationTarget(location: PsiElement?): DocumentationTarget? =
delegate.getDocumentationTarget(location)

View File

@@ -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() }
)

View File

@@ -59,7 +59,7 @@ internal class CommitMessageReferenceProvider : PsiPolySymbolReferenceProvider<P
override fun <T : Any> get(property: PolySymbolProperty<T>): 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

View File

@@ -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