[Kotlin, Gradle] Add support for Kotlin generated source in Kotlin/JVM projects

As ContentRootData is being loaded by main IDEA import pipeline, our KotlinGradleProjectResolverExtension only finds 'SOURCE_GENERATED' or 'TEST_GENERATED' that should mark as generated and changes their type.

^KTIJ-35910

GitOrigin-RevId: 1ff808738b0d575e72ca0b25ff7b97151ab7fb82
This commit is contained in:
Yahor Berdnikau
2026-02-17 00:24:03 +00:00
committed by intellij-monorepo-bot
parent d14ab3efc9
commit d7f2bc3603
13 changed files with 260 additions and 25 deletions
@@ -766,14 +766,14 @@ class GradleFacetImportTest8 : KotlinGradleImportingTestCase() {
assertAllModulesConfigured()
}
// kotlin-2js plugin
@Test
fun testStableModuleNameWhileUsingGradleJS() {
configureByFiles()
importProject()
checkStableModuleName("project.main", "project", JsPlatforms.defaultJsPlatform, isProduction = true)
// Note "_test" suffix: this is current behavior of K2JS Compiler
checkStableModuleName("project.test", "project_test", JsPlatforms.defaultJsPlatform, isProduction = false)
checkStableModuleName("project.main", "project.main", JsPlatforms.defaultJsPlatform, isProduction = true)
checkStableModuleName("project.test", "project.test", JsPlatforms.defaultJsPlatform, isProduction = false)
assertAllModulesConfigured()
}
@@ -35,6 +35,7 @@ import org.jetbrains.kotlin.idea.gradleTooling.KotlinGradleModel
import org.jetbrains.kotlin.idea.gradleTooling.KotlinGradleModelBuilder
import org.jetbrains.kotlin.idea.gradleTooling.KotlinTaskPropertiesBySourceSet
import org.jetbrains.kotlin.idea.projectModel.KotlinTarget
import org.jetbrains.kotlin.idea.projectModel.KotlinTaskProperties
import org.jetbrains.kotlin.idea.statistics.KotlinIDEGradleActionsFUSCollector
import org.jetbrains.kotlin.idea.util.CopyableDataNodeUserDataProperty
import org.jetbrains.kotlin.idea.util.NotNullableCopyableDataNodeUserDataProperty
@@ -358,28 +359,71 @@ class KotlinGradleProjectResolverExtension : AbstractProjectResolverExtension()
gradleModel.kotlinTaskProperties.filter { (k, _) -> gradleSourceSetNode.data.id == "$moduleNamePrefix:$k" }
.toList().singleOrNull()
gradleSourceSetNode.children.forEach { dataNode ->
val data = dataNode.data as? ContentRootData
if (data != null) {
/**
* Code snippet for setting in content root properties
* if (propertiesForSourceSet?.second?.pureKotlinSourceFolders?.contains(File(data.rootPath)) == true) {
* @Suppress("UNCHECKED_CAST")
* (dataNode as DataNode<ContentRootData>).isPureKotlinSourceFolder = true
* }
*/
val packagePrefix = propertiesForSourceSet?.second?.packagePrefix
if (packagePrefix != null) {
ExternalSystemSourceType.values().filter { !(it.isResource || it.isGenerated) }.forEach { type ->
val paths = data.getPaths(type)
val newPaths = paths.map { ContentRootData.SourceRoot(it.path, packagePrefix) }
paths.clear()
paths.addAll(newPaths)
}
}
dataNode.markGeneratedSourceRoots(gradleModel.generatedSourcesRoots)
dataNode.updateSourceRootPackagePrefix(propertiesForSourceSet)
}
}
}
}
}
private fun DataNode<*>.markGeneratedSourceRoots(
generatedSources: List<String>
) {
visitData { contentRootData ->
if (contentRootData == null ||
generatedSources.isEmpty() ||
contentRootData as? ContentRootData == null
) return@visitData contentRootData
// it is already marked as generated or contains other types in which we are not interested in
if (contentRootData.getPaths(ExternalSystemSourceType.SOURCE).isEmpty() &&
contentRootData.getPaths(ExternalSystemSourceType.TEST).isEmpty()
) return@visitData contentRootData
val newDataNode = ContentRootData(contentRootData.owner, contentRootData.rootPath)
ExternalSystemSourceType.entries.forEach { type ->
contentRootData.getPaths(type).forEach { sourceRoot ->
when (type) {
ExternalSystemSourceType.SOURCE if sourceRoot.path in generatedSources -> {
newDataNode.storePath(ExternalSystemSourceType.SOURCE_GENERATED, sourceRoot.path, sourceRoot.packagePrefix)
}
ExternalSystemSourceType.TEST if sourceRoot.path in generatedSources -> {
newDataNode.storePath(ExternalSystemSourceType.TEST_GENERATED, sourceRoot.path, sourceRoot.packagePrefix)
}
else -> {
newDataNode.storePath(type, sourceRoot.path, sourceRoot.packagePrefix)
}
}
}
}
return@visitData newDataNode
}
}
private fun DataNode<*>.updateSourceRootPackagePrefix(
propertiesForSourceSet: Pair<String, KotlinTaskProperties>?,
) {
visitData { contentRootData ->
if (contentRootData == null ||
propertiesForSourceSet == null ||
contentRootData as? ContentRootData == null
) return@visitData contentRootData
val packagePrefix = propertiesForSourceSet.second.packagePrefix ?: return@visitData contentRootData
val newDataNode = ContentRootData(contentRootData.owner, contentRootData.rootPath)
ExternalSystemSourceType.entries
.forEach { type ->
contentRootData.getPaths(type).forEach { path ->
if (type.isResource || type.isGenerated) {
newDataNode.storePath(type, path.path, path.packagePrefix)
} else {
newDataNode.storePath(type, path.path, packagePrefix)
}
}
}
newDataNode
}
}
@@ -0,0 +1,68 @@
// 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.kotlin.gradle
import com.intellij.openapi.externalSystem.model.ProjectKeys
import com.intellij.openapi.externalSystem.model.project.ContentRootData
import com.intellij.openapi.externalSystem.model.project.ExternalSystemSourceType
import com.intellij.openapi.externalSystem.service.project.ProjectDataManager
import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil
import org.jetbrains.kotlin.idea.codeInsight.gradle.KotlinGradleImportingTestCase
import org.jetbrains.plugins.gradle.model.data.GradleSourceSetData
import org.jetbrains.plugins.gradle.tooling.annotation.TargetVersions
import org.jetbrains.plugins.gradle.util.GradleConstants
import org.junit.Test
class KotlinGeneratedSourcesImportTest : KotlinGradleImportingTestCase() {
@Test
@TargetVersions("7.6+")
fun testGeneratedInMainSourceSet() {
configureByFiles()
importProject()
val generatedRoot = getContentRootFor("main", ExternalSystemSourceType.SOURCE_GENERATED)
assertSize(1, generatedRoot)
assertTrue(generatedRoot.first().path.endsWith("src/main/kotlinGen"))
}
@Test
@TargetVersions("7.6+")
fun testGeneratedInTestSourceSet() {
configureByFiles()
importProject()
val generatedRoot = getContentRootFor("test", ExternalSystemSourceType.TEST_GENERATED)
assertSize(1, generatedRoot)
assertTrue(generatedRoot.first().path.endsWith("src/test/kotlinGen"))
}
@Test
@TargetVersions("7.6+")
fun testGeneratedWithIdeaPlugin() {
configureByFiles()
importProject()
val generatedRoot = getContentRootFor("main", ExternalSystemSourceType.SOURCE_GENERATED)
assertSize(1, generatedRoot)
assertTrue(generatedRoot.first().path.endsWith("src/main/kotlinGen"))
}
override fun testDataDirName(): String = "kotlinGeneratedSourcesImportTest"
private fun getContentRootFor(
sourceSetName: String,
type: ExternalSystemSourceType,
): Collection<ContentRootData.SourceRoot> {
val gradleProjectData = ProjectDataManager.getInstance().getExternalProjectsData(myProject, GradleConstants.SYSTEM_ID).first()
val externalProjectData = gradleProjectData!!.externalProjectStructure!!
val modulesNodes = ExternalSystemApiUtil.findAll(externalProjectData, ProjectKeys.MODULE)
// main module
val mainModule = modulesNodes.first()
val gradleSourceSets = ExternalSystemApiUtil.findAll(mainModule, GradleSourceSetData.KEY)
val gradleSourceSet = gradleSourceSets.single { it.data.id == "${mainModule.data.id}:${sourceSetName}" }
val contentRootData = gradleSourceSet.children.single { it.data is ContentRootData }.data as ContentRootData
return contentRootData.getPaths(type)
}
}
@@ -400,7 +400,7 @@ abstract class KotlinGradleImportingTestCase : GradleImportingTestCase(),
const val LATEST_STABLE_GRADLE_PLUGIN_VERSION = "2.0.0"
val SUPPORTED_GRADLE_VERSIONS = arrayOf("6.8.3", "7.6")//, "9.0.0", "9.1.0") // To support them in KTIJ-36754
val SUPPORTED_GRADLE_VERSIONS = arrayOf("6.8.3", "7.6.5")//, "9.0.0", "9.1.0") // To support them in KTIJ-36754
// https://kotlinlang.org/docs/gradle-configure-project.html#targeting-the-jvm
val GRADLE_TO_KGP_VERSION = mapOf(
@@ -28,10 +28,10 @@ import org.jetbrains.plugins.gradle.model.ProjectImportModelProvider.GradleModel
import org.jetbrains.plugins.gradle.tooling.Message
import org.jetbrains.plugins.gradle.tooling.ModelBuilderContext
import org.jetbrains.plugins.gradle.tooling.ModelBuilderService
import java.io.File
import java.io.Serializable
import java.lang.reflect.InvocationTargetException
import java.util.Locale
import kotlin.io.path.absolutePathString
typealias AdditionalVisibleSourceSetsBySourceSet = Map</* Source Set Name */ String, /* Visible Source Set Names */ Set<String>>
@@ -48,6 +48,7 @@ interface KotlinGradleModel : Serializable {
val kotlinTaskProperties: KotlinTaskPropertiesBySourceSet
val gradleUserHome: String
val kotlinGradlePluginVersion: KotlinGradlePluginVersion?
val generatedSourcesRoots: List<String>
}
data class KotlinGradleModelImpl(
@@ -60,7 +61,8 @@ data class KotlinGradleModelImpl(
override val kotlinTarget: String? = null,
override val kotlinTaskProperties: KotlinTaskPropertiesBySourceSet,
override val gradleUserHome: String,
override val kotlinGradlePluginVersion: KotlinGradlePluginVersion?
override val kotlinGradlePluginVersion: KotlinGradlePluginVersion?,
override val generatedSourcesRoots: List<String>,
) : KotlinGradleModel
abstract class AbstractKotlinGradleModelBuilder : ModelBuilderService {
@@ -235,6 +237,7 @@ class KotlinGradleModelBuilder : AbstractKotlinGradleModelBuilder(), ModelBuilde
val compilerArgumentsBySourceSet = LinkedHashMap<String, List<String>>()
val additionalVisibleSourceSets = LinkedHashMap<String, Set<String>>()
val extraProperties = HashMap<String, KotlinTaskProperties>()
val generatedSources = mutableSetOf<String>()
val kotlinCompileTasks = target?.let { it.compilations ?: emptyList() }
?.mapNotNull { compilation -> compilation.getCompileKotlinTaskName(project) }
@@ -251,6 +254,8 @@ class KotlinGradleModelBuilder : AbstractKotlinGradleModelBuilder(), ModelBuilde
extraProperties.acknowledgeTask(compileTask, null)
}
generatedSources.addAll(project.getKotlinSourceSetGeneratedSourceRoots().map { it.absolutePathString() })
val platform = platformPluginId ?: pluginToPlatform.entries.singleOrNull { project.plugins.findPlugin(it.key) != null }?.value
if (builderContext != null) {
@@ -267,7 +272,8 @@ class KotlinGradleModelBuilder : AbstractKotlinGradleModelBuilder(), ModelBuilde
kotlinTarget = platform ?: kotlinPluginId,
kotlinTaskProperties = extraProperties,
gradleUserHome = project.gradle.gradleUserHomeDir.absolutePath,
kotlinGradlePluginVersion = project.kotlinGradlePluginVersion()
kotlinGradlePluginVersion = project.kotlinGradlePluginVersion(),
generatedSourcesRoots = generatedSources.toList(),
)
}
@@ -0,0 +1,27 @@
// 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.kotlin.idea.gradleTooling
import org.gradle.api.Named
import org.gradle.api.NamedDomainObjectCollection
import org.gradle.api.Project
import org.jetbrains.kotlin.idea.gradleTooling.reflect.KotlinSourceSetReflection
import java.nio.file.Path
internal fun Project.getKotlinSourceSetGeneratedSourceRoots(): List<Path> {
val kotlinExtension = project.extensions.findByName("kotlin") ?: return emptyList()
val kotlinExtensionClass = kotlinExtension.javaClass
val getSourceSets = kotlinExtensionClass.getMethodOrNull("getSourceSets") ?: return emptyList()
val sourceSets = getSourceSets.invoke(kotlinExtension) as NamedDomainObjectCollection<*>
val generatedSourceRoots = sourceSets.map { it as Named }.fold(mutableListOf<Path>()) { acc, named ->
named.getKotlinSourceSetGeneratedSourceRoots()?.let { acc.addAll(it) }
acc
}
return generatedSourceRoots.toList()
}
private fun Named.getKotlinSourceSetGeneratedSourceRoots(): List<Path>? {
return KotlinSourceSetReflection(this).generatedSources?.srcDirs?.map {
@Suppress("IO_FILE_USAGE") // Gradle API
it.toPath()
}
}
@@ -15,6 +15,7 @@ interface KotlinSourceSetReflection {
val languageSettings: KotlinLanguageSettingsReflection?
val kotlin: SourceDirectorySet?
val resources: SourceDirectorySet?
val generatedSources: SourceDirectorySet?
val dependsOn: Set<KotlinSourceSetReflection>
val additionalVisibleSourceSets: Set<KotlinSourceSetReflection>
val androidSourceSetInfo: KotlinAndroidSourceSetInfoReflection?
@@ -36,6 +37,10 @@ private class KotlinSourceSetReflectionImpl(override val instance: Named) : Kotl
instance.callReflectiveGetter("getResources", logger)
}
override val generatedSources: SourceDirectorySet? by lazy {
instance.callReflectiveGetter("getGeneratedKotlin", logger)
}
override val dependsOn: Set<KotlinSourceSetReflection> by lazy {
instance.callReflectiveGetter<Iterable<Named>>("getDependsOn", logger)?.map {
KotlinSourceSetReflection(it)
@@ -0,0 +1,25 @@
plugins {
id 'org.jetbrains.kotlin.jvm' version '2.3.0'
}
repositories {
mavenCentral()
}
// Task to create the directory so it exists during import
def generatorTask = project.tasks.register("generator") {
def outputDirectory = project.layout.projectDirectory.dir("src/main/kotlinGen")
outputs.dir(outputDirectory)
doLast {
outputDirectory.file("generatedCommon.kt").asFile.writeText(
//language=kotlin
"""
fun printHello() {
println("hello")
}
""".trimIndent()
)
}
}
kotlin.sourceSets.main.generatedKotlin.srcDir(generatorTask)
@@ -0,0 +1,25 @@
plugins {
id 'org.jetbrains.kotlin.jvm' version '2.3.0'
}
repositories {
mavenCentral()
}
// Task to create the directory so it exists during import
def generatorTask = project.tasks.register("generator") {
def outputDirectory = project.layout.projectDirectory.dir("src/test/kotlinGen")
outputs.dir(outputDirectory)
doLast {
outputDirectory.file("generatedCommon.kt").asFile.writeText(
//language=kotlin
"""
fun printHello() {
println("hello")
}
""".trimIndent()
)
}
}
kotlin.sourceSets.test.generatedKotlin.srcDir(generatorTask)
@@ -0,0 +1,32 @@
plugins {
id 'org.jetbrains.kotlin.jvm' version '2.3.0'
id 'idea'
}
repositories {
mavenCentral()
}
// Task to create the directory so it exists during import
def generatorTask = project.tasks.register("generator") {
def outputDirectory = project.layout.projectDirectory.dir("src/main/kotlinGen")
outputs.dir(outputDirectory)
doLast {
outputDirectory.file("generatedCommon.kt").asFile.writeText(
//language=kotlin
"""
fun printHello() {
println("hello")
}
""".trimIndent()
)
}
}
kotlin.sourceSets.main.generatedKotlin.srcDir(generatorTask)
idea {
module {
generatedSourceDirs += file("src/main/kotlinGen")
}
}