diff --git a/plugins/kotlinx-serialization/kotlinx-serialization.backend/build.gradle.kts b/plugins/kotlinx-serialization/kotlinx-serialization.backend/build.gradle.kts index 99fecdac26a..953b3c7e7fd 100644 --- a/plugins/kotlinx-serialization/kotlinx-serialization.backend/build.gradle.kts +++ b/plugins/kotlinx-serialization/kotlinx-serialization.backend/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { compileOnly(project(":compiler:ir.tree")) compileOnly(project(":compiler:fir:fir2ir")) compileOnly(project(":compiler:fir:tree")) + compileOnly(project(":compiler:fir:fir-deserialization")) compileOnly(project(":js:js.frontend")) compileOnly(project(":js:js.translator")) compileOnly(project(":kotlin-util-klib-metadata")) diff --git a/plugins/kotlinx-serialization/kotlinx-serialization.backend/src/org/jetbrains/kotlinx/serialization/compiler/backend/ir/IrSerializableProperties.kt b/plugins/kotlinx-serialization/kotlinx-serialization.backend/src/org/jetbrains/kotlinx/serialization/compiler/backend/ir/IrSerializableProperties.kt index b517cf90420..78d27d9e5d9 100644 --- a/plugins/kotlinx-serialization/kotlinx-serialization.backend/src/org/jetbrains/kotlinx/serialization/compiler/backend/ir/IrSerializableProperties.kt +++ b/plugins/kotlinx-serialization/kotlinx-serialization.backend/src/org/jetbrains/kotlinx/serialization/compiler/backend/ir/IrSerializableProperties.kt @@ -8,6 +8,7 @@ package org.jetbrains.kotlinx.serialization.compiler.backend.ir import org.jetbrains.kotlin.descriptors.DescriptorVisibilities import org.jetbrains.kotlin.fir.declarations.impl.FirDefaultPropertyGetter import org.jetbrains.kotlin.fir.declarations.impl.FirPrimaryConstructor +import org.jetbrains.kotlin.fir.deserialization.registeredInSerializationPluginMetadataExtension import org.jetbrains.kotlin.fir.lazy.Fir2IrLazyProperty import org.jetbrains.kotlin.ir.ObsoleteDescriptorBasedAPI import org.jetbrains.kotlin.ir.declarations.IrClass @@ -57,11 +58,12 @@ fun IrProperty.analyzeIfFromAnotherModule(): Pair { val hasDefault = descriptor.declaresDefaultValue() hasDefault to (descriptor.backingField != null || hasDefault) } else if (this is Fir2IrLazyProperty) { - // Fir2IrLazyProperty after K2 correctly deserializes information about backing field. - // However, nor Fir2IrLazyProp nor deserialized FirProperty do not store default value (initializer expression) for property, + // Deserialized properties don't contain information about backing field, so we should extract this information from the + // attribute, which is set if the property was mentioned in SerializationPluginMetadataExtensions. + // Also, deserialized properties do not store default value (initializer expression) for property, // so we either should find corresponding constructor parameter and check its default, or rely on less strict check for default getter. // Comments are copied from PropertyDescriptor.declaresDefaultValue() as it has similar logic. - val hasBackingField = backingField != null + val hasBackingField = fir.symbol.registeredInSerializationPluginMetadataExtension val matchingPrimaryConstructorParam = containingClass?.declarations?.filterIsInstance() ?.singleOrNull()?.valueParameters?.find { it.name == this.name } if (matchingPrimaryConstructorParam != null) { @@ -122,10 +124,14 @@ internal fun serializablePropertiesForIrBackend( .map { val isConstructorParameterWithDefault = primaryParamsAsProps[it] ?: false val (isPropertyFromAnotherModuleDeclaresDefaultValue, isPropertyWithBackingFieldFromAnotherModule) = it.analyzeIfFromAnotherModule() + val hasBackingField = when (it.origin) { + IrDeclarationOrigin.IR_EXTERNAL_DECLARATION_STUB -> isPropertyWithBackingFieldFromAnotherModule + else -> it.backingField != null + } IrSerializableProperty( it, isConstructorParameterWithDefault, - it.backingField != null || isPropertyWithBackingFieldFromAnotherModule, + hasBackingField, it.backingField?.initializer.let { init -> init != null && !init.expression.isInitializePropertyFromParameter() } || isConstructorParameterWithDefault || isPropertyFromAnotherModuleDeclaresDefaultValue, typeReplacement?.get(it) ?: it.getter!!.returnType as IrSimpleType diff --git a/plugins/kotlinx-serialization/kotlinx-serialization.k2/build.gradle.kts b/plugins/kotlinx-serialization/kotlinx-serialization.k2/build.gradle.kts index 1fd960b06d1..fd614ada4d7 100644 --- a/plugins/kotlinx-serialization/kotlinx-serialization.k2/build.gradle.kts +++ b/plugins/kotlinx-serialization/kotlinx-serialization.k2/build.gradle.kts @@ -8,6 +8,7 @@ plugins { dependencies { compileOnly(project(":compiler:fir:cones")) compileOnly(project(":compiler:fir:tree")) + compileOnly(project(":compiler:fir:fir-serialization")) compileOnly(project(":compiler:fir:resolve")) compileOnly(project(":compiler:fir:plugin-utils")) compileOnly(project(":compiler:fir:entrypoint")) diff --git a/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/FirSerializableProperty.kt b/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/FirSerializableProperty.kt index 073fbf152bb..bf2a1b78144 100644 --- a/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/FirSerializableProperty.kt +++ b/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/FirSerializableProperty.kt @@ -6,7 +6,9 @@ package org.jetbrains.kotlinx.serialization.compiler.fir import org.jetbrains.kotlin.fir.FirSession +import org.jetbrains.kotlin.fir.declarations.FirDeclarationOrigin import org.jetbrains.kotlin.fir.declarations.utils.hasBackingField +import org.jetbrains.kotlin.fir.deserialization.registeredInSerializationPluginMetadataExtension import org.jetbrains.kotlin.fir.resolve.defaultType import org.jetbrains.kotlin.fir.symbols.impl.FirPropertySymbol import org.jetbrains.kotlin.fir.types.ConeKotlinType @@ -28,7 +30,14 @@ class FirSerializableProperty( override val optional: Boolean = !propertySymbol.getSerialRequired(session) && declaresDefaultValue - override val transient: Boolean = propertySymbol.hasSerialTransient(session) || !propertySymbol.hasBackingField + override val transient: Boolean = run { + if (propertySymbol.hasSerialTransient(session)) return@run true + val hasBackingField = when (propertySymbol.origin) { + FirDeclarationOrigin.Library -> propertySymbol.registeredInSerializationPluginMetadataExtension + else -> propertySymbol.hasBackingField + } + !hasBackingField + } val serializableWith: ConeKotlinType? = propertySymbol.getSerializableWith(session) ?: analyzeSpecialSerializers(session, propertySymbol.resolvedAnnotationsWithArguments)?.defaultType() diff --git a/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/FirSerializationExtensionRegistrar.kt b/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/FirSerializationExtensionRegistrar.kt index fcfae34fb18..41a996c2d7f 100644 --- a/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/FirSerializationExtensionRegistrar.kt +++ b/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/FirSerializationExtensionRegistrar.kt @@ -5,6 +5,7 @@ package org.jetbrains.kotlinx.serialization.compiler.fir +import org.jetbrains.kotlin.fir.extensions.FirExtensionApiInternals import org.jetbrains.kotlin.fir.extensions.FirExtensionRegistrar import org.jetbrains.kotlinx.serialization.compiler.fir.checkers.FirSerializationCheckersComponent import org.jetbrains.kotlinx.serialization.compiler.fir.services.ContextualSerializersProvider @@ -17,6 +18,8 @@ class FirSerializationExtensionRegistrar : FirExtensionRegistrar() { +::SerializationFirResolveExtension +::SerializationFirSupertypesExtension +::FirSerializationCheckersComponent + @OptIn(FirExtensionApiInternals::class) + +::FirSerializationMetadataSerializerPlugin // services +::DependencySerializationInfoProvider diff --git a/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/FirSerializationMetadataSerializerPlugin.kt b/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/FirSerializationMetadataSerializerPlugin.kt new file mode 100644 index 00000000000..33b5125b4b4 --- /dev/null +++ b/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/FirSerializationMetadataSerializerPlugin.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlinx.serialization.compiler.fir + +import org.jetbrains.kotlin.descriptors.Modality +import org.jetbrains.kotlin.fir.FirSession +import org.jetbrains.kotlin.fir.declarations.utils.modality +import org.jetbrains.kotlin.fir.extensions.FirExtensionApiInternals +import org.jetbrains.kotlin.fir.serialization.FirElementAwareStringTable +import org.jetbrains.kotlin.fir.serialization.FirMetadataSerializerPlugin +import org.jetbrains.kotlin.fir.symbols.impl.FirRegularClassSymbol +import org.jetbrains.kotlin.metadata.SerializationPluginMetadataExtensions +import org.jetbrains.kotlinx.serialization.compiler.fir.services.serializablePropertiesProvider + +@OptIn(FirExtensionApiInternals::class) +class FirSerializationMetadataSerializerPlugin(session: FirSession) : FirMetadataSerializerPlugin(session) { + override fun registerProtoExtensions( + symbol: FirRegularClassSymbol, + stringTable: FirElementAwareStringTable, + protoRegistrar: ProtoRegistrar, + ) { + if (!symbol.needSaveProgramOrder) return + val properties = session.serializablePropertiesProvider.getSerializablePropertiesForClass(symbol) + val propertyNames = properties.serializableProperties.map { stringTable.getStringIndex(it.propertySymbol.name.asString()) } + protoRegistrar.setExtension(SerializationPluginMetadataExtensions.propertiesNamesInProgramOrder, propertyNames) + } + + private val FirRegularClassSymbol.needSaveProgramOrder: Boolean + get() = (modality == Modality.OPEN || modality == Modality.ABSTRACT) && with(session) { isInternalSerializable } +} diff --git a/plugins/kotlinx-serialization/testData/boxIr/clashBetweenSerializableAndNonSerializableProperty.kt b/plugins/kotlinx-serialization/testData/boxIr/clashBetweenSerializableAndNonSerializableProperty.kt new file mode 100644 index 00000000000..4d14c87febe --- /dev/null +++ b/plugins/kotlinx-serialization/testData/boxIr/clashBetweenSerializableAndNonSerializableProperty.kt @@ -0,0 +1,34 @@ +// ISSUE: KT-64312 +// WITH_STDLIB +// MODULE: lib +import kotlinx.serialization.* +import kotlinx.serialization.json.* + +@Serializable +abstract class Base { + @SerialName("properties") + protected var propertiesInternal: String? = null + + val properties: String // should not be serialized because it does not have a backing field + get() = propertiesInternal?.reversed().orEmpty() +} + +// MODULE: main(lib) +import kotlinx.serialization.* +import kotlinx.serialization.json.* + +@Serializable +class Derived: Base() { + fun initialize() { + propertiesInternal = "KO" + } +} + +fun box(): String { + val expected = Derived().also { it.initialize() } + val json = Json.encodeToString(Derived.serializer(), expected) + if (json != """{"properties":"KO"}""") return "Fail: $json" + val actual = Json.decodeFromString(Derived.serializer(), json) + if (expected.properties != actual.properties) return "Fail: $actual" + return actual.properties +} diff --git a/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationFirJsBoxTestGenerated.java b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationFirJsBoxTestGenerated.java index d2416c20ec9..ca3989e0fe9 100644 --- a/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationFirJsBoxTestGenerated.java +++ b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationFirJsBoxTestGenerated.java @@ -25,6 +25,12 @@ public class SerializationFirJsBoxTestGenerated extends AbstractSerializationFir KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("plugins/kotlinx-serialization/testData/boxIr"), Pattern.compile("^(.+)\\.kt$"), null, TargetBackend.JS_IR, true); } + @Test + @TestMetadata("clashBetweenSerializableAndNonSerializableProperty.kt") + public void testClashBetweenSerializableAndNonSerializableProperty() throws Exception { + runTest("plugins/kotlinx-serialization/testData/boxIr/clashBetweenSerializableAndNonSerializableProperty.kt"); + } + @Test @TestMetadata("constValInSerialName.kt") public void testConstValInSerialName() throws Exception { diff --git a/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationFirLightTreeBlackBoxTestGenerated.java b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationFirLightTreeBlackBoxTestGenerated.java index 52365eca79f..57a785bb170 100644 --- a/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationFirLightTreeBlackBoxTestGenerated.java +++ b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationFirLightTreeBlackBoxTestGenerated.java @@ -45,6 +45,12 @@ public class SerializationFirLightTreeBlackBoxTestGenerated extends AbstractSeri runTest("plugins/kotlinx-serialization/testData/boxIr/caching.kt"); } + @Test + @TestMetadata("clashBetweenSerializableAndNonSerializableProperty.kt") + public void testClashBetweenSerializableAndNonSerializableProperty() throws Exception { + runTest("plugins/kotlinx-serialization/testData/boxIr/clashBetweenSerializableAndNonSerializableProperty.kt"); + } + @Test @TestMetadata("classSerializerAsObject.kt") public void testClassSerializerAsObject() throws Exception { diff --git a/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationIrBoxTestGenerated.java b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationIrBoxTestGenerated.java index 17857bf039e..13771a763e2 100644 --- a/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationIrBoxTestGenerated.java +++ b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationIrBoxTestGenerated.java @@ -43,6 +43,12 @@ public class SerializationIrBoxTestGenerated extends AbstractSerializationIrBoxT runTest("plugins/kotlinx-serialization/testData/boxIr/caching.kt"); } + @Test + @TestMetadata("clashBetweenSerializableAndNonSerializableProperty.kt") + public void testClashBetweenSerializableAndNonSerializableProperty() throws Exception { + runTest("plugins/kotlinx-serialization/testData/boxIr/clashBetweenSerializableAndNonSerializableProperty.kt"); + } + @Test @TestMetadata("classSerializerAsObject.kt") public void testClassSerializerAsObject() throws Exception { diff --git a/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationIrJsBoxTestGenerated.java b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationIrJsBoxTestGenerated.java index 6057c3ad7ec..912a30ede40 100644 --- a/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationIrJsBoxTestGenerated.java +++ b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationIrJsBoxTestGenerated.java @@ -25,6 +25,12 @@ public class SerializationIrJsBoxTestGenerated extends AbstractSerializationIrJs KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("plugins/kotlinx-serialization/testData/boxIr"), Pattern.compile("^(.+)\\.kt$"), null, TargetBackend.JS_IR, true); } + @Test + @TestMetadata("clashBetweenSerializableAndNonSerializableProperty.kt") + public void testClashBetweenSerializableAndNonSerializableProperty() throws Exception { + runTest("plugins/kotlinx-serialization/testData/boxIr/clashBetweenSerializableAndNonSerializableProperty.kt"); + } + @Test @TestMetadata("constValInSerialName.kt") public void testConstValInSerialName() throws Exception {