summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSergey Shanshin <sergey.shanshin@jetbrains.com>2023-08-02 19:41:51 +0300
committerGitHub <noreply@github.com>2023-08-02 18:41:51 +0200
commitf023988e9dba1ab2e5d0044e804a587d7cf0546e (patch)
treed2839942e620855c2aeb82b9d7338f5c8039c121
parent093321f21e2d28ffcce12fffa677b7c92fb54ea9 (diff)
downloadkotlinx.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
-rw-r--r--core/api/kotlinx-serialization-core.api3
-rw-r--r--core/commonMain/src/kotlinx/serialization/internal/NamedCompanion.kt15
-rw-r--r--core/commonTest/src/kotlinx/serialization/SerializersLookupNamedCompanionTest.kt100
-rw-r--r--core/commonTest/src/kotlinx/serialization/test/CompilerVersions.kt180
-rw-r--r--core/jvmMain/src/kotlinx/serialization/internal/Platform.kt46
-rw-r--r--formats/json-tests/commonTest/src/kotlinx/serialization/SerializableOnPropertyTypeAndTypealiasTest.kt3
-rw-r--r--formats/json-tests/commonTest/src/kotlinx/serialization/test/CompilerVersions.kt43
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)
- }
-}