From de21bec1fe9b8554986ac5ab76921c785bf8ffb7 Mon Sep 17 00:00:00 2001 From: Elijah Semyonov Date: Tue, 3 Feb 2026 10:45:34 +0000 Subject: [PATCH] KMT-2065 KMT-2064 KMT-2063 Xcode parity Swift Export Enables SourceKit code insight for Swift code importing Kotlin modules exported via Swift Export using the .swiftmodule files produced during the regular build. ### Changes - Gradle Sync: Extract Swift Export data model produced by DSL configuration (moduleName, flattenPackage) from KGP via reflection and persist in ProjectDataManager (same way ObjC export does it) - SourceKit Integration: Provide -I compiler flags to SourceKit for built .swiftmodule files, enabling code completion for imported Kotlin modules - Build Listener: Track Swift Export module paths after embed* gradle tasks completion to locate generated artifacts in build/SPMBuild///. KGP SwiftExport runner copies it to Xcode build folder from there during Xcode run, we hardcode the path relative to the module with SwiftExport DSL. Merge-request: IJ-MR-187833 Merged-by: Elijah Semyonov GitOrigin-RevId: 634c7e7a4bdda0bca3e5996dedefb7f2752baa17 --- .../projectModel/KotlinSwiftExportModel.kt | 44 +++++++++++ .../gradleTooling/KotlinMPPGradleModel.kt | 14 ++++ .../KotlinMPPGradleModelBuilder.kt | 25 +++++- .../gradleTooling/KotlinMPPGradleModelImpl.kt | 29 ++++++- .../reflect/KotlinExtensionReflection.kt | 31 ++++++++ .../reflect/KotlinSwiftExportReflection.kt | 78 +++++++++++++++++++ 6 files changed, 217 insertions(+), 4 deletions(-) create mode 100644 plugins/kotlin/base/project-model/src/org/jetbrains/kotlin/idea/projectModel/KotlinSwiftExportModel.kt create mode 100644 plugins/kotlin/gradle/gradle-tooling/impl/src/org/jetbrains/kotlin/idea/gradleTooling/reflect/KotlinSwiftExportReflection.kt diff --git a/plugins/kotlin/base/project-model/src/org/jetbrains/kotlin/idea/projectModel/KotlinSwiftExportModel.kt b/plugins/kotlin/base/project-model/src/org/jetbrains/kotlin/idea/projectModel/KotlinSwiftExportModel.kt new file mode 100644 index 000000000000..40722e6ecad2 --- /dev/null +++ b/plugins/kotlin/base/project-model/src/org/jetbrains/kotlin/idea/projectModel/KotlinSwiftExportModel.kt @@ -0,0 +1,44 @@ +package org.jetbrains.kotlin.idea.projectModel + +import java.io.Serializable + +/** + * Model representing Swift Export configuration extracted from the Kotlin Gradle Plugin. + * + * This model captures the configuration from the `swiftExport {}` DSL block in build.gradle.kts + * and is serialized during Gradle sync for use by IDE features. + * + * ## KGP Reference + * + * `org.jetbrains.kotlin.gradle.plugin.mpp.apple.swiftexport.SwiftExportExtension` + * + * ## Usage in build.gradle.kts + * + * ```kotlin + * kotlin { + * swiftExport { + * moduleName = "MyModule" + * flattenPackage = "com.example.app" + * } + * } + * ``` + */ +interface KotlinSwiftExportModel : Serializable { + /** + * The configured module name for Swift Export. + * + * KGP: `SwiftExportExtension.moduleName: Property` (inherited from `SwiftExportedModuleMetadata`) + * + * If not set, the project name is used as the default module name. + */ + val moduleName: String? + + /** + * The package to flatten in the exported Swift module. + * + * KGP: `SwiftExportExtension.flattenPackage: Property` (inherited from `SwiftExportedModuleMetadata`) + * + * When set, the specified package prefix is collapsed in the generated Swift API. + */ + val flattenPackage: String? +} diff --git a/plugins/kotlin/gradle/gradle-tooling/impl/src/org/jetbrains/kotlin/idea/gradleTooling/KotlinMPPGradleModel.kt b/plugins/kotlin/gradle/gradle-tooling/impl/src/org/jetbrains/kotlin/idea/gradleTooling/KotlinMPPGradleModel.kt index 5ea2da27ef68..314a204b6b46 100644 --- a/plugins/kotlin/gradle/gradle-tooling/impl/src/org/jetbrains/kotlin/idea/gradleTooling/KotlinMPPGradleModel.kt +++ b/plugins/kotlin/gradle/gradle-tooling/impl/src/org/jetbrains/kotlin/idea/gradleTooling/KotlinMPPGradleModel.kt @@ -7,6 +7,7 @@ import org.jetbrains.kotlin.idea.projectModel.KotlinDependencyId import org.jetbrains.kotlin.idea.projectModel.KotlinGradlePluginVersionDependentApi import org.jetbrains.kotlin.idea.projectModel.KotlinSourceSet import org.jetbrains.kotlin.idea.projectModel.KotlinTarget +import org.jetbrains.kotlin.idea.projectModel.KotlinSwiftExportModel import java.io.Serializable interface KotlinMPPGradleModel : KotlinSourceSetContainer, Serializable { @@ -27,6 +28,19 @@ interface KotlinMPPGradleModel : KotlinSourceSetContainer, Serializable { val kotlinImportingDiagnostics: KotlinImportingDiagnosticsContainer val kotlinGradlePluginVersion: KotlinGradlePluginVersion? + /** + * Swift Export configuration if enabled in the project, or null if not configured. + * + * This field is populated by [KotlinMPPGradleModelBuilder.buildSwiftExportModel] during Gradle sync. + * + * ## KGP Reference + * `org.jetbrains.kotlin.gradle.plugin.mpp.apple.swiftexport.SwiftExportExtension` + * + * @see KotlinSwiftExportModel + * @see org.jetbrains.kotlin.idea.gradleTooling.reflect.KotlinSwiftExportReflection + */ + val swiftExport: KotlinSwiftExportModel? + companion object { const val NO_KOTLIN_NATIVE_HOME = "" } diff --git a/plugins/kotlin/gradle/gradle-tooling/impl/src/org/jetbrains/kotlin/idea/gradleTooling/KotlinMPPGradleModelBuilder.kt b/plugins/kotlin/gradle/gradle-tooling/impl/src/org/jetbrains/kotlin/idea/gradleTooling/KotlinMPPGradleModelBuilder.kt index 2367a8ba1fd1..03e196f566e1 100644 --- a/plugins/kotlin/gradle/gradle-tooling/impl/src/org/jetbrains/kotlin/idea/gradleTooling/KotlinMPPGradleModelBuilder.kt +++ b/plugins/kotlin/gradle/gradle-tooling/impl/src/org/jetbrains/kotlin/idea/gradleTooling/KotlinMPPGradleModelBuilder.kt @@ -72,7 +72,7 @@ class KotlinMPPGradleModelBuilder : AbstractModelBuilderService() { val coroutinesState = getCoroutinesState(project) val kotlinNativeHome = KotlinNativeHomeEvaluator.getKotlinNativeHome(project) ?: NO_KOTLIN_NATIVE_HOME - + val swiftExportModel = buildSwiftExportModel(kotlinExtensionReflection) val model = KotlinMPPGradleModelImpl( sourceSetsByName = filterOrphanSourceSets(importingContext), @@ -84,7 +84,8 @@ class KotlinMPPGradleModelBuilder : AbstractModelBuilderService() { kotlinNativeHome = kotlinNativeHome, dependencyMap = importingContext.dependencyMapper.toDependencyMap(), dependencies = dependenciesContainer, - kotlinGradlePluginVersion = importingContext.kotlinGradlePluginVersion + kotlinGradlePluginVersion = importingContext.kotlinGradlePluginVersion, + swiftExport = swiftExportModel ).apply { kotlinImportingDiagnostics += collectDiagnostics(importingContext) } @@ -213,6 +214,26 @@ class KotlinMPPGradleModelBuilder : AbstractModelBuilderService() { return extension.kotlinExtension.javaClass.getMethodOrNull("getTargets") != null && extension.targets.isNotEmpty() } + /** + * Builds the [KotlinSwiftExportModelImpl] from the Swift Export DSL configuration. + * + * ## KGP Reference + * `org.jetbrains.kotlin.gradle.plugin.mpp.apple.swiftexport.SwiftExportExtension` + * + * The extension is registered via `addExtension("swiftExport", ...)` in `SetupSwiftExportDSL.kt`, + * so it's accessed via [KotlinExtensionReflection.swiftExport] which uses `getExtensions().findByName()`. + * + * @return The Swift Export model if the extension is configured, null otherwise + * @see org.jetbrains.kotlin.idea.gradleTooling.reflect.KotlinSwiftExportReflection + */ + private fun buildSwiftExportModel(extension: KotlinExtensionReflection): KotlinSwiftExportModelImpl? { + val swiftExportReflection = extension.swiftExport ?: return null + return KotlinSwiftExportModelImpl( + moduleName = swiftExportReflection.moduleName, + flattenPackage = swiftExportReflection.flattenPackage + ) + } + companion object { private val DEFAULT_IMPORTING_CHECKERS = listOf(OrphanSourceSetImportingChecker) diff --git a/plugins/kotlin/gradle/gradle-tooling/impl/src/org/jetbrains/kotlin/idea/gradleTooling/KotlinMPPGradleModelImpl.kt b/plugins/kotlin/gradle/gradle-tooling/impl/src/org/jetbrains/kotlin/idea/gradleTooling/KotlinMPPGradleModelImpl.kt index d5540e07534f..0b07266f9ce4 100644 --- a/plugins/kotlin/gradle/gradle-tooling/impl/src/org/jetbrains/kotlin/idea/gradleTooling/KotlinMPPGradleModelImpl.kt +++ b/plugins/kotlin/gradle/gradle-tooling/impl/src/org/jetbrains/kotlin/idea/gradleTooling/KotlinMPPGradleModelImpl.kt @@ -23,6 +23,7 @@ import org.jetbrains.kotlin.idea.projectModel.KotlinTargetJar import org.jetbrains.kotlin.idea.projectModel.KotlinTaskProperties import org.jetbrains.kotlin.idea.projectModel.KotlinTestRunTask import org.jetbrains.kotlin.idea.projectModel.KotlinWasmCompilationExtensions +import org.jetbrains.kotlin.idea.projectModel.KotlinSwiftExportModel import java.io.File class KotlinAndroidSourceSetInfoImpl( @@ -282,6 +283,28 @@ data class ExtraFeaturesImpl( override val isHMPPEnabled: Boolean, ) : ExtraFeatures +/** + * Implementation of [KotlinSwiftExportModel] for serialization during Gradle sync. + * + * ## KGP Reference + * + * This class captures data from the Swift Export DSL in the Kotlin Gradle Plugin: + * - Extension: `org.jetbrains.kotlin.gradle.plugin.mpp.apple.swiftexport.SwiftExportExtension` + * - Source: `libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/apple/swiftexport/SwiftExportExtension.kt` + * + * @see KotlinSwiftExportModel + * @see KotlinMPPGradleModelBuilder.buildSwiftExportModel + */ +data class KotlinSwiftExportModelImpl( + override val moduleName: String?, + override val flattenPackage: String? +) : KotlinSwiftExportModel { + constructor(swiftExport: KotlinSwiftExportModel) : this( + moduleName = swiftExport.moduleName, + flattenPackage = swiftExport.flattenPackage + ) +} + data class KotlinMPPGradleModelImpl @OptIn(KotlinGradlePluginVersionDependentApi::class) constructor( override val sourceSetsByName: Map, override val targets: Collection, @@ -290,7 +313,8 @@ data class KotlinMPPGradleModelImpl @OptIn(KotlinGradlePluginVersionDependentApi override val dependencyMap: Map, override val dependencies: IdeaKotlinDependenciesContainer?, override val kotlinImportingDiagnostics: KotlinImportingDiagnosticsContainer = mutableSetOf(), - override val kotlinGradlePluginVersion: KotlinGradlePluginVersion? + override val kotlinGradlePluginVersion: KotlinGradlePluginVersion?, + override val swiftExport: KotlinSwiftExportModel? = null ) : KotlinMPPGradleModel { @OptIn(KotlinGradlePluginVersionDependentApi::class) @@ -312,7 +336,8 @@ data class KotlinMPPGradleModelImpl @OptIn(KotlinGradlePluginVersionDependentApi dependencyMap = mppModel.dependencyMap.map { it.key to it.value.deepCopy(cloningCache) }.toMap(), dependencies = mppModel.dependencies, kotlinImportingDiagnostics = mppModel.kotlinImportingDiagnostics.mapTo(mutableSetOf()) { it.deepCopy(cloningCache) }, - kotlinGradlePluginVersion = mppModel.kotlinGradlePluginVersion?.reparse() + kotlinGradlePluginVersion = mppModel.kotlinGradlePluginVersion?.reparse(), + swiftExport = mppModel.swiftExport?.let { KotlinSwiftExportModelImpl(it) } ) } diff --git a/plugins/kotlin/gradle/gradle-tooling/impl/src/org/jetbrains/kotlin/idea/gradleTooling/reflect/KotlinExtensionReflection.kt b/plugins/kotlin/gradle/gradle-tooling/impl/src/org/jetbrains/kotlin/idea/gradleTooling/reflect/KotlinExtensionReflection.kt index f6bf64e57112..eb5fd02f3cee 100644 --- a/plugins/kotlin/gradle/gradle-tooling/impl/src/org/jetbrains/kotlin/idea/gradleTooling/reflect/KotlinExtensionReflection.kt +++ b/plugins/kotlin/gradle/gradle-tooling/impl/src/org/jetbrains/kotlin/idea/gradleTooling/reflect/KotlinExtensionReflection.kt @@ -33,8 +33,39 @@ class KotlinExtensionReflection( sourceSets.associateBy { it.name } } + /** + * Returns the Swift Export extension if configured, or null if Swift Export is not enabled. + * + * ## KGP Reference + * + * The Swift Export extension is registered in the Kotlin Gradle Plugin via: + * - Setup: `SetUpSwiftExportAction` in `SetupSwiftExportDSL.kt` + * - Registration: `multiplatformExtension.addExtension("swiftExport", swiftExportExtension)` + * - Source: `libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/apple/swiftexport/SetupSwiftExportDSL.kt` + * + * Because the extension is added via `addExtension()` (not as a direct property), + * we must access it via `getExtensions().findByName("swiftExport")` rather than `getSwiftExport()`. + * + * The extension name constant is defined in `SwiftExportDSLConstants.SWIFT_EXPORT_EXTENSION_NAME = "swiftExport"`. + */ + val swiftExport: KotlinSwiftExportReflection? by lazy { + val extensions = kotlinExtension.callReflectiveAnyGetter("getExtensions", logger) ?: return@lazy null + val swiftExportExtension = extensions.callReflective( + "findByName", + parameters(parameter(SWIFT_EXPORT_EXTENSION_NAME)), + returnType(), + logger + ) ?: return@lazy null + KotlinSwiftExportReflection(swiftExportExtension) + } + companion object { private val logger: ReflectionLogger = ReflectionLogger(KotlinExtensionReflection::class.java) private const val KOTLIN_PLUGIN_WRAPPER_FILE_CLASS_NAME = "org.jetbrains.kotlin.gradle.plugin.KotlinPluginWrapperKt" + + /** + * KGP: `SwiftExportDSLConstants.SWIFT_EXPORT_EXTENSION_NAME` in `SetupSwiftExportDSL.kt` + */ + private const val SWIFT_EXPORT_EXTENSION_NAME = "swiftExport" } } \ No newline at end of file diff --git a/plugins/kotlin/gradle/gradle-tooling/impl/src/org/jetbrains/kotlin/idea/gradleTooling/reflect/KotlinSwiftExportReflection.kt b/plugins/kotlin/gradle/gradle-tooling/impl/src/org/jetbrains/kotlin/idea/gradleTooling/reflect/KotlinSwiftExportReflection.kt new file mode 100644 index 000000000000..c38c896dec46 --- /dev/null +++ b/plugins/kotlin/gradle/gradle-tooling/impl/src/org/jetbrains/kotlin/idea/gradleTooling/reflect/KotlinSwiftExportReflection.kt @@ -0,0 +1,78 @@ +package org.jetbrains.kotlin.idea.gradleTooling.reflect + +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider + +/** + * Reflection wrapper for `SwiftExportExtension` from the Kotlin Gradle Plugin. + * + * This class accesses the Swift Export model configured by DSL via reflection. + * + * ## KGP reference + * `org.jetbrains.kotlin.gradle.plugin.mpp.apple.swiftexport.SwiftExportExtension` + * + * ## DSL example in build.gradle.kts + * + * ```kotlin + * kotlin { + * swiftExport { + * moduleName.set("MyModule") + * flattenPackage.set("com.example") + * } + * } + * ``` + * + * @see KotlinExtensionReflection.swiftExport + */ +fun KotlinSwiftExportReflection(swiftExportExtension: Any): KotlinSwiftExportReflection = KotlinSwiftExportReflectionImpl(swiftExportExtension) + +interface KotlinSwiftExportReflection { + /** + * The configured module name for Swift Export, or null if not configured. + * + * KGP: `SwiftExportExtension.moduleName: Property` + */ + val moduleName: String? + + /** + * The package to flatten in the exported Swift module, or null if not configured. + * + * KGP: `SwiftExportExtension.flattenPackage: Property` + */ + val flattenPackage: String? +} + +private class KotlinSwiftExportReflectionImpl(private val instance: Any) : KotlinSwiftExportReflection { + + override val moduleName: String? by lazy { + getPropertyValue("getModuleName") + } + + override val flattenPackage: String? by lazy { + getPropertyValue("getFlattenPackage") + } + + /** + * Helper to get value from a Gradle `Property` via reflection. + * + * The Swift Export extension uses Gradle's lazy properties (`Property`), + * so we need to call `.orNull` to get the actual value. + */ + private fun getPropertyValue(getterName: String): String? { + return try { + val property = instance.callReflectiveAnyGetter(getterName, logger) + when (property) { + is Property<*> -> property.orNull as? String + is Provider<*> -> property.orNull as? String + else -> property as? String + } + } + catch (_: Exception) { + null + } + } + + companion object { + private val logger = ReflectionLogger(KotlinSwiftExportReflection::class.java) + } +}