[devkit] IJPL-116514 Render EP gutter icons for EP interface classes

GitOrigin-RevId: 43e4e1bdf2645dac421fe5feff43163f72f87520
This commit is contained in:
Karol Lewandowski
2026-02-09 17:51:12 +00:00
committed by intellij-monorepo-bot
parent 4a069c3ac8
commit b0a1ff1fba
32 changed files with 143 additions and 324 deletions
@@ -2,133 +2,84 @@
package org.jetbrains.idea.devkit.navigation
import com.intellij.codeInsight.daemon.RelatedItemLineMarkerInfo
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.extensions.ProjectExtensionPointName
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.KeyedExtensionCollector
import com.intellij.openapi.roots.ProjectFileIndex
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiElement
import com.intellij.psi.SmartPointerManager
import com.intellij.psi.util.InheritanceUtil
import com.intellij.psi.util.PsiTypesUtil
import org.jetbrains.annotations.NonNls
import org.jetbrains.idea.devkit.dom.index.ExtensionPointIndex
import org.jetbrains.idea.devkit.dom.ExtensionPoint
import org.jetbrains.idea.devkit.dom.index.ExtensionPointClassIndex
import org.jetbrains.idea.devkit.util.ExtensionPointCandidate
import org.jetbrains.idea.devkit.util.PluginRelatedLocatorsUtils
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UDeclaration
import org.jetbrains.uast.UExpression
import org.jetbrains.uast.UField
import org.jetbrains.uast.UQualifiedReferenceExpression
import org.jetbrains.uast.UastCallKind
import org.jetbrains.uast.evaluateString
import org.jetbrains.uast.expressions.UInjectionHost
import org.jetbrains.uast.getParentOfType
import org.jetbrains.uast.getUParentForIdentifier
import org.jetbrains.uast.sourcePsiElement
import org.jetbrains.uast.UClass
import org.jetbrains.uast.toUElement
/**
* Provides gutter icon for EP code declaration to matching `<extensionPoint>` in `plugin.xml`.
* Provides gutter icon for EP interface class to matching `<extensionPoint>` in `plugin.xml`.
*/
internal class ExtensionPointDeclarationRelatedItemLineMarkerProvider : DevkitRelatedLineMarkerProviderBase() {
override fun collectNavigationMarkers(element: PsiElement, result: MutableCollection<in RelatedItemLineMarkerInfo<*>?>) {
val uElement = getUParentForIdentifier(element)
if (uElement is UField) {
if (!isExtensionPointNameDeclarationField(uElement)) return
process(resolveEpFqn(uElement), uElement, element.getProject(), result)
} else if (uElement is UCallExpression) {
if (!isExtensionPointNameDeclarationViaSuperCall(uElement)) return
val uClass = element.toUElement(UClass::class.java) ?: return
val psiClass = uClass.javaPsi
val project = psiClass.project
val scope = PluginRelatedLocatorsUtils.getCandidatesScope(project)
val uDeclaration = checkNotNull(uElement.getParentOfType(UDeclaration::class.java)) { uElement.asSourceString() }
process(resolveEpFqn(uElement), uDeclaration, element.getProject(), result)
}
}
val extensionPoints = ExtensionPointClassIndex.getExtensionPointsByClass(project, psiClass, scope)
if (extensionPoints.isEmpty()) return
private fun process(
@NonNls epFqn: @NonNls String?,
uDeclaration: UDeclaration,
project: Project,
result: MutableCollection<in RelatedItemLineMarkerInfo<*>?>
) {
if (epFqn == null) return
// Filter to only include EPs where this class is the interface or with.implements, not just the beanClass
val relevantEps = filterRelevantExtensionPoints(psiClass, extensionPoints)
if (relevantEps.isEmpty()) return
val point = ExtensionPointIndex.findExtensionPoint(project, PluginRelatedLocatorsUtils.getCandidatesScope(project), epFqn)
if (point == null) return
val classIdentifier = uClass.uastAnchor?.sourcePsi ?: return
val identifier = uDeclaration.uastAnchor.sourcePsiElement
if (identifier == null) return
val candidate = ExtensionPointCandidate(SmartPointerManager.createPointer(point.getXmlTag()), epFqn)
val info = LineMarkerInfoHelper.createExtensionPointLineMarkerInfo(mutableListOf<ExtensionPointCandidate?>(candidate), identifier)
val targets = relevantEps.map { ExtensionPointCandidate(SmartPointerManager.createPointer(it.xmlTag), it.effectiveQualifiedName) }
val info = LineMarkerInfoHelper.createExtensionPointLineMarkerInfo(targets, classIdentifier)
result.add(info)
}
private fun isExtensionPointNameDeclarationViaSuperCall(uCallExpression: UCallExpression): Boolean {
if (uCallExpression.valueArgumentCount != 1) return false
if (uCallExpression.kind !== UastCallKind.CONSTRUCTOR_CALL) {
if (uCallExpression.kind !== UastCallKind.METHOD_CALL && uCallExpression.methodName != "super") {
return false
/**
* Filters extension points to only include those where the class is referenced as
* `interface` or `with.implements`, excluding those where it's only `beanClass`
* (it would show hundreds of inlay hints for classes like `LanguageExtensionPoint`).
*/
private fun filterRelevantExtensionPoints(psiClass: PsiClass, extensionPoints: List<ExtensionPoint>): List<ExtensionPoint> {
val classQualifiedName = psiClass.qualifiedName ?: return emptyList()
val relevantEps = extensionPoints.filter { ep ->
// include if class is the EP interface
if (ep.`interface`.stringValue == classQualifiedName) {
return@filter true
}
// include if class is the implementationClass of EP's `with`
for (withElement in ep.withElements) {
if (withElement.attribute.stringValue == "implementationClass" && withElement.implements.stringValue == classQualifiedName) {
return@filter true
}
}
false
}
// Kotlin EP_NAME field with CTOR call -> handled by UField branch
if (uCallExpression.getParentOfType(UField::class.java) != null) {
return false
// handle duplications from Kotlin compiler frontend (OSIP-191)
val epsByQualifiedName = relevantEps.groupBy { it.effectiveQualifiedName }
return epsByQualifiedName.values.flatMap {
if (it.size > 1) {
val epsWithoutLibs = extensionPointsWithoutLibs(psiClass, it)
if (epsWithoutLibs.any()) {
return@flatMap epsWithoutLibs
}
}
it
}
val resolvedMethod = uCallExpression.resolve()
if (resolvedMethod == null) return false
if (!resolvedMethod.isConstructor()) return false
return InheritanceUtil.isInheritor(resolvedMethod.getContainingClass(), KeyedExtensionCollector::class.java.name)
}
@NonNls
private fun resolveEpFqn(uCallExpression: UCallExpression): @NonNls String? {
val uParameter = uCallExpression.getArgumentForParameter(0)
if (uParameter == null) return null
return uParameter.evaluateString()
}
@NonNls
private fun resolveEpFqn(uField: UField): @NonNls String? {
val initializer = uField.uastInitializer
var epNameExpression: UExpression? = null
if (initializer is UCallExpression) {
epNameExpression = initializer.getArgumentForParameter(0)
} else if (initializer is UQualifiedReferenceExpression) {
val selector = initializer.selector
if (selector !is UCallExpression) return null
epNameExpression = selector.getArgumentForParameter(0)
private fun extensionPointsWithoutLibs(psiClass: PsiClass, relevantEps: List<ExtensionPoint>): List<ExtensionPoint> {
val project = psiClass.project
val projectFileIndex = ProjectFileIndex.getInstance(project)
val epsWithoutLibs = relevantEps.filter {
val virtualFile = it.xmlElement?.containingFile?.virtualFile ?: return@filter false
!projectFileIndex.isInLibrary(virtualFile)
}
if (epNameExpression == null) return null
if (epNameExpression is UInjectionHost) {
return epNameExpression.evaluateToString()
}
// constants
return epNameExpression.evaluateString()
}
private fun isExtensionPointNameDeclarationField(uField: UField): Boolean {
if (!uField.isFinal) {
return false
}
val initializer = uField.uastInitializer
if (initializer !is UCallExpression && initializer !is UQualifiedReferenceExpression) {
return false
}
val fieldClass = PsiTypesUtil.getPsiClass(uField.getType())
if (fieldClass == null) {
return false
}
val qualifiedClassName = fieldClass.getQualifiedName()
return ExtensionPointName::class.java.name == qualifiedClassName ||
ProjectExtensionPointName::class.java.name == qualifiedClassName ||
InheritanceUtil.isInheritor(fieldClass, false, KeyedExtensionCollector::class.java.name)
return epsWithoutLibs
}
}
@@ -0,0 +1,2 @@
public interface My<caret>BeanClassInterface {
}
@@ -1,10 +0,0 @@
import com.intellij.openapi.extensions.ExtensionPointName;
import java.lang.String;
public class MyBeanClassStringEP {
public static final ExtensionPointName<String> EP_<caret>NAME =
ExtensionPointName.create("com.intellij.myBeanClassStringEP");
}
@@ -0,0 +1,2 @@
public class My<caret>Class {
}
@@ -0,0 +1,2 @@
public interface My<caret>Interface {
}
@@ -0,0 +1,2 @@
public interface MyMultiple<caret>EPsInterface {
}
@@ -1,10 +0,0 @@
import com.intellij.openapi.extensions.ExtensionPointName;
import java.lang.String;
public class MyStringEP {
public static final ExtensionPointName<String> EP_<caret>NAME =
ExtensionPointName.create("com.intellij.myStringEP");
}
@@ -1,11 +0,0 @@
import com.intellij.openapi.extensions.ExtensionPointName;
import java.lang.String;
public class MyStringEPConstructor {
// private is allowed
private static final ExtensionPointName<String> EP_<caret>NAME =
new ExtensionPointName<String>("com.intellij.myStringEP");
}
@@ -1,9 +0,0 @@
import com.intellij.lang.LanguageExtension;
import java.lang.String;
public class MyStringEP {
public static final LanguageExtension<String> EP_<caret>NAME =
new LanguageExtension("com.intellij.myStringEP", "My Default Implementation");
}
@@ -1,12 +0,0 @@
import com.intellij.openapi.extensions.ExtensionPointName;
import java.lang.String;
public class MyStringEPViaConstant {
public static final String EP_ID = "com.intellij.myStringEP";
public static final ExtensionPointName<String> EP_<caret>NAME =
ExtensionPointName.create(EP_ID);
}
@@ -1,7 +0,0 @@
import com.intellij.lang.LanguageExtension;
public class MyStringEPViaLanguageExtensionSuperCall extends LanguageExtension<String> {
private My<caret>StringEPViaLanguageExtensionSuperCall() {
super("com.intellij.myStringEP")
}
}
@@ -1,9 +0,0 @@
import com.intellij.openapi.extensions.ExtensionPointName;
import com.intellij.util.KeyedLazyInstance;
public class MyStringEP {
public static final ExtensionPointName<KeyedLazyInstance<String>> EP_<caret>NAME =
ExtensionPointName.create("com.intellij.myStringEP");
}
@@ -1,10 +0,0 @@
import com.intellij.openapi.extensions.ProjectExtensionPointName;
import java.lang.String;
public class MyStringProjectEP {
public static final ProjectExtensionPointName<String> EP_<caret>NAME =
new ProjectExtensionPointName<>("com.intellij.myStringEP");
}
@@ -0,0 +1,2 @@
public interface MyWith<caret>ImplementsInterface {
}
@@ -1,11 +1,23 @@
<idea-plugin>
<extensionPoints>
<extensionPoint name="myStringEP" interface="java.lang.String"/>
<!-- Basic interface EP -->
<extensionPoint name="myInterfaceEP" interface="MyInterface"/>
<!-- Basic class EP -->
<extensionPoint name="myClassEP" interface="MyClass"/>
<!-- with.implements EP -->
<extensionPoint name="myWithImplementsEP" beanClass="java.lang.Object">
<with attribute="implementationClass" implements="MyWithImplementsInterface"/>
</extensionPoint>
<extensionPoint name="myBeanClassStringEP" beanClass="java.lang.String"/>
<!-- wrong EP name -->
<!-- beanClass only (negative test - should NOT show gutter) -->
<extensionPoint name="myBeanClassEP" beanClass="MyBeanClassInterface"/>
<!-- Multiple EPs referencing the same interface -->
<extensionPoint name="myMultipleEPs1" interface="MyMultipleEPsInterface"/>
<extensionPoint name="myMultipleEPs2" interface="MyMultipleEPsInterface"/>
<!-- Wrong EP name (for testing no match) -->
<extensionPoint name="myStringEP2" interface="java.lang.String"/>
</extensionPoints>
</idea-plugin>
@@ -1,4 +1,4 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.idea.devkit.navigation;
import com.intellij.testFramework.TestDataPath;
@@ -17,36 +17,4 @@ public class ExtensionPointDeclarationRelatedItemLineMarkerProviderTest
protected String getExtension() {
return ".java";
}
public void testMyStringEP() {
assertStringEP();
}
public void testMyStringEPViaConstant() {
assertStringEP();
}
public void testMyStringEPConstructor() {
assertStringEP();
}
public void testMyStringEPLanguageExtension() {
assertStringEP();
}
public void testMyStringProjectEP() {
assertStringEP();
}
public void testMyStringKeyedLazyInstanceEP() {
assertStringEP();
}
public void testMyStringEPViaLanguageExtensionSuperCall() {
assertStringEP();
}
public void testMyBeanClassStringEP() {
assertSingleEPDeclaration("MyBeanClassStringEP.java", "com.intellij.myBeanClassStringEP");
}
}
}
@@ -0,0 +1 @@
interface My<caret>BeanClassInterface
@@ -0,0 +1 @@
interface My<caret>Class
@@ -0,0 +1 @@
interface My<caret>Interface
@@ -0,0 +1 @@
interface MyMultiple<caret>EPsInterface
@@ -1,8 +0,0 @@
import com.intellij.openapi.extensions.ExtensionPointName
class MyStringEP {
companion object {
private val EP_<caret>NAME = ExtensionPointName.create<String>("com.intellij.myStringEP")
}
}
@@ -1,8 +0,0 @@
import com.intellij.openapi.extensions.ExtensionPointName
class MyStringEPConstructor {
companion object {
val EP_<caret>NAME = ExtensionPointName<String>("com.intellij.myStringEP")
}
}
@@ -1,7 +0,0 @@
import com.intellij.openapi.extensions.ExtensionPointName
interface MyStringEPInterface {
companion object {
private val EP_<caret>NAME = ExtensionPointName.create<String>("com.intellij.myStringEP")
}
}
@@ -1,8 +0,0 @@
import com.intellij.lang.LanguageExtension
class MyStringEPLanguageExtension {
companion object {
val EP_<caret>NAME = LanguageExtension<String>("com.intellij.myStringEP", "My Default Implementation")
}
}
@@ -1,10 +0,0 @@
import com.intellij.openapi.extensions.ExtensionPointName
class MyStringEPViaConstant {
companion object {
val EP_ID = "com.intellij.myStringEP"
val EP_<caret>NAME = ExtensionPointName.create<String>(EP_ID)
}
}
@@ -1,9 +0,0 @@
import com.intellij.openapi.extensions.ExtensionPointName
class MyStringEPViaInterpolation {
companion object {
const val EP_SHORT_NAME = "myStringEP"
private val EP_<caret>NAME = ExtensionPointName.create<String>("com.intellij.$EP_SHORT_NAME")
}
}
@@ -1,3 +0,0 @@
import com.intellij.lang.LanguageExtension
class My<caret>StringEPViaLanguageExtensionSuperCall : LanguageExtension<String>("com.intellij.myStringEP") {}
@@ -1,8 +0,0 @@
import com.intellij.openapi.extensions.ProjectExtensionPointName
class MyStringProjectEP {
companion object {
private val EP_<caret>NAME = ProjectExtensionPointName<String>("com.intellij.myStringEP")
}
}
@@ -0,0 +1 @@
interface MyWith<caret>ImplementsInterface
@@ -1,11 +1,23 @@
<idea-plugin>
<extensionPoints>
<extensionPoint name="myStringEP" interface="java.lang.String"/>
<!-- Basic interface EP -->
<extensionPoint name="myInterfaceEP" interface="MyInterface"/>
<!-- Basic class EP -->
<extensionPoint name="myClassEP" interface="MyClass"/>
<!-- with.implements EP -->
<extensionPoint name="myWithImplementsEP" beanClass="java.lang.Object">
<with attribute="implementationClass" implements="MyWithImplementsInterface"/>
</extensionPoint>
<extensionPoint name="myBeanClassStringEP" beanClass="java.lang.String"/>
<!-- wrong EP name -->
<!-- beanClass only (negative test - should NOT show gutter) -->
<extensionPoint name="myBeanClassEP" beanClass="MyBeanClassInterface"/>
<!-- Multiple EPs referencing the same interface -->
<extensionPoint name="myMultipleEPs1" interface="MyMultipleEPsInterface"/>
<extensionPoint name="myMultipleEPs2" interface="MyMultipleEPsInterface"/>
<!-- Wrong EP name (for testing no match) -->
<extensionPoint name="myStringEP2" interface="java.lang.String"/>
</extensionPoints>
</idea-plugin>
@@ -1,4 +1,4 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.idea.devkit.kotlin.navigation;
import com.intellij.testFramework.TestDataPath;
@@ -6,8 +6,9 @@ import org.jetbrains.idea.devkit.kotlin.DevkitKtTestsUtil;
import org.jetbrains.idea.devkit.navigation.ExtensionPointDeclarationRelatedItemLineMarkerProviderTestBase;
@TestDataPath("$CONTENT_ROOT/testData/navigation/extensionPointDeclaration")
public class KtExtensionPointDeclarationRelatedItemLineMarkerProviderTest extends
ExtensionPointDeclarationRelatedItemLineMarkerProviderTestBase {
public class KtExtensionPointDeclarationRelatedItemLineMarkerProviderTest
extends ExtensionPointDeclarationRelatedItemLineMarkerProviderTestBase {
@Override
protected String getBasePath() {
return DevkitKtTestsUtil.TESTDATA_PATH + "navigation/extensionPointDeclaration";
@@ -17,36 +18,4 @@ public class KtExtensionPointDeclarationRelatedItemLineMarkerProviderTest extend
protected String getExtension() {
return ".kt";
}
public void testMyStringEP() {
assertStringEP();
}
public void testMyStringEPInterface() {
assertStringEP();
}
public void testMyStringEPViaConstant() {
assertStringEP();
}
public void testMyStringEPViaInterpolation() {
assertStringEP();
}
public void testMyStringEPConstructor() {
assertStringEP();
}
public void testMyStringEPLanguageExtension() {
assertStringEP();
}
public void testMyStringProjectEP() {
assertStringEP();
}
public void testMyStringEPViaLanguageExtensionSuperCall() {
assertStringEP();
}
}
@@ -1,4 +1,4 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.idea.devkit.navigation;
import com.intellij.codeInsight.daemon.GutterMark;
@@ -24,8 +24,39 @@ public abstract class ExtensionPointDeclarationRelatedItemLineMarkerProviderTest
moduleBuilder.addLibrary("platform-core", PathUtil.getJarPathForClass(LanguageExtension.class));
}
protected void assertStringEP() {
assertSingleEPDeclaration(getTestName(false) + getExtension(), "com.intellij.myStringEP");
public void testMyInterface() {
assertSingleEPDeclaration("MyInterface" + getExtension(), "com.intellij.myInterfaceEP");
}
public void testMyClass() {
assertSingleEPDeclaration("MyClass" + getExtension(), "com.intellij.myClassEP");
}
public void testMyWithImplementsInterface() {
assertSingleEPDeclaration("MyWithImplementsInterface" + getExtension(), "com.intellij.myWithImplementsEP");
}
public void testMyBeanClassInterface() {
// Negative test - should NOT show gutter for beanClass only
myFixture.copyFileToProject("extensionPointDeclarationEPs.xml");
final GutterMark gutter = myFixture.findGutter("MyBeanClassInterface" + getExtension());
assertNull("Gutter should not appear for beanClass-only EP", gutter);
}
public void testMyMultipleEPsInterface() {
// Should show gutter when multiple EPs reference the same interface
VirtualFile pluginXmlFile = myFixture.copyFileToProject("extensionPointDeclarationEPs.xml");
PsiFile pluginPsiFile = getPsiManager().findFile(pluginXmlFile);
assertNotNull(pluginPsiFile);
final GutterMark gutter = myFixture.findGutter("MyMultipleEPsInterface" + getExtension());
assertNotNull(gutter);
// Should list both EPs in tooltip
DevKitGutterTargetsChecker.checkGutterTargets(gutter,
"<html><body>&nbsp;&nbsp;&nbsp;&nbsp;com.intellij.myMultipleEPs1<br>&nbsp;&nbsp;&nbsp;&nbsp;com.intellij.myMultipleEPs2<br></body></html>",
DevkitCoreIcons.Gutter.Plugin, "extensionPoint", "extensionPoint");
}
protected void assertSingleEPDeclaration(String filePath, String epFqn) {