[kotlin] KTIJ-39542 Handle Maven pluginManagement ordering during Kotlin config

Avoid reordering plugins across `build/plugins` and `pluginManagement` when configuring Kotlin in Maven projects. The previous logic assumed the reordered plugin would always be resolved from `build/plugins`, which caused a NoSuchElementException for root POMs that keep javac settings in `pluginManagement`.


(cherry picked from commit 431b37eece386b8c85a7a04cfd2a5cc139a4c355)

IJ-CR-213466

GitOrigin-RevId: 631849e34a1b0907ac680566bd925378447fae53
This commit is contained in:
Andrey Cherkasov
2026-07-20 19:10:12 +00:00
committed by intellij-monorepo-bot
parent d375e34d0b
commit 8c3d78652c
2 changed files with 206 additions and 2 deletions
@@ -201,11 +201,13 @@ class PomFile private constructor(private val xmlFile: XmlFile, val domModel: Ma
?: domModel.build.pluginManagement.plugins.plugins.firstOrNull { it.matches(groupArtifact) }
fun isPluginAfter(plugin: MavenDomPlugin, referencePlugin: MavenDomPlugin): Boolean {
require(plugin.parent === referencePlugin.parent) { "Plugins should be siblings" }
require(plugin !== referencePlugin)
val referenceElement = referencePlugin.xmlElement!!
var e: PsiElement = plugin.xmlElement!!
// `pluginManagement/plugins` and `build/plugins` can both contain the same Maven plugin.
// In that case there is no shared sibling order to compare inside one XML container.
if (e.parent !== referenceElement.parent) return false
while (e !== referenceElement) {
val prev = e.prevSibling ?: return false
@@ -216,13 +218,21 @@ class PomFile private constructor(private val xmlFile: XmlFile, val domModel: Ma
}
private fun ensurePluginAfter(plugin: MavenDomPlugin, referencePlugin: MavenDomPlugin): MavenDomPlugin {
// Only reorder plugins that already live in the same XML list. Moving a plugin across
// `pluginManagement` and `build/plugins` would change the POM structure instead of order.
if (plugin.xmlElement?.parent !== referencePlugin.xmlElement?.parent) {
return plugin
}
if (!isPluginAfter(plugin, referencePlugin)) {
// rearrange
val referenceElement = referencePlugin.xmlElement!!
val newElement = referenceElement.parent.addAfter(plugin.xmlElement!!, referenceElement)
plugin.xmlTag?.delete()
return domModel.build.plugins.plugins.single { it.xmlElement == newElement }
return (domModel.build.plugins.plugins + domModel.build.pluginManagement.plugins.plugins)
.singleOrNull { it.xmlElement == newElement }
?: error("Failed to find plugin after reordering in ${xmlFile.name}")
}
return plugin
@@ -401,6 +401,200 @@ class KotlinSourceRootDirsMavenTest : AbstractKotlinMavenImporterTest() {
doTest(project, module, afterFile)
}
@Test
fun `test root module with plugin management and processor submodule`() = runBlocking {
val mainPom = createProjectPom(
"""
<groupId>org.example</groupId>
<artifactId>project</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>sub-mod</module>
</modules>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.15.0</version>
</plugin>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>$KOTLIN_VERSION</version>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
</build>
""".trimIndent()
)
val subModulePom = createModulePom(
"sub-mod",
"""
<parent>
<groupId>org.example</groupId>
<artifactId>project</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>sub-mod</artifactId>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.6.3</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
""".trimIndent()
)
importProjectsAsync(mainPom, subModulePom)
assertModules("project", "sub-mod")
val module = project.modules.first { it.name == "project" }
val afterFile = """<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>project</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>sub-mod</module>
</modules>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<kotlin.version>2.2.20</kotlin.version>
</properties>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>${'$'}{kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test</artifactId>
<version>${'$'}{kotlin.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${'$'}{kotlin.version}</version>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
<configuration>
<jvmTarget>${'$'}{maven.compiler.target}</jvmTarget>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.15.0</version>
<executions>
<execution>
<id>default-compile</id>
<phase>none</phase>
</execution>
<execution>
<id>default-testCompile</id>
<phase>none</phase>
</execution>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>testCompile</id>
<phase>test-compile</phase>
<goals>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>"""
val childTextBefore = runReadAction { subModulePom.toPsiFile(project)!!.text }
doTest(project, module, afterFile)
val childTextAfter = runReadAction { subModulePom.toPsiFile(project)!!.text }
assertEquals(
"Submodule POM should stay untouched:\n$childTextAfter",
childTextBefore,
childTextAfter
)
}
private fun doTest(project: Project, module: Module, afterFile: String) {
return runInEdtAndGet {
val moduleRootManager = ModuleRootManager.getInstance(module)