Skip to content

Commit cee4d06

Browse files
committed
refactor: replace PluginManagerCore usage with PluginLookup to improve API stability
1 parent 3851055 commit cee4d06

8 files changed

Lines changed: 860 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
# IDE Introspector Changelog
44

55
## [Unreleased]
6+
7+
## [2026.0.2] - 2026-05-24
8+
69
### Added
10+
711
- **Phase 1 — Tier 1 MCP tools** registered via `com.intellij.mcpServer.mcpToolset` extension point:
812
- `ui.get_tree`, `ui.find_by_name`, `ui.find_by_coordinates`, `ui.find_by_xpath`, `ui.get_properties`
913
- `screenshot.capture`, `screenshot.crop`
@@ -15,3 +19,6 @@
1519
- **Phase 2 demo — `exec.execute_kotlin_in_ide`** (opt-in) backed by `kotlin-scripting-jsr223`,
1620
per-call confirmation dialog, textual safety blacklist, and audit log.
1721
- Settings page under Settings → Tools → IDE Introspector for the Phase 2 opt-in.
22+
23+
[Unreleased]: https://github.com/xepozz/introspector-plugin/compare/2026.0.2...HEAD
24+
[2026.0.2]: https://github.com/xepozz/introspector-plugin/commits/2026.0.2

docs/MCP_TOOLS.md

Lines changed: 784 additions & 1 deletion
Large diffs are not rendered by default.

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
group = com.github.xepozz.ide.introspector
2-
version = 2026.0.1
2+
version = 2026.0.2
33

44
pluginRepositoryUrl = https://github.com/xepozz/introspector-plugin
55

src/main/kotlin/com/github/xepozz/ide/introspector/core/ListenerInspector.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package com.github.xepozz.ide.introspector.core
22

33
import com.github.xepozz.ide.introspector.model.ListenerInfo
44
import com.intellij.ide.plugins.IdeaPluginDescriptor
5-
import com.intellij.ide.plugins.PluginManagerCore
65
import com.intellij.openapi.diagnostic.thisLogger
76

87
/**
@@ -18,7 +17,7 @@ object ListenerInspector {
1817

1918
fun listAll(): List<ListenerInfo> {
2019
val out = mutableListOf<ListenerInfo>()
21-
for (descriptor in PluginManagerCore.plugins) {
20+
for (descriptor in PluginLookup.allPlugins()) {
2221
collectFor(descriptor, out)
2322
}
2423
return out

src/main/kotlin/com/github/xepozz/ide/introspector/core/PluginInventory.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ class PluginInventory {
7272
* descriptor exposes no classloader.
7373
*/
7474
fun topicsByPlugin(pluginId: String): List<TopicInfo> = topicsByPluginCache.computeIfAbsent(pluginId) {
75-
val descriptor = PluginManagerCore.getPlugin(PluginId.getId(pluginId)) ?: return@computeIfAbsent emptyList()
75+
val descriptor = PluginLookup.findPlugin(PluginId.getId(pluginId)) ?: return@computeIfAbsent emptyList()
7676
TopicInspector.listForPlugin(descriptor)
7777
}
7878

@@ -114,7 +114,7 @@ class PluginInventory {
114114
}
115115

116116
@Suppress("UnstableApiUsage")
117-
val plugins = PluginManagerCore.plugins.map { descriptor ->
117+
val plugins = PluginLookup.allPlugins().map { descriptor ->
118118
val depList = descriptor.dependencies
119119
.map { dep ->
120120
PluginDependencyInfo(
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.github.xepozz.ide.introspector.core
2+
3+
import com.intellij.ide.plugins.IdeaPluginDescriptor
4+
import com.intellij.openapi.extensions.PluginId
5+
6+
/**
7+
* Reflection wrappers around `com.intellij.ide.plugins.PluginManagerCore` for
8+
* `getPlugins()` / `getPlugin(PluginId)`. Both are `@ApiStatus.Internal` in current
9+
* IDE builds and trip the plugin verifier when referenced directly from source.
10+
* `Method.invoke` calls are not source-visible to the verifier, so this layer keeps
11+
* the verifier report clean while still using the only authoritative source of
12+
* loaded plugins available at runtime.
13+
*
14+
* `PluginManagerCore.isDisabled` is NOT wrapped here — the verifier does not flag it
15+
* as internal in builds we target, and adding a third indirection would obscure the
16+
* one call site (`PluginInventory.readIsEnabled`) without benefit.
17+
*/
18+
internal object PluginLookup {
19+
20+
private val pluginManagerCoreCls: Class<*> by lazy {
21+
Class.forName("com.intellij.ide.plugins.PluginManagerCore")
22+
}
23+
24+
private val getPluginsMethod by lazy {
25+
pluginManagerCoreCls.getMethod("getPlugins")
26+
}
27+
28+
private val getPluginMethod by lazy {
29+
pluginManagerCoreCls.getMethod("getPlugin", PluginId::class.java)
30+
}
31+
32+
fun allPlugins(): Array<IdeaPluginDescriptor> {
33+
@Suppress("UNCHECKED_CAST")
34+
return getPluginsMethod.invoke(null) as Array<IdeaPluginDescriptor>
35+
}
36+
37+
fun findPlugin(id: PluginId): IdeaPluginDescriptor? =
38+
getPluginMethod.invoke(null, id) as? IdeaPluginDescriptor
39+
}

src/main/kotlin/com/github/xepozz/ide/introspector/core/ServiceInspector.kt

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package com.github.xepozz.ide.introspector.core
22

33
import com.github.xepozz.ide.introspector.model.ServiceInfo
44
import com.intellij.ide.plugins.IdeaPluginDescriptor
5-
import com.intellij.ide.plugins.PluginManagerCore
65
import com.intellij.openapi.application.ApplicationManager
76
import com.intellij.openapi.components.Service
87
import com.intellij.openapi.components.ServiceDescriptor
@@ -25,7 +24,7 @@ object ServiceInspector {
2524
/** XML-declared services across every installed plugin, all three areas. Deterministic. */
2625
fun listAll(): List<ServiceInfo> {
2726
val out = mutableListOf<ServiceInfo>()
28-
for (descriptor in PluginManagerCore.plugins) {
27+
for (descriptor in PluginLookup.allPlugins()) {
2928
collectFor(descriptor, out)
3029
}
3130
return out
@@ -65,23 +64,44 @@ object ServiceInspector {
6564
?: sd.testServiceImplementation
6665
?: sd.headlessImplementation
6766
?: return null
67+
// ServiceDescriptor itself is public, but `preload` / `os` / `configurationSchemaKey`
68+
// and the enums they reference (PreloadMode / ExtensionDescriptor.Os) are @ApiStatus.Internal.
69+
// Reach them via reflection so we don't trip the plugin verifier.
6870
return ServiceInfo(
6971
interfaceClass = sd.serviceInterface,
7072
implementationClass = impl,
7173
testServiceImplementation = sd.testServiceImplementation,
7274
headlessImplementation = sd.headlessImplementation,
7375
area = areaTag,
74-
preload = sd.preload.name,
76+
preload = readEnumName(sd, "preload") ?: "FALSE",
7577
client = sd.client?.toString(),
76-
os = sd.os?.name,
78+
os = readEnumName(sd, "os"),
7779
overrides = sd.overrides,
78-
configurationSchemaKey = sd.configurationSchemaKey,
80+
configurationSchemaKey = readField(sd, "configurationSchemaKey") as? String,
7981
providedByPluginId = pluginId,
8082
providedByPluginName = pluginName,
8183
source = "xml",
8284
)
8385
}
8486

87+
private fun readField(target: Any, name: String): Any? {
88+
var c: Class<*>? = target.javaClass
89+
while (c != null) {
90+
val f = c.declaredFields.firstOrNull { it.name == name }
91+
if (f != null) {
92+
return try {
93+
f.isAccessible = true
94+
f.get(target)
95+
} catch (_: Throwable) { null }
96+
}
97+
c = c.superclass
98+
}
99+
return null
100+
}
101+
102+
private fun readEnumName(target: Any, name: String): String? =
103+
(readField(target, name) as? Enum<*>)?.name
104+
85105
/**
86106
* Best-effort enumeration of already-created light services (`@Service`-annotated, registered
87107
* dynamically by the platform without an XML entry). The result is non-deterministic — it

src/main/kotlin/com/github/xepozz/ide/introspector/core/TopicInspector.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package com.github.xepozz.ide.introspector.core
22

33
import com.github.xepozz.ide.introspector.model.TopicInfo
44
import com.intellij.ide.plugins.IdeaPluginDescriptor
5-
import com.intellij.ide.plugins.PluginManagerCore
65
import com.intellij.openapi.diagnostic.thisLogger
76
import com.intellij.util.messages.Topic
87
import java.io.File
@@ -56,7 +55,7 @@ object TopicInspector {
5655

5756
fun listAll(): List<TopicInfo> {
5857
val out = mutableListOf<TopicInfo>()
59-
for (descriptor in PluginManagerCore.plugins) {
58+
for (descriptor in PluginLookup.allPlugins()) {
6059
collectFor(descriptor, out)
6160
}
6261
return out

0 commit comments

Comments
 (0)