IJPL-217668 convert structuralsearch.xml to product module

GitOrigin-RevId: 2d2759d61b68f84690eab827fa4642dde3c67e28
This commit is contained in:
Vladimir Krivosheev
2025-11-10 19:18:32 +00:00
committed by intellij-monorepo-bot
parent 5f36969dc8
commit 4252fbae15
22 changed files with 166 additions and 342 deletions
@@ -65,10 +65,6 @@ open class IdeaCommunityProperties(private val communityHomeDir: Path) : JetBrai
CommunityRepositoryModules.groovyPlugin(),
)
productLayout.addPlatformSpec { layout, _ ->
layout.withModule("intellij.platform.structuralSearch")
}
productLayout.skipUnresolvedContentModules = true
mavenArtifacts.forIdeModules = true
@@ -25,9 +25,7 @@
<xi:fallback/>
</xi:include>
<xi:include href="/META-INF/structuralsearch.xml">
<xi:fallback/>
</xi:include>
<extensions defaultExtensionNs="com.intellij">
<applicationService serviceInterface="com.intellij.openapi.application.IdeUrlTrackingParametersProvider"
@@ -12,16 +12,24 @@ Each module set is defined as a Kotlin function returning a `ModuleSet` object.
### Generating XML Files
To regenerate XML files from Kotlin code, run:
```bash
# For all products (community + ultimate)
UltimateModuleSets.main()
To regenerate XML files from Kotlin code:
# For community products only
CommunityModuleSets.main()
**Using JetBrains MCP (Recommended):**
```kotlin
mcp__jetbrains__execute_run_configuration(configurationName="Generate Product Layouts")
```
Or use the IDE's "Generate Product Layouts" run configuration.
**Using Gradle:**
```bash
# For all products (community + ultimate)
./gradlew :platform.buildScripts:runIde -PmainClass=com.intellij.platform.commercial.buildScripts.productLayout.UltimateModuleSets
# For community products only
./gradlew :intellij.platform.buildScripts:runIde -PmainClass=org.jetbrains.intellij.build.productLayout.CommunityModuleSets
```
**From IDE:**
Run the "Generate Product Layouts" run configuration.
### Querying Module Sets
@@ -11,7 +11,7 @@ import com.intellij.platform.plugins.parser.impl.elements.ModuleLoadingRule
*
* @param spec The product modules specification
* @param collectModuleSetAliases Whether to collect module set aliases during traversal (for inlineModuleSets mode)
* @return Triple of (content blocks, module-to-set chain mapping, module set alias to source mapping)
* @return Triple of (content blocks, module-to-set chain mapping, module set alias-to-source mapping)
*/
internal fun buildContentBlocksAndChainMapping(
spec: ProductModulesContentSpec,
@@ -22,7 +22,7 @@ internal fun buildContentBlocksAndChainMapping(
val moduleToSets = mutableMapOf<String, MutableList<String>>()
val aliasToSource = if (collectModuleSetAliases) mutableMapOf<String, String>() else null
val processedSets = HashSet<String>()
val contentBlockByName = mutableMapOf<String, ContentBlock>()
val contentBlockByName = HashMap<String, ContentBlock>()
fun traverse(moduleSet: ModuleSet, chain: List<String>, overrides: Map<String, ModuleLoadingRule>) {
val setName = "$MODULE_SET_PREFIX${moduleSet.name}"
@@ -30,7 +30,7 @@ internal fun buildContentBlocksAndChainMapping(
// Check if already processed
val alreadyProcessed = !processedSets.add(setName)
if (alreadyProcessed) {
// If already processed but now we have overrides, update the existing content block
// If already processed, but now we have overrides, update the existing content block
if (overrides.isNotEmpty()) {
val existingBlock = contentBlockByName[moduleSet.name]
if (existingBlock != null) {
@@ -69,13 +69,9 @@ internal fun buildContentBlocksAndChainMapping(
}
val currentChain = chain + setName
// Get direct modules for this set
val directModules = getDirectModules(moduleSet, spec.excludedModules)
// Build content block and track chains/duplicates in single pass
val modulesWithLoading = mutableListOf<ModuleWithLoading>()
for (module in directModules) {
for (module in moduleSet.modules) {
// Track for duplicate detection
moduleToSets.computeIfAbsent(module.name) { mutableListOf() }.add(moduleSet.name)
// Track chain
@@ -104,7 +100,7 @@ internal fun buildContentBlocksAndChainMapping(
// Validate that all overridden modules exist as direct modules in their respective module sets
for (moduleSetWithOverrides in spec.moduleSets) {
validateModuleSetOverrides(moduleSetWithOverrides, spec)
validateModuleSetOverrides(moduleSetWithOverrides)
}
// Check for duplicates and FAIL if found
@@ -113,9 +109,7 @@ internal fun buildContentBlocksAndChainMapping(
// Add additional modules if any
val additionalModulesWithLoading = mutableListOf<ModuleWithLoading>()
for (module in spec.additionalModules) {
if (module.name !in spec.excludedModules) {
additionalModulesWithLoading.add(ModuleWithLoading(module.name, module.loading))
}
additionalModulesWithLoading.add(ModuleWithLoading(module.name, module.loading))
}
if (additionalModulesWithLoading.isNotEmpty()) {
@@ -137,7 +131,7 @@ internal fun collectAndValidateAliases(
spec: ProductModulesContentSpec,
moduleSetAliases: Map<String, String>
): List<String> {
val allAliases = moduleSetAliases.toMutableMap()
val allAliases = HashMap(moduleSetAliases)
// Collect product-level aliases and check for conflicts with module set aliases
for (alias in spec.productModuleAliases) {
@@ -70,15 +70,14 @@ class ModuleSetBuilder {
}
/**
* Include all modules from another ModuleSet.
* Include another ModuleSet.
*/
fun moduleSet(set: ModuleSet) {
modules.addAll(set.modules)
nestedSets.add(set)
}
@PublishedApi
internal fun build(): Pair<List<ContentModule>, List<ModuleSet>> = Pair(modules, nestedSets)
internal fun build(): Pair<List<ContentModule>, List<ModuleSet>> = Pair(java.util.List.copyOf(modules), java.util.List.copyOf(nestedSets))
}
/**
@@ -124,7 +123,7 @@ private fun appendModuleXml(sb: StringBuilder, module: ContentModule) {
*/
private fun appendModuleSetContent(sb: StringBuilder, moduleSet: ModuleSet, indent: String = " ", breadcrumb: String = "") {
// Get direct modules (not from nested sets)
val directModules = getDirectModules(moduleSet)
val directModules = moduleSet.modules
// Recursively append nested sets first
for (nestedSet in moduleSet.nestedSets) {
@@ -167,9 +166,6 @@ private fun appendModuleSetContent(sb: StringBuilder, moduleSet: ModuleSet, inde
* @return ModuleSetBuildResult containing XML and direct module count
*/
internal fun buildModuleSetXml(moduleSet: ModuleSet, label: String): ModuleSetBuildResult {
// Count direct modules (excluding nested) - do this once upfront
val directModuleCount = getDirectModules(moduleSet).size
val xml = buildString {
// Add generated file header
val mainClass = if (label == "community") "CommunityModuleSets" else "UltimateModuleSets"
@@ -203,7 +199,7 @@ internal fun buildModuleSetXml(moduleSet: ModuleSet, label: String): ModuleSetBu
append("</idea-plugin>")
}
return ModuleSetBuildResult(xml, directModuleCount)
return ModuleSetBuildResult(xml, moduleSet.modules.size)
}
/**
@@ -180,11 +180,6 @@ private fun enrichProductsWithMetrics(
allModules.add(module.name)
}
// Remove excluded modules
for (excludedModule in contentSpec.excludedModules) {
allModules.remove(excludedModule)
}
val totalModuleCount = allModules.size
val directModuleCount = contentSpec.additionalModules.size
val moduleSetCount = contentSpec.moduleSets.size
@@ -518,7 +513,7 @@ private fun writeDuplicateAnalysis(
for ((moduleSet, _, _) in allModuleSets) {
val allModules = collectAllModuleNames(moduleSet)
for (moduleName in allModules) {
moduleToSets.getOrPut(moduleName) { mutableListOf() }.add(moduleSet.name)
moduleToSets.computeIfAbsent(moduleName) { mutableListOf() }.add(moduleSet.name)
}
}
@@ -674,10 +669,10 @@ private fun writeModuleDistribution(
// Build module → {inModuleSets: [], inProducts: []} mapping
val moduleMap = mutableMapOf<String, ModuleDistributionInfo>()
// Collect modules from module sets
// Collect modules from module sets (recursively including nested sets)
for ((moduleSet, _, _) in allModuleSets) {
for (module in moduleSet.modules) {
val info = moduleMap.getOrPut(module.name) { ModuleDistributionInfo() }
visitAllModules(moduleSet) { module ->
val info = moduleMap.computeIfAbsent(module.name) { ModuleDistributionInfo() }
info.inModuleSets.add(moduleSet.name)
}
}
@@ -702,7 +697,7 @@ private fun writeModuleDistribution(
// Add to module map
for (moduleName in allModulesInProduct) {
val info = moduleMap.getOrPut(moduleName) { ModuleDistributionInfo() }
val info = moduleMap.computeIfAbsent(moduleName) { ModuleDistributionInfo() }
if (product.name !in info.inProducts) {
info.inProducts.add(product.name)
}
@@ -827,10 +822,10 @@ private fun writeModuleUsageIndex(
// Build comprehensive usage index
val usageIndex = mutableMapOf<String, ModuleUsageInfo>()
// Collect from module sets
// Collect from module sets (recursively including nested sets)
for ((moduleSet, location, sourceFile) in allModuleSets) {
for (module in moduleSet.modules) {
val info = usageIndex.getOrPut(module.name) { ModuleUsageInfo() }
visitAllModules(moduleSet) { module ->
val info = usageIndex.computeIfAbsent(module.name) { ModuleUsageInfo() }
info.moduleSets.add(ModuleSetReference(moduleSet.name, location, sourceFile))
}
}
@@ -855,7 +850,7 @@ private fun writeModuleUsageIndex(
// Add to index
for (moduleName in allModulesInProduct) {
val info = usageIndex.getOrPut(moduleName) { ModuleUsageInfo() }
val info = usageIndex.computeIfAbsent(moduleName) { ModuleUsageInfo() }
info.products.add(ProductReference(product.name, product.sourceFile))
}
}
@@ -1402,7 +1397,9 @@ private fun suggestModuleSetUnification(
p.contentSpec?.moduleSets?.any { it.moduleSet.name == msEntry.moduleSet.name } == true
}
if (usedByProducts.size <= 1 && msEntry.moduleSet.modules.size <= 5) {
// Use total module count (including nested sets) for inline candidate detection
val totalModuleCount = collectAllModuleNames(msEntry.moduleSet).size
if (usedByProducts.size <= 1 && totalModuleCount <= 5) {
suggestions.add(UnificationSuggestion(
priority = "low",
strategy = "inline",
@@ -1412,10 +1409,10 @@ private fun suggestModuleSetUnification(
moduleSet2 = null,
products = null,
sharedModuleSets = null,
reason = "Used by only ${usedByProducts.size} product(s) and contains only ${msEntry.moduleSet.modules.size} modules. Consider inlining into the product directly.",
reason = "Used by only ${usedByProducts.size} product(s) and contains only $totalModuleCount modules. Consider inlining into the product directly.",
impact = mapOf(
"moduleSetsSaved" to 1,
"moduleCount" to msEntry.moduleSet.modules.size,
"moduleCount" to totalModuleCount,
"affectedProducts" to usedByProducts.map { it.name }
)
))
@@ -1449,7 +1446,9 @@ private fun suggestModuleSetUnification(
// Strategy 4: Split large module sets
if (strategy == "split" || strategy == "all") {
for (msEntry in allModuleSets) {
if (msEntry.moduleSet.modules.size > 200) {
// Use total module count (including nested sets) for split suggestions
val totalModuleCount = collectAllModuleNames(msEntry.moduleSet).size
if (totalModuleCount > 200) {
suggestions.add(UnificationSuggestion(
priority = "low",
strategy = "split",
@@ -1459,8 +1458,8 @@ private fun suggestModuleSetUnification(
moduleSet2 = null,
products = null,
sharedModuleSets = null,
reason = "Module set contains ${msEntry.moduleSet.modules.size} modules. Consider splitting into smaller, more focused sets for better maintainability.",
impact = mapOf("moduleCount" to msEntry.moduleSet.modules.size)
reason = "Module set contains $totalModuleCount modules. Consider splitting into smaller, more focused sets for better maintainability.",
impact = mapOf("moduleCount" to totalModuleCount)
))
}
}
@@ -115,11 +115,6 @@ class ProductModulesContentSpec(
*/
@JvmField val additionalModules: List<ContentModule>,
/**
* Module names to exclude from the final set (after resolving all module sets and additions).
* This is useful to remove specific modules from included module sets.
*/
@JvmField val excludedModules: Set<String>,
/**
* Composition graph tracking how this spec was assembled.
@@ -198,7 +193,6 @@ class ProductModulesContentSpecBuilder @PublishedApi internal constructor() {
xmlIncludes.addAll(spec.deprecatedXmlIncludes)
moduleSets.addAll(spec.moduleSets)
additionalModules.addAll(spec.additionalModules)
excludedModules.addAll(spec.excludedModules)
// Also preserve the nested spec's composition graph for deep analysis
compositionGraph.addAll(spec.compositionGraph)
@@ -323,7 +317,6 @@ class ProductModulesContentSpecBuilder @PublishedApi internal constructor() {
deprecatedXmlIncludes = java.util.List.copyOf(xmlIncludes),
moduleSets = java.util.List.copyOf(moduleSets),
additionalModules = java.util.List.copyOf(additionalModules),
excludedModules = java.util.Set.copyOf(excludedModules),
compositionGraph = java.util.List.copyOf(compositionGraph),
metadata = metadata,
)
@@ -30,15 +30,13 @@ internal fun formatValidationError(
* Throws an error if invalid overrides are found.
*
* @param moduleSetWithOverrides The module set with overrides to validate
* @param spec The product modules specification (for excluded modules)
*/
internal fun validateModuleSetOverrides(
moduleSetWithOverrides: ModuleSetWithOverrides,
spec: ProductModulesContentSpec
moduleSetWithOverrides: ModuleSetWithOverrides
) {
if (moduleSetWithOverrides.loadingOverrides.isEmpty()) return
val directModules = getDirectModules(moduleSetWithOverrides.moduleSet, spec.excludedModules).map { it.name }.toSet()
val directModules = moduleSetWithOverrides.moduleSet.modules.mapTo(LinkedHashSet()) { it.name }
val invalidOverrides = moduleSetWithOverrides.loadingOverrides.keys.filter { it !in directModules }
if (invalidOverrides.isNotEmpty()) {
@@ -135,7 +133,7 @@ internal fun validateAndRecordAlias(
/**
* Validates that products don't reference redundant module sets.
* A module set is redundant if it's already nested inside another module set the product uses
* A module set is redundant if it's already nested inside another module set the product uses,
* and the product doesn't apply any overrides to it.
*
* This validation ensures product specifications are correct and maintainable.
@@ -94,39 +94,19 @@ internal fun collectAllModuleNames(moduleSet: ModuleSet, excludedModules: Set<St
}
/**
* Gets direct modules from a module set (excluding modules from nested sets and excluded modules).
* Visits all modules recursively (including nested sets), applying the visitor function to each.
* More efficient than collecting when you only need iteration (avoids intermediate collection).
*
* @param moduleSet The module set to get direct modules from
* @param excludedModules Set of module names to exclude
* @return List of direct modules
* @param moduleSet The module set to visit
* @param visitor Function to apply to each module
*/
internal fun getDirectModules(moduleSet: ModuleSet, excludedModules: Set<String> = emptySet()): List<ContentModule> {
// Fast path: no filtering needed (very common for leaf nodes)
if (moduleSet.nestedSets.isEmpty() && excludedModules.isEmpty()) {
return moduleSet.modules
}
// Fast path: only check exclusions (no nested sets)
if (moduleSet.nestedSets.isEmpty()) {
return moduleSet.modules.filter { it.name !in excludedModules }
}
// Build set of module names from nested sets
val nestedModuleNames = HashSet<String>()
for (nestedSet in moduleSet.nestedSets) {
for (module in nestedSet.modules) {
nestedModuleNames.add(module.name)
}
}
// Full filtering: check both nested modules and exclusions
val directModules = mutableListOf<ContentModule>()
internal fun visitAllModules(moduleSet: ModuleSet, visitor: (ContentModule) -> Unit) {
for (module in moduleSet.modules) {
if (module.name !in nestedModuleNames && module.name !in excludedModules) {
directModules.add(module)
}
visitor(module)
}
for (nestedSet in moduleSet.nestedSets) {
visitAllModules(nestedSet, visitor)
}
return directModules
}
/**
@@ -117,8 +117,7 @@ fun buildProductContentXml(
isUltimateBuild: Boolean,
): ProductContentBuildResult {
// Build content blocks, chain mapping, and collect module set aliases in single traversal
val (contentBlocks, moduleToSetChainMapping, moduleSetAliases) =
buildContentBlocksAndChainMapping(spec, collectModuleSetAliases = inlineModuleSets)
val (contentBlocks, moduleToSetChainMapping, moduleSetAliases) = buildContentBlocksAndChainMapping(spec, collectModuleSetAliases = inlineModuleSets)
val xml = buildString {
appendXmlHeader(generatorCommand, productPropertiesClass)
@@ -12,7 +12,7 @@ interface ApplicationInfoProperties {
val isEAP: Boolean
val versionSuffix: String?
/**
* The first number from 'minor' part of the version. This property is temporary added because some products specify composite number (like '1.3')
* The first number from 'minor' part of the version. This property is temporarily added because some products specify composite number (like '1.3')
* in 'minor version' attribute instead of using 'micro version' (i.e. set minor='1' micro='3').
*/
val minorVersionMainPart: String
@@ -39,5 +39,6 @@ interface ApplicationInfoProperties {
* another product, to get the instance of the product currently being build, use [BuildContext.applicationInfo] instead.
*/
@Internal
fun BuildContext.loadApplicationInfoPropertiesForProduct(productProperties: ProductProperties): ApplicationInfoProperties =
ApplicationInfoPropertiesImpl(project, productProperties, options)
fun loadApplicationInfoPropertiesForProduct(productProperties: ProductProperties, context: BuildContext): ApplicationInfoProperties {
return ApplicationInfoPropertiesImpl(context.project, productProperties, context.options)
}
@@ -35,7 +35,6 @@ object CommunityModuleSets : ModuleSetProvider {
/**
* Essential platform modules required by most IDE products.
* Corresponds to intellij.moduleSets.essential.xml and includes libraries.
*/
fun essential(): ModuleSet = moduleSet("essential") {
// Include libraries first (they are xi:included in essential.xml)
@@ -130,7 +129,6 @@ object CommunityModuleSets : ModuleSetProvider {
/**
* All library module sets combined (meta-set that includes core, ktor, misc, temporaryBundled).
* Corresponds to intellij.moduleSets.libraries.xml
*/
fun libraries(): ModuleSet = moduleSet("libraries") {
moduleSet(librariesCore())
@@ -141,7 +139,6 @@ object CommunityModuleSets : ModuleSetProvider {
/**
* Core library modules.
* Corresponds to intellij.moduleSets.libraries.core.xml
*/
fun librariesCore(): ModuleSet = moduleSet("libraries.core") {
embeddedModule("intellij.libraries.kotlin.reflect")
@@ -229,7 +226,6 @@ object CommunityModuleSets : ModuleSetProvider {
/**
* Ktor library modules.
* Corresponds to intellij.moduleSets.libraries.ktor.xml
*/
fun librariesKtor(): ModuleSet = moduleSet("libraries.ktor") {
embeddedModule("intellij.libraries.ktor.io")
@@ -241,7 +237,6 @@ object CommunityModuleSets : ModuleSetProvider {
/**
* Miscellaneous library modules.
* Corresponds to intellij.moduleSets.libraries.misc.xml
*/
fun librariesMisc(): ModuleSet = moduleSet("libraries.misc") {
// all libs here must not be embedded, if it is embedded, it should be moved to libs-core.xml
@@ -253,7 +248,6 @@ object CommunityModuleSets : ModuleSetProvider {
/**
* Temporarily bundled library modules (planned to be removed).
* Corresponds to intellij.moduleSets.libraries.temporaryBundled.xml
*/
fun librariesTemporaryBundled(): ModuleSet = moduleSet("libraries.temporaryBundled") {
// Currently used only by DBE (see https://youtrack.jetbrains.com/issue/IJPL-211789/CNFE-org.codehaus.jettison.mapped.Configuration).
@@ -268,7 +262,6 @@ object CommunityModuleSets : ModuleSetProvider {
/**
* VCS (Version Control System) modules including shared and frontend parts.
* Corresponds to intellij.moduleSets.vcs.xml
*/
fun vcs(): ModuleSet = moduleSet("vcs") {
module("intellij.platform.vcs.impl")
@@ -290,7 +283,6 @@ object CommunityModuleSets : ModuleSetProvider {
/**
* VCS shared modules (used by both frontend and backend).
* Corresponds to intellij.moduleSets.vcs.shared.xml
*/
fun vcsShared(): ModuleSet = moduleSet("vcs.shared") {
embeddedModule("intellij.platform.vcs.core")
@@ -301,7 +293,6 @@ object CommunityModuleSets : ModuleSetProvider {
/**
* VCS frontend modules.
* Corresponds to intellij.moduleSets.vcs.frontend.xml
*/
fun vcsFrontend(): ModuleSet = moduleSet("vcs.frontend") {
module("intellij.platform.vcs.impl.frontend")
@@ -309,7 +300,6 @@ object CommunityModuleSets : ModuleSetProvider {
/**
* XML support modules.
* Corresponds to intellij.moduleSets.xml.xml
*/
fun xml(): ModuleSet = moduleSet("xml", alias = "com.intellij.modules.xml") {
embeddedModule("intellij.xml.dom")
@@ -334,7 +324,6 @@ object CommunityModuleSets : ModuleSetProvider {
/**
* Duplicates analysis modules.
* Corresponds to intellij.moduleSets.duplicates.xml
*/
fun duplicates(): ModuleSet = moduleSet("duplicates") {
embeddedModule("intellij.platform.duplicates.analysis")
@@ -342,7 +331,6 @@ object CommunityModuleSets : ModuleSetProvider {
/**
* Stream debugger modules.
* Corresponds to intellij.moduleSets.debugger.streams.xml
*/
fun debuggerStreams(): ModuleSet = moduleSet("debugger.streams") {
module("intellij.debugger.streams.core")
@@ -352,7 +340,6 @@ object CommunityModuleSets : ModuleSetProvider {
/**
* Process elevation support (for operations requiring elevated privileges).
* Corresponds to intellij.moduleSets.elevation.xml
*/
fun elevation(): ModuleSet = moduleSet("elevation") {
module("intellij.execution.process.elevation")
@@ -363,7 +350,6 @@ object CommunityModuleSets : ModuleSetProvider {
/**
* Compose UI modules.
* Corresponds to intellij.moduleSets.compose.xml
*/
fun compose(): ModuleSet = moduleSet("compose") {
module("intellij.libraries.skiko")
@@ -386,7 +372,6 @@ object CommunityModuleSets : ModuleSetProvider {
/**
* Grid/data viewer core modules.
* Corresponds to intellij.moduleSets.grid.core.xml
*/
fun gridCore(): ModuleSet = moduleSet("grid.core") {
module("intellij.grid")
@@ -398,7 +383,6 @@ object CommunityModuleSets : ModuleSetProvider {
/**
* Remote development common modules.
* Corresponds to intellij.moduleSets.rd.common.xml
*/
fun rdCommon(): ModuleSet = moduleSet("rd.common") {
module("intellij.rd.ide.model.generated")
@@ -408,7 +392,6 @@ object CommunityModuleSets : ModuleSetProvider {
/**
* IDE common modules (includes essential, compose, grid.core, vcs, xml, duplicates).
* Corresponds to intellij.moduleSets.ide.common.xml
*/
fun ideCommon(): ModuleSet = moduleSet("ide.common") {
// Include essential first (which includes libraries)
@@ -454,10 +437,10 @@ object CommunityModuleSets : ModuleSetProvider {
moduleSet(vcs())
moduleSet(xml())
moduleSet(duplicates())
module("intellij.platform.structuralSearch")
// Note: rd.common is intentionally NOT included in ide.common
// Reason: Rider uses custom module loading mode due to early backend startup requirements.
// Products that need rd.common include it explicitly in their product files.
}
}
@@ -7,7 +7,7 @@ import kotlinx.serialization.json.Json
import java.nio.file.Path
/**
* Parses JSON argument from command line in the format --json or --json='{"filter":"...","value":"..."}'.
* Parses JSON argument from command line in the format `--json` or `--json='{"filter":"...","value":"..."}'`.
* Returns null for full JSON output, or JsonFilter for filtered output.
*
* @param arg The command line argument (e.g., "--json" or "--json={...}")
@@ -36,12 +36,10 @@ fun parseJsonArgument(arg: String): JsonFilter? {
* 1. JSON mode (--json): Outputs comprehensive analysis as JSON to stdout
* 2. Default mode: Generates XML files for module sets and products
*
* This is the generic orchestration logic that was previously duplicated in UltimateModuleSets.kt.
* Now module set providers (community, ultimate) can call this function with their specific data.
*
* @param args Command line arguments
* @param communityModuleSets Module sets from community
* @param ultimateModuleSets Module sets from ultimate (or empty for community-only)
* @param testProducts Test product specifications (name to ProductModulesContentSpec pairs)
* @param communitySourceFile Source file path for community module sets
* @param ultimateSourceFile Source file path for ultimate module sets (or null for community-only)
* @param projectRoot Project root path
@@ -51,45 +49,49 @@ fun runModuleSetMain(
args: Array<String>,
communityModuleSets: List<ModuleSet>,
ultimateModuleSets: List<ModuleSet>,
testProducts: List<Pair<String, ProductModulesContentSpec>> = emptyList(),
communitySourceFile: String,
ultimateSourceFile: String?,
projectRoot: Path,
generateXmlImpl: suspend () -> Unit
) {
// Parse --json arg with optional filter
generateXmlImpl: suspend () -> Unit,
): Unit = runBlocking(Dispatchers.Default) {
// Parse `--json` arg with optional filter
val jsonArg = args.firstOrNull { it.startsWith("--json") }
when {
jsonArg != null -> {
runBlocking(Dispatchers.Default) {
// Prepare all module sets with metadata
val communityModuleSetsWithMeta = communityModuleSets.map {
ModuleSetMetadata(it, "community", communitySourceFile)
}
val ultimateModuleSetsWithMeta = if (ultimateSourceFile != null) {
ultimateModuleSets.map {
ModuleSetMetadata(it, "ultimate", ultimateSourceFile)
}
} else {
emptyList()
}
val allModuleSets = communityModuleSetsWithMeta + ultimateModuleSetsWithMeta
// Discover all products (reuse existing logic from productXmlFileGenerator)
val products = discoverAllProductsForJson(projectRoot)
// Parse filter from --json='{"filter":"..."}' format
val filter = parseJsonArgument(jsonArg)
// Stream JSON to stdout
streamModuleAnalysisJson(allModuleSets, products, projectRoot, filter)
// Prepare all module sets with metadata
val communityModuleSetsWithMeta = communityModuleSets.map {
ModuleSetMetadata(it, "community", communitySourceFile)
}
val ultimateModuleSetsWithMeta = if (ultimateSourceFile == null) {
emptyList()
}
else {
ultimateModuleSets.map {
ModuleSetMetadata(it, "ultimate", ultimateSourceFile)
}
}
val allModuleSets = communityModuleSetsWithMeta + ultimateModuleSetsWithMeta
// Discover regular products and add passed test products
val regularProducts = discoverAllProductsForJson(projectRoot)
val testProductSpecs = testProducts.asSequence().map { (name, spec) ->
ProductSpec(
name = name,
className = "test-product",
sourceFile = "test-product",
pluginXmlPath = "ultimate/platform-ultimate/testResources/META-INF/${name}Plugin.xml",
contentSpec = spec,
buildModules = emptyList()
)
}
streamModuleAnalysisJson(allModuleSets = allModuleSets, products = (regularProducts + testProductSpecs).toList(), projectRoot = projectRoot, filter = parseJsonArgument(jsonArg))
}
else -> {
// Default mode: Generate XML files
runBlocking(Dispatchers.Default) {
generateXmlImpl()
}
generateXmlImpl()
}
}
}
@@ -1,106 +0,0 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.intellij.build.productLayout
/**
* Product spec helper functions for reusable product content fragments.
* These functions return ProductModulesContentSpec instances that can be composed using include().
*
* Use these helpers to reduce duplication across product specifications while keeping
* ProductModulesContentSpec immutable and declarative (suitable for future YAML representation).
*/
/**
* Common capability aliases shared across most ultimate products.
* Bundles frequently repeated capability declarations to reduce duplication.
*
* Includes:
* - Run targets support
* - Microservices capabilities
* - ML inline completion
* - IDE provisioner
* - Marketplace integration
*
* Usage:
* ```
* override fun getProductContentDescriptor(): ProductModulesContentSpec = productModules {
* include(commonCapabilityAliases())
* // ...
* }
* ```
*/
fun commonCapabilityAliases(): ProductModulesContentSpec = productModules {
alias("com.intellij.modules.run.targets")
alias("com.intellij.modules.microservices")
alias("com.intellij.ml.inline.completion")
alias("com.intellij.platform.ide.provisioner")
alias("com.intellij.marketplace")
}
/**
* Python capability aliases for non-PyCharm products.
* Enables Python support in multi-language IDEs like GoLand, DataGrip, CLion, RustRover, etc.
*
* Includes:
* - Python core capabilities
* - Python in mini-IDE capabilities
* - Python in non-PyCharm IDE capabilities
*
* Usage:
* ```
* override fun getProductContentDescriptor(): ProductModulesContentSpec = productModules {
* include(pythonMiniIdeCapabilities())
* // ...
* }
* ```
*/
fun pythonMiniIdeCapabilities(): ProductModulesContentSpec = productModules {
alias("com.intellij.modules.python-core-capable")
alias("com.intellij.modules.python-in-mini-ide-capable")
alias("com.intellij.modules.python-in-non-pycharm-ide-capable")
}
/**
* Common platform includes repeated across most ultimate products.
* Bundles deprecatedInclude statements for legacy XML resource inclusion.
*
* Includes:
* - Platform lang plugin resources
* - Structural search resources
* - Remote servers implementation
* - Ultimate edition resources
*
* Usage:
* ```
* override fun getProductContentDescriptor(): ProductModulesContentSpec = productModules {
* include(platformCommonIncludes())
* // ...
* }
* ```
*/
fun platformCommonIncludes(): ProductModulesContentSpec = productModules {
deprecatedInclude("intellij.platform.resources", "META-INF/PlatformLangPlugin.xml")
deprecatedInclude("intellij.platform.structuralSearch", "META-INF/structuralsearch.xml")
deprecatedInclude("intellij.platform.remoteServers.impl", "intellij.platform.remoteServers.impl.xml")
deprecatedInclude("intellij.platform.commercial", "META-INF/ultimate.xml")
}
/**
* Extensions for native development IDEs (CLion, GoLand, RustRover).
* Combines process elevation support with native debugger capability.
*
* Includes:
* - Process elevation module set (for operations requiring elevated privileges)
* - Native debugger plugin capability alias
*
* Usage:
* ```
* override fun getProductContentDescriptor(): ProductModulesContentSpec = productModules {
* include(nativeDevExtensions())
* // ...
* }
* ```
*/
fun nativeDevExtensions(): ProductModulesContentSpec = productModules {
moduleSet(CommunityModuleSets.elevation())
alias("com.intellij.modules.nativeDebug-plugin-capable")
}
@@ -17,11 +17,11 @@ import java.nio.file.Path
* Test products have properties = null (they don't have ProductProperties classes).
*/
internal data class DiscoveredProduct(
val name: String,
val config: ProductConfiguration,
val properties: ProductProperties?,
val spec: ProductModulesContentSpec?,
val pluginXmlPath: String?
@JvmField val name: String,
@JvmField val config: ProductConfiguration,
@JvmField val properties: ProductProperties?,
@JvmField val spec: ProductModulesContentSpec?,
@JvmField val pluginXmlPath: String?,
)
/**
@@ -31,11 +31,12 @@ internal data class DiscoveredProduct(
*/
internal fun DiscoveredProduct.toProductSpec(projectRoot: Path): ProductSpec {
// For test products (properties = null), use "test-product" as source file
val sourceFile = if (properties != null) {
getProductPropertiesSourceFile(properties.javaClass, projectRoot)
} else {
val sourceFile = if (properties == null) {
"test-product"
}
else {
getProductPropertiesSourceFile(properties.javaClass, projectRoot)
}
return ProductSpec(
name = name,
@@ -74,7 +75,7 @@ private suspend fun discoverAllProductsInternal(projectRoot: Path): List<Discove
platformPrefix = null
)
val spec = productProperties.getProductContentDescriptor()
products.add(
DiscoveredProduct(
name = productName,
@@ -85,74 +86,22 @@ private suspend fun discoverAllProductsInternal(projectRoot: Path): List<Discove
)
)
}
return products
}
/**
* Discovers test products from known locations.
* These are products used for testing that shouldn't be in dev-build.json.
* Retrieves contentSpec from module set registries (UltimateModuleSets).
*
* @param projectRoot The project root path
* @return List of test products as DiscoveredProduct with contentSpec
*/
private fun discoverTestProducts(projectRoot: Path): List<DiscoveredProduct> {
// Build a registry of test product name -> (xmlPath, contentSpec)
val testProductRegistry = mutableMapOf<String, Pair<String, ProductModulesContentSpec>>()
// Get ultimate test product specs (only if ultimate directory exists)
if (Files.exists(projectRoot.resolve("ultimate"))) {
try {
// Load UltimateModuleSets via reflection to avoid hard dependency
val ultimateModuleSetsClass = Class.forName("com.intellij.platform.commercial.buildScripts.productLayout.UltimateModuleSets")
val instanceField = ultimateModuleSetsClass.getDeclaredField("INSTANCE")
val ultimateModuleSetsInstance = instanceField.get(null)
val getTestProductSpecsMethod = ultimateModuleSetsClass.getDeclaredMethod("getTestProductSpecs")
@Suppress("UNCHECKED_CAST")
val ultimateSpecs = getTestProductSpecsMethod.invoke(ultimateModuleSetsInstance) as List<Pair<String, ProductModulesContentSpec>>
for ((name, spec) in ultimateSpecs) {
val xmlPath = "ultimate/platform-ultimate/testResources/META-INF/${name}Plugin.xml"
testProductRegistry[name] = xmlPath to spec
}
} catch (e: ClassNotFoundException) {
// Ultimate module not available, skip ultimate test products
}
}
// Build DiscoveredProduct instances for test products that exist on disk
return testProductRegistry.mapNotNull { (name, pair) ->
val (xmlPath, spec) = pair
val xmlFile = projectRoot.resolve(xmlPath)
if (!Files.exists(xmlFile)) return@mapNotNull null
DiscoveredProduct(
name = name,
config = ProductConfiguration(
className = "test-product", // test products don't have Properties class
modules = emptyList(),
pluginXmlPath = xmlPath
),
properties = null, // test products don't have ProductProperties
spec = spec,
pluginXmlPath = xmlPath
)
}
}
/**
* Discovers all products from dev-build.json registry and converts to ProductSpec for JSON output.
* Public function that can be called from other modules (like ultimate buildScripts).
* Test products should be passed separately via runModuleSetMain's testProducts parameter.
*
* @param projectRoot The project root path
* @return List of products as ProductSpec (simple data class with no internal dependencies)
*/
suspend fun discoverAllProductsForJson(projectRoot: Path): List<ProductSpec> {
suspend fun discoverAllProductsForJson(projectRoot: Path): Sequence<ProductSpec> {
val regularProducts = discoverAllProductsInternal(projectRoot)
val testProducts = discoverTestProducts(projectRoot)
val allProducts = regularProducts + testProducts
return allProducts.map { it.toProductSpec(projectRoot) }
return regularProducts.asSequence().map { it.toProductSpec(projectRoot) }
}
/**
@@ -169,14 +118,37 @@ suspend fun discoverAllProductsForValidation(projectRoot: Path): List<Pair<Strin
/**
* Generates product XMLs for all products using programmatic content.
* Discovers products from dev-build.json and test products, then generates complete plugin.xml files.
* Discovers products from dev-build.json and accepts test products as parameter, then generates complete plugin.xml files.
*
* @param projectRoot The project root path
* @param testProductSpecs Test product specifications (name to ProductModulesContentSpec pairs)
* @return Result containing generation statistics
*/
suspend fun generateAllProductXmlFiles(projectRoot: Path): ProductGenerationResult {
suspend fun generateAllProductXmlFiles(
projectRoot: Path,
testProductSpecs: List<Pair<String, ProductModulesContentSpec>> = emptyList(),
): ProductGenerationResult {
val regularProducts = discoverAllProductsInternal(projectRoot)
val testProducts = discoverTestProducts(projectRoot)
// Convert test product specs to DiscoveredProduct instances
val testProducts = testProductSpecs.mapNotNull { (name, spec) ->
val xmlPath = "ultimate/platform-ultimate/testResources/META-INF/${name}Plugin.xml"
val xmlFile = projectRoot.resolve(xmlPath)
if (!Files.exists(xmlFile)) return@mapNotNull null
DiscoveredProduct(
name = name,
config = ProductConfiguration(
className = "test-product",
modules = emptyList(),
pluginXmlPath = xmlPath
),
properties = null,
spec = spec,
pluginXmlPath = xmlPath
)
}
val products = regularProducts + testProducts
// Detect if this is an Ultimate build (projectRoot != communityRoot)
@@ -193,7 +165,7 @@ suspend fun generateAllProductXmlFiles(projectRoot: Path): ProductGenerationResu
.loadProject(projectRoot.toString(), mapOf("MAVEN_REPOSITORY" to JpsMavenSettings.getMavenRepositoryPath()), false)
.modules
)
generateProductXml(
pluginXmlPath = pluginXmlPath,
spec = spec,
@@ -15,8 +15,10 @@ private val RIDER_MODULE_ID = PluginModuleId("intellij.rider", PluginModuleId.JE
private val JSON_ALIAS_ID = PluginId.getId("com.intellij.modules.json")
private val JSON_BACKEND_MODULE_ID = PluginModuleId("intellij.json.backend", PluginModuleId.JETBRAINS_NAMESPACE)
internal class ModulesWithDependencies(val modules: List<PluginModuleDescriptor>,
val directDependencies: Map<PluginModuleDescriptor, List<PluginModuleDescriptor>>) {
internal class ModulesWithDependencies(
@JvmField val modules: List<PluginModuleDescriptor>,
@JvmField val directDependencies: Map<PluginModuleDescriptor, List<PluginModuleDescriptor>>,
) {
internal fun sorted(topologicalComparator: Comparator<PluginModuleDescriptor>): ModulesWithDependencies {
return ModulesWithDependencies(
modules = modules.sortedWith(topologicalComparator),
@@ -59,7 +61,10 @@ internal fun createModulesWithDependenciesAndAdditionalEdges(plugins: Collection
// and is loaded only in IntelliJ IDEA, so it may use classes from Java plugin.
val implicitDep = if (hasAllModules && PluginCompatibilityUtils.isLegacyPluginWithoutPlatformAliasDependencies(module)) {
pluginIdToDescriptor.get(PluginManagerCore.JAVA_PLUGIN_ALIAS_ID)
} else null
}
else {
null
}
if (implicitDep != null) {
if (module === implicitDep) {
PluginManagerCore.logger.error("Plugin $module depends on self")
@@ -108,7 +113,7 @@ internal fun createModulesWithDependenciesAndAdditionalEdges(plugins: Collection
dependenciesCollector.add(main)
}
/* if the plugin containing the module is incompatible with some other plugins, make sure that the module is processed after that plugins (and all their required modules)
/* if the plugin containing the module is incompatible with some other plugins, make sure that the module is processed after these plugins (and all their required modules)
to ensure that the proper module is disabled in case of package conflict */
for (incompatibility in main.incompatiblePlugins) {
val incompatibleDescriptor = pluginIdToDescriptor.get(incompatibility)
@@ -137,7 +142,7 @@ internal fun createModulesWithDependenciesAndAdditionalEdges(plugins: Collection
) to additionalEdges
}
// alias in most cases points to Core plugin, so, we cannot use computed dependencies to check
// alias in most cases points to Core plugin, so we cannot use computed dependencies to check
private fun doesDependOnPluginAlias(plugin: IdeaPluginDescriptorImpl, @Suppress("SameParameterValue") aliasId: PluginId): Boolean {
return plugin.dependencies.any { it.pluginId == aliasId } || plugin.moduleDependencies.plugins.any { it == aliasId }
}
@@ -164,7 +169,7 @@ internal fun toCoreAwareComparator(comparator: Comparator<PluginModuleDescriptor
* `<depends>com.intellij.modules.platform</depends>` or `<depends>com.intellij.modules.lang</depends>` tags.
* See [this article](https://youtrack.jetbrains.com/articles/IJPL-A-956#keep-compatibility-with-external-plugins) for more details.
*/
private val contentModulesExtractedInCorePluginWhichCanBeUsedFromExternalPlugins = listOf(
private val contentModulesExtractedInCorePluginWhichCanBeUsedFromExternalPlugins = arrayOf(
"intellij.platform.collaborationTools.auth",
"intellij.platform.collaborationTools.auth.base",
"intellij.platform.tasks",
@@ -175,13 +180,14 @@ private val contentModulesExtractedInCorePluginWhichCanBeUsedFromExternalPlugins
"intellij.spellchecker.xml",
"intellij.relaxng",
"intellij.spellchecker",
"intellij.platform.structuralSearch",
).map { PluginModuleId(it, PluginModuleId.JETBRAINS_NAMESPACE) }
/**
* List of content modules from the core plugin which should be automatically added as dependencies third-party plugins and plugins with dependency on `com.intellij.modules.vcs`
* plugin alias for compatibility.
*/
private val vcsApiContentModules = listOf(
private val vcsApiContentModules = arrayOf(
"intellij.platform.vcs.impl",
"intellij.platform.vcs.dvcs",
"intellij.platform.vcs.dvcs.impl",
@@ -196,7 +202,7 @@ private val COLLABORATION_TOOLS_MODULE_ID = PluginModuleId("intellij.platform.co
* List of content modules from the core plugin which should be automatically added as dependencies to all plugins with dependency on `org.jetbrains.completion.full.line` plugin
* alias for compatibility.
*/
private val fullLineApiContentModules = listOf(
private val fullLineApiContentModules = arrayOf(
"intellij.fullLine.core",
"intellij.fullLine.local",
"intellij.fullLine.core.impl",
@@ -222,8 +228,10 @@ private fun collectDirectDependenciesInOldFormat(
}
}
else {
// e.g. `.env` plugin in an old format and doesn't explicitly specify dependency on a new extracted modules
dependenciesCollector.addAll(dep.contentModules)
// e.g. `.env` plugin in an old format and doesn't explicitly specify dependency on new extracted modules
if (dep is PluginMainDescriptor) {
dependenciesCollector.addAll(dep.contentModules)
}
dependenciesCollector.add(dep)
}
@@ -290,7 +298,7 @@ private fun collectDirectDependenciesInNewFormat(
}
}
if (module.pluginId != PluginManagerCore.CORE_ID) {
if (module.pluginId != PluginManagerCore.CORE_ID && module is PluginMainDescriptor) {
/* Add edges to all required content modules.
This is needed to ensure that the main plugin module is processed after them, and at that point we can determine whether the plugin
can be loaded or not. */
@@ -280,6 +280,7 @@
<module name="intellij.platform.langInjection.backend"/>
<module name="intellij.libraries.grpc"/>
<module name="intellij.libraries.grpc.netty.shaded"/>
<module name="intellij.platform.structuralSearch"/>
<!-- </editor-fold> -->
</content>
</idea-plugin>
@@ -0,0 +1,3 @@
- name: dist.all/lib/intellij.platform.structuralSearch.jar
modules:
- name: intellij.platform.structuralSearch
@@ -1,4 +1,4 @@
<idea-plugin>
<idea-plugin visibility="public">
<module value="com.intellij.modules.structuralsearch"/>
<extensionPoints>
@@ -1,6 +1,6 @@
<idea-plugin package="org.jetbrains.kotlin.idea.k2.codeinsight.structuralsearch">
<dependencies>
<plugin id="com.intellij.modules.structuralsearch"/>
<module name="intellij.platform.structuralSearch"/>
<module name="kotlin.code-insight.live-templates.shared"/>
</dependencies>
<extensions defaultExtensionNs="com.intellij">
@@ -8,6 +8,7 @@
<module name="intellij.properties.psi"/>
<module name="intellij.properties.backend.psi"/>
<module name="intellij.spellchecker"/>
<module name="intellij.platform.structuralSearch"/>
</dependencies>
<extensionPoints>
+1 -3
View File
@@ -5,9 +5,7 @@
<module value="com.intellij.modules.pycharm"/>
<xi:include href="/META-INF/PlatformLangPlugin.xml"/>
<xi:include href="/META-INF/structuralsearch.xml">
<xi:fallback/>
</xi:include>
<xi:include href="intellij.platform.remoteServers.impl.xml"/>
<extensions defaultExtensionNs="com.intellij">