Add declarations from serialization plugin to FIR metadata declarations provider.
These declarations should not be visible to users (and therefore are not added to FIR), but plugin itself can reference them in already compiled serializable classes, and therefore they should be available in metadata: - synthetic deserialization constructor - static write$Self function See also: ^KT-55885
This commit is contained in:
committed by
Space Team
parent
8953b25c5b
commit
c564dd973b
@@ -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<FirProperty>, Fir2IrComponents by components {
|
||||
|
||||
@@ -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"))
|
||||
|
||||
+39
-5
@@ -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<IrSerializableProperty>
|
||||
) : ISerializableProperties<IrSerializableProperty>
|
||||
|
||||
/**
|
||||
* 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<Boolean, Boolean> {
|
||||
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<FirPrimaryConstructor>()
|
||||
?.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,
|
||||
|
||||
+2
-5
@@ -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(
|
||||
|
||||
+12
-2
@@ -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
|
||||
|
||||
+2
-1
@@ -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
|
||||
|
||||
+118
@@ -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<FirDeclaration> {
|
||||
// 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<FirConstructorSymbol>() ?: 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)
|
||||
@@ -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!!
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user