From 445d7b3ab8ae78b8a67df73e6d0306c26dd91255 Mon Sep 17 00:00:00 2001 From: Willem Veelenturf Date: Thu, 26 Mar 2026 20:30:40 +0100 Subject: [PATCH] fix: fall back to FirNamedReference when property symbol resolution fails (#22) When target types are from generated source directories (e.g. Wirespec), toResolvedPropertySymbol() can return null. Fall back to reading the field name from FirNamedReference on the calleeReference, which is always preserved by KMapperAssignAlterer. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../compiler/fir/KMapperFirMappingChecker.kt | 15 +++-- .../flock/kmapper/SerializableTest.kt | 59 +++++++++++++++++++ 2 files changed, 70 insertions(+), 4 deletions(-) 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 558a0b1..e39244f 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 @@ -20,6 +20,7 @@ import org.jetbrains.kotlin.fir.declarations.utils.isEnumClass import org.jetbrains.kotlin.fir.expressions.FirAnonymousFunctionExpression import org.jetbrains.kotlin.fir.expressions.FirCall import org.jetbrains.kotlin.fir.expressions.FirFunctionCall +import org.jetbrains.kotlin.fir.expressions.FirQualifiedAccessExpression import org.jetbrains.kotlin.fir.expressions.arguments import org.jetbrains.kotlin.fir.expressions.explicitReceiver import org.jetbrains.kotlin.fir.expressions.toReference @@ -96,10 +97,13 @@ class KMapperFirMappingChecker(val collector: MessageCollector, private val sess val propSymbol = call.explicitReceiver ?.toReference(session) ?.toResolvedPropertySymbol() + val propName = propSymbol?.name + ?: (call.explicitReceiver as? FirQualifiedAccessExpression) + ?.calleeReference?.let { it as? FirNamedReference }?.name val valueExpr = call.arguments.firstOrNull() - if (propSymbol != null && valueExpr != null) { + if (propName != null && valueExpr != null) { Field( - name = propSymbol.name, + name = propName, type = valueExpr.resolvedType, hasDefaultValue = false, fields = valueExpr.resolvedType.resolveConstructorFields() @@ -110,9 +114,12 @@ class KMapperFirMappingChecker(val collector: MessageCollector, private val sess val propSymbol = call.extensionReceiver ?.toReference(session) ?.toResolvedPropertySymbol() - if (propSymbol != null) { + val propName = propSymbol?.name + ?: (call.extensionReceiver as? FirQualifiedAccessExpression) + ?.calleeReference?.let { it as? FirNamedReference }?.name + if (propName != null) { // Find the matching toField to include its type info - toFields.find { it.name == propSymbol.name } + toFields.find { it.name == propName } } else null } else -> null diff --git a/test-integration/src/test/kotlin/community/flock/kmapper/SerializableTest.kt b/test-integration/src/test/kotlin/community/flock/kmapper/SerializableTest.kt index 28edc48..52c38f7 100644 --- a/test-integration/src/test/kotlin/community/flock/kmapper/SerializableTest.kt +++ b/test-integration/src/test/kotlin/community/flock/kmapper/SerializableTest.kt @@ -111,6 +111,65 @@ class SerializableTest { } } + @Test + fun shouldCompile_explicitMappingWithSerializableTargetInSeparateFile() { + IntegrationTest(options) + .file("Dto.kt") { + $$""" + |package sample + | + |import kotlinx.serialization.Serializable + |import kotlinx.serialization.SerialName + | + |@Serializable + |@SerialName("RunAnnotationDataDto") + |data class RunAnnotationDataDto( + | val runId: String, + | val displayId: String, + | val inputHash: String, + | val outputJson: String, + |) + | + """.trimMargin() + } + .file("App.kt") { + $$""" + |package sample + | + |import community.flock.kmapper.mapper + | + |data class RunAnnotationData( + | val runId: String, + | val sequenceNumber: Int, + | val inputHash: String, + | val outputJson: String, + |) + | + |fun RunAnnotationData.toDto(): RunAnnotationDataDto = mapper { + | displayId = it.sequenceNumber.toString() + |} + | + |fun main() { + | val data = RunAnnotationData( + | runId = "run-1", + | sequenceNumber = 42, + | inputHash = "abc123", + | outputJson = "{}" + | ) + | val dto = data.toDto() + | println(dto) + |} + | + """.trimMargin() + } + .compileSuccess { output -> + assertTrue( + output.contains("RunAnnotationDataDto(runId=run-1, displayId=42, inputHash=abc123, outputJson={})"), + "Expected RunAnnotationDataDto(runId=run-1, displayId=42, inputHash=abc123, outputJson={}) in output" + ) + } + } + @Test fun shouldCompile_allFieldsAutoMappedWithSerializable() { IntegrationTest(options)