diff options
author | Sergey Shanshin <sergey.shanshin@jetbrains.com> | 2023-08-02 19:41:51 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-08-02 18:41:51 +0200 |
commit | f023988e9dba1ab2e5d0044e804a587d7cf0546e (patch) | |
tree | d2839942e620855c2aeb82b9d7338f5c8039c121 | |
parent | 093321f21e2d28ffcce12fffa677b7c92fb54ea9 (diff) | |
download | kotlinx.serialization-f023988e9dba1ab2e5d0044e804a587d7cf0546e.tar.gz |
Added annotation for named companion objects (#2381)
The annotation will be added to the named companion class by the compiler starting from 1.9.20
7 files changed, 333 insertions, 57 deletions
diff --git a/core/api/kotlinx-serialization-core.api b/core/api/kotlinx-serialization-core.api index 1fddfd85..4b46dcca 100644 --- a/core/api/kotlinx-serialization-core.api +++ b/core/api/kotlinx-serialization-core.api @@ -892,6 +892,9 @@ public abstract class kotlinx/serialization/internal/MapLikeSerializer : kotlinx public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V } +public abstract interface annotation class kotlinx/serialization/internal/NamedCompanion : java/lang/annotation/Annotation { +} + public abstract class kotlinx/serialization/internal/NamedValueDecoder : kotlinx/serialization/internal/TaggedDecoder { public fun <init> ()V protected fun composeName (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; diff --git a/core/commonMain/src/kotlinx/serialization/internal/NamedCompanion.kt b/core/commonMain/src/kotlinx/serialization/internal/NamedCompanion.kt new file mode 100644 index 00000000..0756dc62 --- /dev/null +++ b/core/commonMain/src/kotlinx/serialization/internal/NamedCompanion.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2017-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.serialization.internal + +import kotlinx.serialization.* + +/** + * An annotation added by the compiler to the companion object of [Serializable] class, if it has a non-default name. + */ +@InternalSerializationApi +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +public annotation class NamedCompanion diff --git a/core/commonTest/src/kotlinx/serialization/SerializersLookupNamedCompanionTest.kt b/core/commonTest/src/kotlinx/serialization/SerializersLookupNamedCompanionTest.kt new file mode 100644 index 00000000..6c429dbd --- /dev/null +++ b/core/commonTest/src/kotlinx/serialization/SerializersLookupNamedCompanionTest.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2017-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("RUNTIME_ANNOTATION_NOT_SUPPORTED") + +package kotlinx.serialization + +import kotlinx.serialization.builtins.* +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.encoding.* +import kotlinx.serialization.internal.* +import kotlinx.serialization.test.* +import kotlin.reflect.* +import kotlin.test.* + +class SerializersLookupNamedCompanionTest { + @Serializable + class Plain(val i: Int) { + companion object Named + } + + @Serializable + class Parametrized<T>(val value: T) { + companion object Named + } + + + @Serializer(forClass = PlainWithCustom::class) + object PlainSerializer + + @Serializable(PlainSerializer::class) + class PlainWithCustom(val i: Int) { + companion object Named + } + + class ParametrizedSerializer<T : Any>(val serializer: KSerializer<T>) : KSerializer<ParametrizedWithCustom<T>> { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("parametrized (${serializer.descriptor})", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): ParametrizedWithCustom<T> = TODO("Not yet implemented") + override fun serialize(encoder: Encoder, value: ParametrizedWithCustom<T>) = TODO("Not yet implemented") + } + + @Serializable(ParametrizedSerializer::class) + class ParametrizedWithCustom<T>(val i: T) { + companion object Named + } + + @Serializable + sealed interface SealedInterface { + companion object Named + } + + @Serializable + sealed interface SealedInterfaceWithExplicitAnnotation { + @NamedCompanion + companion object Named + } + + + @Test + fun test() { + assertSame<KSerializer<*>>(Plain.serializer(), serializer(typeOf<Plain>())) + + shouldFail<SerializationException>(beforeKotlin = "1.9.20", onJs = false, onNative = false) { + assertSame<KSerializer<*>>(PlainSerializer, serializer(typeOf<PlainWithCustom>())) + } + + shouldFail<SerializationException>(beforeKotlin = "1.9.20", onJs = false, onNative = false) { + assertEquals( + Parametrized.serializer(Int.serializer()).descriptor.toString(), + serializer(typeOf<Parametrized<Int>>()).descriptor.toString() + ) + } + + shouldFail<SerializationException>(beforeKotlin = "1.9.20", onJs = false, onNative = false) { + assertEquals( + ParametrizedWithCustom.serializer(Int.serializer()).descriptor.toString(), + serializer(typeOf<ParametrizedWithCustom<Int>>()).descriptor.toString() + ) + } + + shouldFail<SerializationException>(beforeKotlin = "1.9.20", onJs = false, onNative = false) { + assertEquals( + SealedInterface.serializer().descriptor.toString(), + serializer(typeOf<SealedInterface>()).descriptor.toString() + ) + } + + // should fail because annotation @NamedCompanion will be placed again by the compilation plugin + // and they both will be placed into @Container annotation - thus they will be invisible to the runtime + shouldFail<SerializationException>(sinceKotlin = "1.9.20", onJs = false, onNative = false) { + serializer(typeOf<SealedInterfaceWithExplicitAnnotation>()) + } + } + + +}
\ No newline at end of file diff --git a/core/commonTest/src/kotlinx/serialization/test/CompilerVersions.kt b/core/commonTest/src/kotlinx/serialization/test/CompilerVersions.kt new file mode 100644 index 00000000..e9c7670a --- /dev/null +++ b/core/commonTest/src/kotlinx/serialization/test/CompilerVersions.kt @@ -0,0 +1,180 @@ +/* + * Copyright 2017-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.serialization.test + +import kotlin.test.* + +private val currentKotlinVersion = KotlinVersion.CURRENT + +private fun String.toKotlinVersion(): KotlinVersion { + val parts = split(".") + val intParts = parts.mapNotNull { it.toIntOrNull() } + if (parts.size != 3 || intParts.size != 3) error("Illegal kotlin version, expected format is 1.2.3") + + return KotlinVersion(intParts[0], intParts[1], intParts[2]) +} + +internal fun runSince(kotlinVersion: String, test: () -> Unit) { + if (currentKotlinVersion >= kotlinVersion.toKotlinVersion()) { + test() + } +} + + +internal inline fun <reified T : Throwable> shouldFail( + sinceKotlin: String? = null, + beforeKotlin: String? = null, + onJvm: Boolean = true, + onJs: Boolean = true, + onNative: Boolean = true, + test: () -> Unit +) { + val args = mapOf( + "since" to sinceKotlin, + "before" to beforeKotlin, + "onJvm" to onJvm, + "onJs" to onJs, + "onNative" to onNative + ) + + val sinceVersion = sinceKotlin?.toKotlinVersion() + val beforeVersion = beforeKotlin?.toKotlinVersion() + + val version = (sinceVersion != null && currentKotlinVersion >= sinceVersion) + || (beforeVersion != null && currentKotlinVersion < beforeVersion) + + val platform = (isJvm() && onJvm) || (isJs() && onJs) || (isNative() && onNative) + + var error: Throwable? = null + try { + test() + } catch (e: Throwable) { + error = e + } + + if (version && platform) { + if (error == null) { + throw AssertionError("Exception with type '${T::class.simpleName}' expected for $args") + } + if (error !is T) throw AssertionError( + "Illegal exception type, expected '${T::class.simpleName}' actual '${error::class.simpleName}' for $args", + error + ) + } else { + if (error != null) throw AssertionError( + "Unexpected error for $args", + error + ) + } +} + +internal class CompilerVersionTest { + @Test + fun testSince() { + var executed = false + + runSince("1.0.0") { + executed = true + } + assertTrue(executed) + + executed = false + runSince("255.255.255") { + executed = true + } + assertFalse(executed) + } + + @Test + fun testFailBefore() { + // ok if there is no exception if current version greater is before of the specified + shouldFail<IllegalArgumentException>(beforeKotlin = "0.0.0") { + // no-op + } + + // error if there is no exception and if current version is before of the specified + assertFails { + shouldFail<IllegalArgumentException>(beforeKotlin = "255.255.255") { + // no-op + } + } + + // ok if thrown expected exception if current version is before of the specified + shouldFail<IllegalArgumentException>(beforeKotlin = "255.255.255") { + throw IllegalArgumentException() + } + + // ok if thrown unexpected exception if current version is before of the specified + assertFails { + shouldFail<IllegalArgumentException>(beforeKotlin = "255.255.255") { + throw Exception() + } + } + + } + + @Test + fun testFailSince() { + // ok if there is no exception if current version less then specified + shouldFail<IllegalArgumentException>(sinceKotlin = "255.255.255") { + // no-op + } + + // error if there is no exception and if current version is greater or equals specified + assertFails { + shouldFail<IllegalArgumentException>(sinceKotlin = "0.0.0") { + // no-op + } + } + + // ok if thrown expected exception if current version is greater or equals specified + shouldFail<IllegalArgumentException>(sinceKotlin = "0.0.0") { + throw IllegalArgumentException() + } + + // ok if thrown unexpected exception if current version is greater or equals specified + assertFails { + shouldFail<IllegalArgumentException>(sinceKotlin = "0.0.0") { + throw Exception() + } + } + } + + @Test + fun testExcludePlatform() { + if (isJvm()) { + shouldFail<IllegalArgumentException>(beforeKotlin = "255.255.255", onJvm = false) { + // no-op + } + shouldFail<IllegalArgumentException>(sinceKotlin = "0.0.0", onJvm = false) { + // no-op + } + shouldFail<IllegalArgumentException>(sinceKotlin = "0.0.0", beforeKotlin = "255.255.255", onJvm = false) { + // no-op + } + } else if (isJs()) { + shouldFail<IllegalArgumentException>(beforeKotlin = "255.255.255", onJs = false) { + // no-op + } + shouldFail<IllegalArgumentException>(sinceKotlin = "0.0.0", onJs = false) { + // no-op + } + shouldFail<IllegalArgumentException>(sinceKotlin = "0.0.0", beforeKotlin = "255.255.255", onJs = false) { + // no-op + } + } else if (isNative()) { + shouldFail<IllegalArgumentException>(beforeKotlin = "255.255.255", onNative = false) { + // no-op + } + shouldFail<IllegalArgumentException>(sinceKotlin = "0.0.0", onNative = false) { + // no-op + } + shouldFail<IllegalArgumentException>(sinceKotlin = "0.0.0", beforeKotlin = "255.255.255", onNative = false) { + // no-op + } + } + } + +} diff --git a/core/jvmMain/src/kotlinx/serialization/internal/Platform.kt b/core/jvmMain/src/kotlinx/serialization/internal/Platform.kt index 67a735c9..399dbb23 100644 --- a/core/jvmMain/src/kotlinx/serialization/internal/Platform.kt +++ b/core/jvmMain/src/kotlinx/serialization/internal/Platform.kt @@ -35,7 +35,6 @@ internal actual fun <T : Any> KClass<T>.constructSerializerForGivenTypeArgs(vara return java.constructSerializerForGivenTypeArgs(*args) } -@Suppress("UNCHECKED_CAST") internal fun <T: Any> Class<T>.constructSerializerForGivenTypeArgs(vararg args: KSerializer<Any?>): KSerializer<T>? { if (isEnum && isNotAnnotated()) { return createEnumSerializer() @@ -43,18 +42,13 @@ internal fun <T: Any> Class<T>.constructSerializerForGivenTypeArgs(vararg args: // Fall-through if the serializer is not found -- lookup on companions (for sealed interfaces) or fallback to polymorphic if applicable if (isInterface) interfaceSerializer()?.let { return it } // Search for serializer defined on companion object. - val serializer = invokeSerializerOnCompanion<T>(this, *args) + val serializer = invokeSerializerOnDefaultCompanion<T>(this, *args) if (serializer != null) return serializer // Check whether it's serializable object findObjectSerializer()?.let { return it } // Search for default serializer if no serializer is defined in companion object. // It is required for named companions - val fromNamedCompanion = try { - declaredClasses.singleOrNull { it.simpleName == ("\$serializer") } - ?.getField("INSTANCE")?.get(null) as? KSerializer<T> - } catch (e: NoSuchFieldException) { - null - } + val fromNamedCompanion = findInNamedCompanion(*args) if (fromNamedCompanion != null) return fromNamedCompanion // Check for polymorphic return if (isPolymorphicSerializer()) { @@ -64,6 +58,30 @@ internal fun <T: Any> Class<T>.constructSerializerForGivenTypeArgs(vararg args: } } +@Suppress("UNCHECKED_CAST") +private fun <T: Any> Class<T>.findInNamedCompanion(vararg args: KSerializer<Any?>): KSerializer<T>? { + val namedCompanion = findNamedCompanionByAnnotation() + if (namedCompanion != null) { + invokeSerializerOnCompanion<T>(namedCompanion, *args)?.let { return it } + } + + // fallback strategy for old compiler - try to locate plugin-generated singleton (without type parameters) serializer + return try { + declaredClasses.singleOrNull { it.simpleName == ("\$serializer") } + ?.getField("INSTANCE")?.get(null) as? KSerializer<T> + } catch (e: NoSuchFieldException) { + null + } +} + +private fun <T: Any> Class<T>.findNamedCompanionByAnnotation(): Any? { + val companionClass = declaredClasses.firstOrNull { clazz -> + clazz.getAnnotation(NamedCompanion::class.java) != null + } ?: return null + + return companionOrNull(companionClass.simpleName) +} + private fun <T: Any> Class<T>.isNotAnnotated(): Boolean { /* * For annotated enums search serializer directly (or do not search at all?) @@ -100,9 +118,13 @@ private fun <T: Any> Class<T>.interfaceSerializer(): KSerializer<T>? { return null } +private fun <T : Any> invokeSerializerOnDefaultCompanion(jClass: Class<*>, vararg args: KSerializer<Any?>): KSerializer<T>? { + val companion = jClass.companionOrNull("Companion") ?: return null + return invokeSerializerOnCompanion(companion, *args) +} + @Suppress("UNCHECKED_CAST") -private fun <T : Any> invokeSerializerOnCompanion(jClass: Class<*>, vararg args: KSerializer<Any?>): KSerializer<T>? { - val companion = jClass.companionOrNull() ?: return null +private fun <T : Any> invokeSerializerOnCompanion(companion: Any, vararg args: KSerializer<Any?>): KSerializer<T>? { return try { val types = if (args.isEmpty()) emptyArray() else Array(args.size) { KSerializer::class.java } companion.javaClass.getDeclaredMethod("serializer", *types) @@ -115,9 +137,9 @@ private fun <T : Any> invokeSerializerOnCompanion(jClass: Class<*>, vararg args: } } -private fun Class<*>.companionOrNull() = +private fun Class<*>.companionOrNull(companionName: String) = try { - val companion = getDeclaredField("Companion") + val companion = getDeclaredField(companionName) companion.isAccessible = true companion.get(null) } catch (e: Throwable) { diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/SerializableOnPropertyTypeAndTypealiasTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/SerializableOnPropertyTypeAndTypealiasTest.kt index 505cb48d..7c7133c7 100644 --- a/formats/json-tests/commonTest/src/kotlinx/serialization/SerializableOnPropertyTypeAndTypealiasTest.kt +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/SerializableOnPropertyTypeAndTypealiasTest.kt @@ -3,7 +3,6 @@ package kotlinx.serialization import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* import kotlinx.serialization.json.* -import kotlinx.serialization.test.runSince import kotlin.test.* @Serializable @@ -83,7 +82,7 @@ class SerializableOnPropertyTypeAndTypealiasTest : JsonTestBase() { } @Test - fun testWithoutDefault() = runSince("1.8.20") { // Ignored by #1895 + fun testWithoutDefault() { // Ignored by #1895 val t = TesterWithoutDefault(WithoutDefault("a"), WithoutDefault("b"), WithoutDefault("c"), WithoutDefault("d")) assertJsonFormAndRestored( TesterWithoutDefault.serializer(), diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/test/CompilerVersions.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/test/CompilerVersions.kt deleted file mode 100644 index 9a524054..00000000 --- a/formats/json-tests/commonTest/src/kotlinx/serialization/test/CompilerVersions.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2017-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.serialization.test - -import kotlin.test.Test -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -private val currentKotlinVersion = KotlinVersion.CURRENT - -private fun String.toKotlinVersion(): KotlinVersion { - val parts = split(".") - val intParts = parts.mapNotNull { it.toIntOrNull() } - if (parts.size != 3 || intParts.size != 3) error("Illegal kotlin version, expected format is 1.2.3") - - return KotlinVersion(intParts[0], intParts[1], intParts[2]) -} - -internal fun runSince(kotlinVersion: String, test: () -> Unit) { - if (currentKotlinVersion >= kotlinVersion.toKotlinVersion()) { - test() - } -} - -internal class CompilerVersionTest { - @Test - fun testSince() { - var executed = false - - runSince("1.0.0") { - executed = true - } - assertTrue(executed) - - executed = false - runSince("255.255.255") { - executed = true - } - assertFalse(executed) - } -} |