diff --git a/compiler/fir/fir2ir/src/org/jetbrains/kotlin/fir/lazy/Fir2IrLazyProperty.kt b/compiler/fir/fir2ir/src/org/jetbrains/kotlin/fir/lazy/Fir2IrLazyProperty.kt index b91a9cbe43e..ef2680656dd 100644 --- a/compiler/fir/fir2ir/src/org/jetbrains/kotlin/fir/lazy/Fir2IrLazyProperty.kt +++ b/compiler/fir/fir2ir/src/org/jetbrains/kotlin/fir/lazy/Fir2IrLazyProperty.kt @@ -1,5 +1,5 @@ /* - * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Copyright 2010-2023 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. */ @@ -37,7 +37,7 @@ class Fir2IrLazyProperty( override val endOffset: Int, override var origin: IrDeclarationOrigin, override val fir: FirProperty, - internal val containingClass: FirRegularClass?, + val containingClass: FirRegularClass?, override val symbol: Fir2IrPropertySymbol, override val isFakeOverride: Boolean ) : IrProperty(), AbstractFir2IrLazyDeclaration, Fir2IrComponents by components { diff --git a/plugins/kotlinx-serialization/kotlinx-serialization.backend/build.gradle.kts b/plugins/kotlinx-serialization/kotlinx-serialization.backend/build.gradle.kts index 601af3bd6bd..02fbdf77fce 100644 --- a/plugins/kotlinx-serialization/kotlinx-serialization.backend/build.gradle.kts +++ b/plugins/kotlinx-serialization/kotlinx-serialization.backend/build.gradle.kts @@ -12,6 +12,8 @@ dependencies { compileOnly(project(":compiler:backend.jvm.codegen")) compileOnly(project(":compiler:backend.jvm.lower")) compileOnly(project(":compiler:ir.tree")) + compileOnly(project(":compiler:fir:fir2ir")) + compileOnly(project(":compiler:fir:tree")) 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 92a5f11fd91..b517cf90420 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 @@ -6,6 +6,9 @@ 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.lazy.Fir2IrLazyProperty import org.jetbrains.kotlin.ir.ObsoleteDescriptorBasedAPI import org.jetbrains.kotlin.ir.declarations.IrClass import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin @@ -41,6 +44,41 @@ class IrSerializableProperties( override val serializableStandaloneProperties: List ) : ISerializableProperties +/** + * This function checks if a deserialized property declares default value and has backing field. + * + * Returns (declaresDefaultValue, hasBackingField) boolean pair. Returns (false, false) for properties from current module. + */ +@OptIn(ObsoleteDescriptorBasedAPI::class) +fun IrProperty.analyzeIfFromAnotherModule(): Pair { + return if (descriptor is DeserializedPropertyDescriptor) { + // IrLazyProperty does not deserialize backing fields correctly, so we should fall back to info from descriptor. + // DeserializedPropertyDescriptor can be encountered only after K1, so it is safe to check it. + 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, + // 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 matchingPrimaryConstructorParam = containingClass?.declarations?.filterIsInstance() + ?.singleOrNull()?.valueParameters?.find { it.name == this.name } + if (matchingPrimaryConstructorParam != null) { + // If property is a constructor parameter, check parameter default value + // (serializable classes always have parameters-as-properties, so no name clash here) + (matchingPrimaryConstructorParam.defaultValue != null) to hasBackingField + } else { + // If it is a body property, then it is likely to have initializer when getter is not specified + // note this approach is not working well if we have smth like `get() = field`, but such cases on cross-module boundaries + // should be very marginal. If we want to solve them, we need to add protobuf metadata extension. + (fir.getter is FirDefaultPropertyGetter) to hasBackingField + } + } else { + false to false + } +} + /** * typeReplacement should be populated from FakeOverrides and is used when we want to determine the type for property * accounting for generic substitutions performed in subclasses: @@ -83,11 +121,7 @@ internal fun serializablePropertiesForIrBackend( .filter(::isPropSerializable) .map { val isConstructorParameterWithDefault = primaryParamsAsProps[it] ?: false - // FIXME: workaround because IrLazyProperty doesn't deserialize information about backing fields. Fallback to descriptor won't work with FIR. - val isPropertyFromAnotherModuleDeclaresDefaultValue = - it.descriptor is DeserializedPropertyDescriptor && it.descriptor.declaresDefaultValue() - val isPropertyWithBackingFieldFromAnotherModule = - it.descriptor is DeserializedPropertyDescriptor && (it.descriptor.backingField != null || isPropertyFromAnotherModuleDeclaresDefaultValue) + val (isPropertyFromAnotherModuleDeclaresDefaultValue, isPropertyWithBackingFieldFromAnotherModule) = it.analyzeIfFromAnotherModule() IrSerializableProperty( it, isConstructorParameterWithDefault, diff --git a/plugins/kotlinx-serialization/kotlinx-serialization.backend/src/org/jetbrains/kotlinx/serialization/compiler/backend/ir/SerializableIrGenerator.kt b/plugins/kotlinx-serialization/kotlinx-serialization.backend/src/org/jetbrains/kotlinx/serialization/compiler/backend/ir/SerializableIrGenerator.kt index abc798f3473..0963fe545b2 100644 --- a/plugins/kotlinx-serialization/kotlinx-serialization.backend/src/org/jetbrains/kotlinx/serialization/compiler/backend/ir/SerializableIrGenerator.kt +++ b/plugins/kotlinx-serialization/kotlinx-serialization.backend/src/org/jetbrains/kotlinx/serialization/compiler/backend/ir/SerializableIrGenerator.kt @@ -1,5 +1,5 @@ /* - * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Copyright 2010-2023 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. */ @@ -39,10 +39,7 @@ class SerializableIrGenerator( protected val properties = serializablePropertiesForIrBackend(irClass) private val serialDescriptorClass = compilerContext.referenceClass( - ClassId( - SerializationPackages.descriptorsPackageFqName, - Name.identifier(SerialEntityNames.SERIAL_DESCRIPTOR_CLASS) - ) + SerializationRuntimeClassIds.descriptorClassId )!!.owner private val serialDescriptorImplClass = compilerContext.referenceClass( diff --git a/plugins/kotlinx-serialization/kotlinx-serialization.common/src/org/jetbrains/kotlinx/serialization/compiler/resolve/NamingConventions.kt b/plugins/kotlinx-serialization/kotlinx-serialization.common/src/org/jetbrains/kotlinx/serialization/compiler/resolve/NamingConventions.kt index 7c9d2aefd94..b9288f528a3 100644 --- a/plugins/kotlinx-serialization/kotlinx-serialization.common/src/org/jetbrains/kotlinx/serialization/compiler/resolve/NamingConventions.kt +++ b/plugins/kotlinx-serialization/kotlinx-serialization.common/src/org/jetbrains/kotlinx/serialization/compiler/resolve/NamingConventions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Copyright 2010-2023 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. */ @@ -30,6 +30,7 @@ object SerializationAnnotations { val serialNameAnnotationFqName = FqName("kotlinx.serialization.SerialName") val requiredAnnotationFqName = FqName("kotlinx.serialization.Required") val serialTransientFqName = FqName("kotlinx.serialization.Transient") + // Also implicitly used in kotlin-native.compiler.backend.native/CodeGenerationInfo.kt val serialInfoFqName = FqName("kotlinx.serialization.SerialInfo") val inheritableSerialInfoFqName = FqName("kotlinx.serialization.InheritableSerialInfo") @@ -200,7 +201,8 @@ object SerializersClassIds { val kSerializerId = ClassId(SerializationPackages.packageFqName, SerialEntityNames.KSERIALIZER_NAME) val enumSerializerId = ClassId(SerializationPackages.internalPackageFqName, Name.identifier(SpecialBuiltins.enumSerializer)) val polymorphicSerializerId = ClassId(SerializationPackages.packageFqName, Name.identifier(SpecialBuiltins.polymorphicSerializer)) - val referenceArraySerializerId = ClassId(SerializationPackages.internalPackageFqName, Name.identifier(SpecialBuiltins.referenceArraySerializer)) + val referenceArraySerializerId = + ClassId(SerializationPackages.internalPackageFqName, Name.identifier(SpecialBuiltins.referenceArraySerializer)) val objectSerializerId = ClassId(SerializationPackages.internalPackageFqName, Name.identifier(SpecialBuiltins.objectSerializer)) val sealedSerializerId = ClassId(SerializationPackages.packageFqName, Name.identifier(SpecialBuiltins.sealedSerializer)) val contextSerializerId = ClassId(SerializationPackages.packageFqName, Name.identifier(SpecialBuiltins.contextSerializer)) @@ -209,6 +211,14 @@ object SerializersClassIds { val setOfSpecialSerializers = setOf(contextSerializerId, polymorphicSerializerId) } +object SerializationRuntimeClassIds { + + val descriptorClassId = + ClassId(SerializationPackages.descriptorsPackageFqName, Name.identifier(SerialEntityNames.SERIAL_DESCRIPTOR_CLASS)) + val compositeEncoderClassId = + ClassId(SerializationPackages.encodingPackageFqName, Name.identifier(SerialEntityNames.STRUCTURE_ENCODER_CLASS)) +} + fun findStandardKotlinTypeSerializerName(typeName: String?): String? { return when (typeName) { null -> null 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 ed2fd402a8b..36719bec7f1 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 @@ -1,5 +1,5 @@ /* - * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Copyright 2010-2023 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. */ @@ -17,6 +17,7 @@ class FirSerializationExtensionRegistrar : FirExtensionRegistrar() { +::SerializationFirResolveExtension +::SerializationFirSupertypesExtension +::FirSerializationCheckersComponent + +::SerializationFirDeclarationsForMetadataProvider // services +::DependencySerializationInfoProvider diff --git a/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/SerializationFirDeclarationsForMetadataProvider.kt b/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/SerializationFirDeclarationsForMetadataProvider.kt new file mode 100644 index 00000000000..24a41917855 --- /dev/null +++ b/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/SerializationFirDeclarationsForMetadataProvider.kt @@ -0,0 +1,118 @@ +/* + * Copyright 2010-2023 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.fir.FirSession +import org.jetbrains.kotlin.fir.declarations.FirClass +import org.jetbrains.kotlin.fir.declarations.FirDeclaration +import org.jetbrains.kotlin.fir.declarations.utils.isInline +import org.jetbrains.kotlin.fir.expressions.FirAnnotation +import org.jetbrains.kotlin.fir.expressions.builder.buildAnnotationCall +import org.jetbrains.kotlin.fir.extensions.FirDeclarationsForMetadataProviderExtension +import org.jetbrains.kotlin.fir.moduleData +import org.jetbrains.kotlin.fir.plugin.createConstructor +import org.jetbrains.kotlin.fir.plugin.createMemberFunction +import org.jetbrains.kotlin.fir.references.builder.buildResolvedNamedReference +import org.jetbrains.kotlin.fir.resolve.ScopeSession +import org.jetbrains.kotlin.fir.resolve.defaultType +import org.jetbrains.kotlin.fir.resolve.providers.symbolProvider +import org.jetbrains.kotlin.fir.scopes.impl.toConeType +import org.jetbrains.kotlin.fir.symbols.impl.FirConstructorSymbol +import org.jetbrains.kotlin.fir.symbols.impl.FirRegularClassSymbol +import org.jetbrains.kotlin.fir.types.* +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.name.StandardClassIds +import org.jetbrains.kotlin.platform.jvm.isJvm +import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull +import org.jetbrains.kotlinx.serialization.compiler.fir.services.serializablePropertiesProvider +import org.jetbrains.kotlinx.serialization.compiler.resolve.* + +class SerializationFirDeclarationsForMetadataProvider(session: FirSession) : FirDeclarationsForMetadataProviderExtension(session) { + override fun provideDeclarationsForClass(klass: FirClass, scopeSession: ScopeSession): List { + // FIXME: multi-field value classes require additional 'if' here, but they're not supported for now in serialization K2. + return if (klass.symbol.isInline || with(session) { !klass.symbol.isInternalSerializable }) emptyList() + // As we deprecated 'customize serializer via companion', we know for sure + // that serialize/deserialize functions are synthetic and generated by our plugin. + else listOfNotNull(generateDeserializationConstructor(klass), generateWriteSelf(klass)) + } + + private fun generateDeserializationConstructor(klass: FirClass): FirDeclaration = + createConstructor(klass.symbol, SerializationPluginKey, isPrimary = false) { + val serializableProperties = + session.serializablePropertiesProvider.getSerializablePropertiesForClass(klass.symbol).serializableProperties + val bitMaskSlotCount = serializableProperties.bitMaskSlotCount() + repeat(bitMaskSlotCount) { + valueParameter(Name.identifier("seen$it"), session.builtinTypes.intType.type) + } + serializableProperties.forEach { prop -> + valueParameter( + prop.originalDescriptorName, + prop.propertySymbol.resolvedReturnType.makeNullableIfNotPrimitive(session.typeContext) + ) + } + val markerType = + ClassId(SerializationPackages.internalPackageFqName, SerialEntityNames.SERIAL_CTOR_MARKER_NAME).constructClassLikeType( + emptyArray(), + isNullable = true + ) + valueParameter(SerialEntityNames.dummyParamName, markerType) + } + + private fun generateWriteSelf(klass: FirClass): FirDeclaration? { + // write$Self in K1 is created only on JVM (see SerializationResolveExtension) + if (!session.moduleData.platform.isJvm()) return null + return createMemberFunction( + klass.symbol, + SerializationPluginKey, + SerialEntityNames.WRITE_SELF_NAME, + session.builtinTypes.unitType.type + ) { + klass.typeParameters.forEach { + typeParameter(it.symbol.name) + } + + valueParameter(Name.identifier("self"), { functionTypeParams -> + klass.symbol.constructType(functionTypeParams.map { it.toConeType() }.toTypedArray(), false) + }) + + valueParameter( + Name.identifier("output"), + SerializationRuntimeClassIds.compositeEncoderClassId.constructClassLikeType(emptyArray(), false) + ) + + valueParameter( + Name.identifier("serialDesc"), + SerializationRuntimeClassIds.descriptorClassId.constructClassLikeType(emptyArray(), false) + ) + + klass.typeParameters.forEachIndexed { i, _ -> + valueParameter(Name.identifier("${SerialEntityNames.typeArgPrefix}$i"), { functionTps -> + SerializersClassIds.kSerializerId.constructClassLikeType(arrayOf(functionTps[i].toConeType()), false) + }) + } + }.apply { replaceAnnotations(listOfNotNull(createJvmStaticAnnotation())) } + } + + private fun createJvmStaticAnnotation(): FirAnnotation? { + val jvmStatic = + session.symbolProvider.getClassLikeSymbolByClassId(StandardClassIds.Annotations.JvmStatic) as? FirRegularClassSymbol + ?: return null + val jvmStaticCtor = + jvmStatic.declarationSymbols.firstIsInstanceOrNull() ?: return null + + return buildAnnotationCall { + annotationTypeRef = jvmStatic.defaultType().toFirResolvedTypeRef() + calleeReference = buildResolvedNamedReference { + name = jvmStatic.name + resolvedSymbol = jvmStaticCtor + } + } + } +} + +internal fun ConeKotlinType.makeNullableIfNotPrimitive(typeContext: ConeTypeContext): ConeKotlinType = + if (isPrimitive) this else withNullability(ConeNullability.NULLABLE, typeContext) diff --git a/plugins/kotlinx-serialization/testData/boxIr/multimoduleInheritance.kt b/plugins/kotlinx-serialization/testData/boxIr/multimoduleInheritance.kt index 7aa87ff4646..783d3e2cd25 100644 --- a/plugins/kotlinx-serialization/testData/boxIr/multimoduleInheritance.kt +++ b/plugins/kotlinx-serialization/testData/boxIr/multimoduleInheritance.kt @@ -1,4 +1,3 @@ -// IGNORE_BACKEND_K2: JVM_IR // TARGET_BACKEND: JVM_IR // WITH_STDLIB @@ -18,6 +17,15 @@ open class OpenBody { @Serializable abstract class AbstractConstructor(var optional: String = "foo") +// TODO: test fails for K2 if places of 'color' and 'name' are swapped +// because of https://youtrack.jetbrains.com/issue/KT-54792 (KT-20980) +// and serialization proto extension is not available in K2. +@Serializable +open class Vehicle { + var color: String? = null + var name: String? = null +} + // MODULE: app(lib) // FILE: app.kt @@ -35,6 +43,11 @@ class Test1: OpenBody() @Serializable class Test2: AbstractConstructor() +@Serializable +open class Car : Vehicle() { + var maxSpeed: Int = 100 +} + fun test1() { val string = Json.encodeToString(Test1.serializer(), Test1()) assertEquals("{}", string) @@ -49,8 +62,29 @@ fun test2() { assertEquals("foo", reconstructed.optional) } +fun test3() { + val json = Json { allowStructuredMapKeys = true; encodeDefaults = true } + + val car = Car() + car.maxSpeed = 100 + car.name = "ford" + val s = json.encodeToString(Car.serializer(), car) + assertEquals("""{"color":null,"name":"ford","maxSpeed":100}""", s) + val restoredCar = json.decodeFromString(Car.serializer(), s) + assertEquals(100, restoredCar.maxSpeed) + assertEquals("ford", restoredCar.name) + assertEquals(null, restoredCar.color) +} + fun box(): String { - test1() - test2() - return "OK" + try { + test1() + test2() + test3() + return "OK" + } catch (e: Throwable) { + e.printStackTrace() + return e.message!! + } + } \ No newline at end of file