Correctly determine the type of serializable property
when supertype of serializable class is generic and also serializable, and contains the property with type with its generic parameter. Fixes https://github.com/Kotlin/kotlinx.serialization/issues/1264 #KT-43910 Fixed #KT-49660 Fixed
This commit is contained in:
committed by
Space Team
parent
2ea0cdf46d
commit
2a626b27d3
+43
-16
@@ -10,27 +10,25 @@ import org.jetbrains.kotlin.ir.ObsoleteDescriptorBasedAPI
|
|||||||
import org.jetbrains.kotlin.ir.declarations.IrClass
|
import org.jetbrains.kotlin.ir.declarations.IrClass
|
||||||
import org.jetbrains.kotlin.ir.declarations.IrProperty
|
import org.jetbrains.kotlin.ir.declarations.IrProperty
|
||||||
import org.jetbrains.kotlin.ir.types.IrSimpleType
|
import org.jetbrains.kotlin.ir.types.IrSimpleType
|
||||||
import org.jetbrains.kotlin.ir.util.hasAnnotation
|
import org.jetbrains.kotlin.ir.util.*
|
||||||
import org.jetbrains.kotlin.ir.util.hasDefaultValue
|
|
||||||
import org.jetbrains.kotlin.ir.util.primaryConstructor
|
|
||||||
import org.jetbrains.kotlin.ir.util.properties
|
|
||||||
import org.jetbrains.kotlin.name.Name
|
import org.jetbrains.kotlin.name.Name
|
||||||
import org.jetbrains.kotlin.serialization.deserialization.descriptors.DeserializedPropertyDescriptor
|
import org.jetbrains.kotlin.serialization.deserialization.descriptors.DeserializedPropertyDescriptor
|
||||||
import org.jetbrains.kotlinx.serialization.compiler.extensions.SerializationDescriptorSerializerPlugin
|
import org.jetbrains.kotlinx.serialization.compiler.extensions.SerializationDescriptorSerializerPlugin
|
||||||
import org.jetbrains.kotlinx.serialization.compiler.extensions.SerializationPluginContext
|
|
||||||
import org.jetbrains.kotlinx.serialization.compiler.resolve.*
|
import org.jetbrains.kotlinx.serialization.compiler.resolve.*
|
||||||
|
|
||||||
class IrSerializableProperty(
|
class IrSerializableProperty(
|
||||||
val ir: IrProperty,
|
val ir: IrProperty,
|
||||||
override val isConstructorParameterWithDefault: Boolean,
|
override val isConstructorParameterWithDefault: Boolean,
|
||||||
hasBackingField: Boolean,
|
hasBackingField: Boolean,
|
||||||
declaresDefaultValue: Boolean
|
declaresDefaultValue: Boolean,
|
||||||
|
val type: IrSimpleType
|
||||||
) : ISerializableProperty {
|
) : ISerializableProperty {
|
||||||
override val name = ir.annotations.serialNameValue ?: ir.name.asString()
|
override val name = ir.annotations.serialNameValue ?: ir.name.asString()
|
||||||
override val originalDescriptorName: Name = ir.name
|
override val originalDescriptorName: Name = ir.name
|
||||||
val type = ir.getter!!.returnType as IrSimpleType
|
|
||||||
val genericIndex = type.genericIndex
|
val genericIndex = type.genericIndex
|
||||||
fun serializableWith(ctx: SerializationBaseContext) = ir.annotations.serializableWith() ?: analyzeSpecialSerializers(ctx, ir.annotations)
|
fun serializableWith(ctx: SerializationBaseContext) =
|
||||||
|
ir.annotations.serializableWith() ?: analyzeSpecialSerializers(ctx, ir.annotations)
|
||||||
|
|
||||||
override val optional = !ir.annotations.hasAnnotation(SerializationAnnotations.requiredAnnotationFqName) && declaresDefaultValue
|
override val optional = !ir.annotations.hasAnnotation(SerializationAnnotations.requiredAnnotationFqName) && declaresDefaultValue
|
||||||
override val transient = ir.annotations.hasAnnotation(SerializationAnnotations.serialTransientFqName) || !hasBackingField
|
override val transient = ir.annotations.hasAnnotation(SerializationAnnotations.serialTransientFqName) || !hasBackingField
|
||||||
}
|
}
|
||||||
@@ -42,10 +40,27 @@ class IrSerializableProperties(
|
|||||||
override val serializableStandaloneProperties: List<IrSerializableProperty>
|
override val serializableStandaloneProperties: List<IrSerializableProperty>
|
||||||
) : ISerializableProperties<IrSerializableProperty>
|
) : ISerializableProperties<IrSerializableProperty>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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:
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* @Serializable
|
||||||
|
* sealed class TypedSealedClass<T>(val a: T) {
|
||||||
|
* @Serializable
|
||||||
|
* data class Child(val y: Int) : TypedSealedClass<String>("10")
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
* In this case, serializableProperties for TypedSealedClass is a listOf(IrSerProp(val a: T)),
|
||||||
|
* but for Child is a listOf(IrSerProp(val a: String), IrSerProp(val y: Int)).
|
||||||
|
*
|
||||||
|
* Using this approach, we can correctly deserialize parent's properties in Child.Companion.deserialize()
|
||||||
|
*/
|
||||||
@OptIn(ObsoleteDescriptorBasedAPI::class)
|
@OptIn(ObsoleteDescriptorBasedAPI::class)
|
||||||
internal fun serializablePropertiesForIrBackend(
|
internal fun serializablePropertiesForIrBackend(
|
||||||
irClass: IrClass,
|
irClass: IrClass,
|
||||||
serializationDescriptorSerializer: SerializationDescriptorSerializerPlugin? = null
|
serializationDescriptorSerializer: SerializationDescriptorSerializerPlugin? = null,
|
||||||
|
typeReplacement: Map<IrProperty, IrSimpleType>? = null
|
||||||
): IrSerializableProperties {
|
): IrSerializableProperties {
|
||||||
val properties = irClass.properties.toList()
|
val properties = irClass.properties.toList()
|
||||||
val primaryConstructorParams = irClass.primaryConstructor?.valueParameters.orEmpty()
|
val primaryConstructorParams = irClass.primaryConstructor?.valueParameters.orEmpty()
|
||||||
@@ -68,14 +83,17 @@ internal fun serializablePropertiesForIrBackend(
|
|||||||
.map {
|
.map {
|
||||||
val isConstructorParameterWithDefault = primaryParamsAsProps[it] ?: false
|
val isConstructorParameterWithDefault = primaryParamsAsProps[it] ?: false
|
||||||
// FIXME: workaround because IrLazyProperty doesn't deserialize information about backing fields. Fallback to descriptor won't work with FIR.
|
// 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 isPropertyFromAnotherModuleDeclaresDefaultValue =
|
||||||
val isPropertyWithBackingFieldFromAnotherModule = it.descriptor is DeserializedPropertyDescriptor && (it.descriptor.backingField != null || isPropertyFromAnotherModuleDeclaresDefaultValue)
|
it.descriptor is DeserializedPropertyDescriptor && it.descriptor.declaresDefaultValue()
|
||||||
|
val isPropertyWithBackingFieldFromAnotherModule =
|
||||||
|
it.descriptor is DeserializedPropertyDescriptor && (it.descriptor.backingField != null || isPropertyFromAnotherModuleDeclaresDefaultValue)
|
||||||
IrSerializableProperty(
|
IrSerializableProperty(
|
||||||
it,
|
it,
|
||||||
isConstructorParameterWithDefault,
|
isConstructorParameterWithDefault,
|
||||||
it.backingField != null || isPropertyWithBackingFieldFromAnotherModule,
|
it.backingField != null || isPropertyWithBackingFieldFromAnotherModule,
|
||||||
it.backingField?.initializer.let { init -> init != null && !init.expression.isInitializePropertyFromParameter() } || isConstructorParameterWithDefault
|
it.backingField?.initializer.let { init -> init != null && !init.expression.isInitializePropertyFromParameter() } || isConstructorParameterWithDefault
|
||||||
|| isPropertyFromAnotherModuleDeclaresDefaultValue
|
|| isPropertyFromAnotherModuleDeclaresDefaultValue,
|
||||||
|
typeReplacement?.get(it) ?: it.getter!!.returnType as IrSimpleType
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.filterNot { it.transient }
|
.filterNot { it.transient }
|
||||||
@@ -83,13 +101,22 @@ internal fun serializablePropertiesForIrBackend(
|
|||||||
|
|
||||||
var serializableProps = run {
|
var serializableProps = run {
|
||||||
val supers = irClass.getSuperClassNotAny()
|
val supers = irClass.getSuperClassNotAny()
|
||||||
if (supers == null || !supers.isInternalSerializable)
|
if (supers == null || !supers.isInternalSerializable) {
|
||||||
primaryCtorSerializableProps + bodySerializableProps
|
primaryCtorSerializableProps + bodySerializableProps
|
||||||
else
|
} else {
|
||||||
|
val originalToTypeFromFO = typeReplacement ?: buildMap<IrProperty, IrSimpleType> {
|
||||||
|
irClass.properties.filter { it.isFakeOverride }.forEach { prop ->
|
||||||
|
val orig = prop.resolveFakeOverride()
|
||||||
|
val type = prop.getter?.returnType as? IrSimpleType
|
||||||
|
if (orig != null && type != null) put(orig, type)
|
||||||
|
}
|
||||||
|
}
|
||||||
serializablePropertiesForIrBackend(
|
serializablePropertiesForIrBackend(
|
||||||
supers,
|
supers,
|
||||||
serializationDescriptorSerializer
|
serializationDescriptorSerializer,
|
||||||
|
originalToTypeFromFO
|
||||||
).serializableProperties + primaryCtorSerializableProps + bodySerializableProps
|
).serializableProperties + primaryCtorSerializableProps + bodySerializableProps
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: since descriptor from FIR does not have classProto in it(?), this line won't do anything
|
// FIXME: since descriptor from FIR does not have classProto in it(?), this line won't do anything
|
||||||
@@ -99,4 +126,4 @@ internal fun serializablePropertiesForIrBackend(
|
|||||||
irClass.isInternallySerializableEnum() || primaryConstructorParams.size == primaryParamsAsProps.size
|
irClass.isInternallySerializableEnum() || primaryConstructorParams.size == primaryParamsAsProps.size
|
||||||
|
|
||||||
return IrSerializableProperties(serializableProps, isExternallySerializable, primaryCtorSerializableProps, bodySerializableProps)
|
return IrSerializableProperties(serializableProps, isExternallySerializable, primaryCtorSerializableProps, bodySerializableProps)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
// TARGET_BACKEND: JVM_IR
|
||||||
|
|
||||||
|
// WITH_STDLIB
|
||||||
|
|
||||||
|
import kotlinx.serialization.*
|
||||||
|
import kotlinx.serialization.json.*
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
abstract class Top<T: Any> {
|
||||||
|
var top: T? = null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
open class Intermediate<V>: Top<List<V>>() {
|
||||||
|
var inter: V? = null
|
||||||
|
override fun toString(): String {
|
||||||
|
return "Intermediate($top, $inter)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
open class Bottom: Intermediate<String>() {
|
||||||
|
var bot: String? = null
|
||||||
|
override fun toString(): String {
|
||||||
|
return "Bottom($top, $inter, $bot)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class Bottom2: Bottom() {
|
||||||
|
override fun toString(): String {
|
||||||
|
return "Bottom2($top, $inter, $bot)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Full(
|
||||||
|
val b: Bottom2,
|
||||||
|
val i: Intermediate<String>
|
||||||
|
)
|
||||||
|
|
||||||
|
fun box(): String {
|
||||||
|
val j = Json { ignoreUnknownKeys = true }
|
||||||
|
val b = Bottom2().apply {
|
||||||
|
top = listOf("a", "b")
|
||||||
|
inter = "v"
|
||||||
|
bot = "bot"
|
||||||
|
}
|
||||||
|
val f = Full(b, b)
|
||||||
|
val encoded = j.encodeToString(f)
|
||||||
|
if (encoded != """{"b":{"top":["a","b"],"inter":"v","bot":"bot"},"i":{"top":["a","b"],"inter":"v"}}""") return "Encoded: $encoded"
|
||||||
|
|
||||||
|
val decoded = j.decodeFromString<Full>(encoded)
|
||||||
|
if (decoded.toString() != "Full(b=Bottom2([a, b], v, bot), i=Intermediate([a, b], v))") return "Decoded: $decoded"
|
||||||
|
return "OK"
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
// TARGET_BACKEND: JVM_IR
|
||||||
|
|
||||||
|
// WITH_STDLIB
|
||||||
|
|
||||||
|
import kotlinx.serialization.*
|
||||||
|
import kotlinx.serialization.json.*
|
||||||
|
|
||||||
|
// From #1264
|
||||||
|
@Serializable
|
||||||
|
sealed class TypedSealedClass<T>(val a: T) {
|
||||||
|
@Serializable
|
||||||
|
class Child(val y: Int) : TypedSealedClass<String>("10") {
|
||||||
|
override fun toString(): String = "Child($a, $y)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// From #KT-43910
|
||||||
|
@Serializable
|
||||||
|
open class ValidatableValue<T : Any, V: Any>(
|
||||||
|
var value: T? = null,
|
||||||
|
var error: V? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class Email<T: Any> : ValidatableValue<String, T>() { // Note this is a different T
|
||||||
|
override fun toString(): String {
|
||||||
|
return "Email($value, $error)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun box(): String {
|
||||||
|
val encodedChild = """{"a":"11","y":42}"""
|
||||||
|
val decodedChild = Json.decodeFromString<TypedSealedClass.Child>(encodedChild)
|
||||||
|
if (decodedChild.toString() != "Child(11, 42)") return "DecodedChild: $decodedChild"
|
||||||
|
Json.encodeToString(decodedChild)?.let { if (it != encodedChild) return "EncodedChild: $it" }
|
||||||
|
|
||||||
|
val email = Email<Int>().apply {
|
||||||
|
value = "foo"
|
||||||
|
error = 1
|
||||||
|
}
|
||||||
|
val encodedEmail = Json.encodeToString(email)
|
||||||
|
if (encodedEmail != """{"value":"foo","error":1}""") return "EncodedEmail: $encodedEmail"
|
||||||
|
val decodedEmail = Json.decodeFromString<Email<Int>>(encodedEmail)
|
||||||
|
if (decodedEmail.toString() != "Email(foo, 1)") return "DecodedEmail: $decodedEmail"
|
||||||
|
return "OK"
|
||||||
|
}
|
||||||
+12
@@ -51,6 +51,18 @@ public class SerializationFirBlackBoxTestGenerated extends AbstractSerialization
|
|||||||
runTest("plugins/kotlinx-serialization/testData/boxIr/enumsAreCached.kt");
|
runTest("plugins/kotlinx-serialization/testData/boxIr/enumsAreCached.kt");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@TestMetadata("genericBaseClassMultiple.kt")
|
||||||
|
public void testGenericBaseClassMultiple() throws Exception {
|
||||||
|
runTest("plugins/kotlinx-serialization/testData/boxIr/genericBaseClassMultiple.kt");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@TestMetadata("genericBaseClassSimple.kt")
|
||||||
|
public void testGenericBaseClassSimple() throws Exception {
|
||||||
|
runTest("plugins/kotlinx-serialization/testData/boxIr/genericBaseClassSimple.kt");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@TestMetadata("generics.kt")
|
@TestMetadata("generics.kt")
|
||||||
public void testGenerics() throws Exception {
|
public void testGenerics() throws Exception {
|
||||||
|
|||||||
+12
@@ -49,6 +49,18 @@ public class SerializationIrBoxTestGenerated extends AbstractSerializationIrBoxT
|
|||||||
runTest("plugins/kotlinx-serialization/testData/boxIr/enumsAreCached.kt");
|
runTest("plugins/kotlinx-serialization/testData/boxIr/enumsAreCached.kt");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@TestMetadata("genericBaseClassMultiple.kt")
|
||||||
|
public void testGenericBaseClassMultiple() throws Exception {
|
||||||
|
runTest("plugins/kotlinx-serialization/testData/boxIr/genericBaseClassMultiple.kt");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@TestMetadata("genericBaseClassSimple.kt")
|
||||||
|
public void testGenericBaseClassSimple() throws Exception {
|
||||||
|
runTest("plugins/kotlinx-serialization/testData/boxIr/genericBaseClassSimple.kt");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@TestMetadata("generics.kt")
|
@TestMetadata("generics.kt")
|
||||||
public void testGenerics() throws Exception {
|
public void testGenerics() throws Exception {
|
||||||
|
|||||||
Reference in New Issue
Block a user