From 4479e0bc0ebaa3aea42455aa9211f988a8709d31 Mon Sep 17 00:00:00 2001 From: Willem Veelenturf Date: Thu, 26 Mar 2026 15:54:22 +0100 Subject: [PATCH] fix: resolve properties for @Serializable target classes Remove the FirBinaryDependenciesModuleData check that prevented property resolution for types involving binary dependencies. This blocked mapping to @Serializable target classes because the serialization compiler plugin adds synthetic properties from binary module data. Also refactor IntegrationTest to support additional Gradle plugins and dependencies, enabling integration tests with kotlinx.serialization. Closes #20 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../compiler/fir/KMapperFirMappingChecker.kt | 1 - .../flock/kmapper/IntegrationTest.kt | 60 +++---- .../flock/kmapper/SerializableTest.kt | 146 ++++++++++++++++++ 3 files changed, 180 insertions(+), 27 deletions(-) create mode 100644 test-integration/src/test/kotlin/community/flock/kmapper/SerializableTest.kt diff --git a/compiler-plugin/src/main/kotlin/community/flock/kmapper/compiler/fir/KMapperFirMappingChecker.kt b/compiler-plugin/src/main/kotlin/community/flock/kmapper/compiler/fir/KMapperFirMappingChecker.kt index cf1464d..558a0b1 100644 --- a/compiler-plugin/src/main/kotlin/community/flock/kmapper/compiler/fir/KMapperFirMappingChecker.kt +++ b/compiler-plugin/src/main/kotlin/community/flock/kmapper/compiler/fir/KMapperFirMappingChecker.kt @@ -176,7 +176,6 @@ class KMapperFirMappingChecker(val collector: MessageCollector, private val sess private fun ConeKotlinType.resolvePropertyFields(): List { val classSymbol = toRegularClassSymbol(session) - if (classSymbol?.moduleData is FirBinaryDependenciesModuleData) return emptyList() return classSymbol?.declaredProperties(session) .orEmpty() .map { property -> diff --git a/test-integration/src/main/kotlin/community/flock/kmapper/IntegrationTest.kt b/test-integration/src/main/kotlin/community/flock/kmapper/IntegrationTest.kt index 71399cc..6231d9f 100644 --- a/test-integration/src/main/kotlin/community/flock/kmapper/IntegrationTest.kt +++ b/test-integration/src/main/kotlin/community/flock/kmapper/IntegrationTest.kt @@ -5,7 +5,9 @@ import java.nio.file.Files class IntegrationTest(options: Options) { data class Options( - val kotlinVersion: String + val kotlinVersion: String, + val additionalPlugins: List = emptyList(), + val additionalDependencies: List = emptyList(), ) data class File( @@ -16,7 +18,7 @@ class IntegrationTest(options: Options) { val files = mutableListOf( settingsGradle, - buildGradle, + buildGradle(options), ) private fun compile(): GradleRunner { @@ -69,29 +71,35 @@ class IntegrationTest(options: Options) { """.trimMargin() ) - val buildGradle = File( - "", - "build.gradle.kts", - """ - |plugins { - | id("community.flock.kmapper") version "0.0.0-SNAPSHOT" - | kotlin("jvm") version "2.3.10" - | application - |} - |repositories { - | mavenCentral() - | mavenLocal() - |} - |dependencies { - | implementation(kotlin("stdlib")) - |} - |kotlin { - | jvmToolchain(21) - |} - |application { - | mainClass.set("sample.AppKt") - |} - """.trimMargin() - ) + fun buildGradle(options: Options): File { + val additionalPlugins = options.additionalPlugins.joinToString("\n") { "| $it" } + val additionalDeps = options.additionalDependencies.joinToString("\n") { "| implementation(\"$it\")" } + return File( + "", + "build.gradle.kts", + """ + |plugins { + | id("community.flock.kmapper") version "0.0.0-SNAPSHOT" + | kotlin("jvm") version "${options.kotlinVersion}" + | application + ${additionalPlugins} + |} + |repositories { + | mavenCentral() + | mavenLocal() + |} + |dependencies { + | implementation(kotlin("stdlib")) + ${additionalDeps} + |} + |kotlin { + | jvmToolchain(21) + |} + |application { + | mainClass.set("sample.AppKt") + |} + """.trimMargin() + ) + } } } \ No newline at end of file diff --git a/test-integration/src/test/kotlin/community/flock/kmapper/SerializableTest.kt b/test-integration/src/test/kotlin/community/flock/kmapper/SerializableTest.kt new file mode 100644 index 0000000..28edc48 --- /dev/null +++ b/test-integration/src/test/kotlin/community/flock/kmapper/SerializableTest.kt @@ -0,0 +1,146 @@ +package community.flock.kmapper + +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class SerializableTest { + + val options = IntegrationTest.Options( + kotlinVersion = "2.3.10", + additionalPlugins = listOf("""kotlin("plugin.serialization") version "2.3.10""""), + additionalDependencies = listOf("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1"), + ) + + @Test + fun shouldCompile_autoMapWithSerializableTarget() { + IntegrationTest(options) + .file("App.kt") { + $$""" + |package sample + | + |import community.flock.kmapper.mapper + |import kotlinx.serialization.Serializable + | + |data class Pagination( + | val pageNumber: Int, + | val pageSize: Int, + | val totalElements: Long, + |) + | + |@Serializable + |data class PaginationDto( + | val pageNumber: Long, + | val pageSize: Long, + | val totalElements: Long, + |) + | + |fun Pagination.toDto(): PaginationDto = mapper { + | pageNumber = it.pageNumber.toLong() + | pageSize = it.pageSize.toLong() + |} + | + |fun main() { + | val pagination = Pagination(pageNumber = 1, pageSize = 10, totalElements = 100L) + | val dto = pagination.toDto() + | println(dto) + |} + | + """.trimMargin() + } + .compileSuccess { output -> + assertTrue( + output.contains("PaginationDto(pageNumber=1, pageSize=10, totalElements=100)"), + "Expected PaginationDto(pageNumber=1, pageSize=10, totalElements=100) in output" + ) + } + } + + @Test + fun shouldCompile_autoMapWithSerializableAndSealedInterface() { + IntegrationTest(options) + .file("App.kt") { + $$""" + |package sample + | + |import community.flock.kmapper.mapper + |import kotlinx.serialization.Serializable + |import kotlinx.serialization.SerialName + | + |sealed interface Message { + | val sender: String + | val createdAt: String + | val category: String + | + | data class Text( + | override val sender: String, + | override val createdAt: String, + | val text: String, + | override val category: String = "EXECUTION", + | ) : Message + |} + | + |@Serializable + |sealed interface MessageDto + | + |@Serializable + |@SerialName("TextMessageDto") + |data class TextMessageDto( + | val sender: String, + | val createdAt: String, + | val text: String, + |) : MessageDto + | + |fun Message.Text.toDto(): TextMessageDto = mapper { + | sender = it.sender.uppercase() + | createdAt = it.createdAt + |} + | + |fun main() { + | val message = Message.Text(sender = "agent", createdAt = "2025-01-01", text = "hello") + | val dto = message.toDto() + | println(dto) + |} + | + """.trimMargin() + } + .compileSuccess { output -> + assertTrue( + output.contains("TextMessageDto(sender=AGENT, createdAt=2025-01-01, text=hello)"), + "Expected TextMessageDto(sender=AGENT, createdAt=2025-01-01, text=hello) in output" + ) + } + } + + @Test + fun shouldCompile_allFieldsAutoMappedWithSerializable() { + IntegrationTest(options) + .file("App.kt") { + $$""" + |package sample + | + |import community.flock.kmapper.mapper + |import kotlinx.serialization.Serializable + | + |data class Result(val description: String, val response: String?) + | + |@Serializable + |data class ResultDto(val description: String, val response: String?) + | + |fun Result.toDto(): ResultDto = mapper() + | + |fun main() { + | val result = Result(description = "done", response = "ok") + | val dto = result.toDto() + | println(dto) + |} + | + """.trimMargin() + } + .compileSuccess { output -> + assertTrue( + output.contains("ResultDto(description=done, response=ok)"), + "Expected ResultDto(description=done, response=ok) in output" + ) + } + } +}