Skip to content

Commit f023988

Browse files
authored
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
1 parent 093321f commit f023988

File tree

7 files changed

+333
-57
lines changed

7 files changed

+333
-57
lines changed

core/api/kotlinx-serialization-core.api

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -892,6 +892,9 @@ public abstract class kotlinx/serialization/internal/MapLikeSerializer : kotlinx
892892
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
893893
}
894894

895+
public abstract interface annotation class kotlinx/serialization/internal/NamedCompanion : java/lang/annotation/Annotation {
896+
}
897+
895898
public abstract class kotlinx/serialization/internal/NamedValueDecoder : kotlinx/serialization/internal/TaggedDecoder {
896899
public fun <init> ()V
897900
protected fun composeName (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
* Copyright 2017-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.serialization.internal
6+
7+
import kotlinx.serialization.*
8+
9+
/**
10+
* An annotation added by the compiler to the companion object of [Serializable] class, if it has a non-default name.
11+
*/
12+
@InternalSerializationApi
13+
@Target(AnnotationTarget.CLASS)
14+
@Retention(AnnotationRetention.RUNTIME)
15+
public annotation class NamedCompanion
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* Copyright 2017-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
@file:Suppress("RUNTIME_ANNOTATION_NOT_SUPPORTED")
6+
7+
package kotlinx.serialization
8+
9+
import kotlinx.serialization.builtins.*
10+
import kotlinx.serialization.descriptors.*
11+
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
12+
import kotlinx.serialization.encoding.*
13+
import kotlinx.serialization.internal.*
14+
import kotlinx.serialization.test.*
15+
import kotlin.reflect.*
16+
import kotlin.test.*
17+
18+
class SerializersLookupNamedCompanionTest {
19+
@Serializable
20+
class Plain(val i: Int) {
21+
companion object Named
22+
}
23+
24+
@Serializable
25+
class Parametrized<T>(val value: T) {
26+
companion object Named
27+
}
28+
29+
30+
@Serializer(forClass = PlainWithCustom::class)
31+
object PlainSerializer
32+
33+
@Serializable(PlainSerializer::class)
34+
class PlainWithCustom(val i: Int) {
35+
companion object Named
36+
}
37+
38+
class ParametrizedSerializer<T : Any>(val serializer: KSerializer<T>) : KSerializer<ParametrizedWithCustom<T>> {
39+
override val descriptor: SerialDescriptor =
40+
PrimitiveSerialDescriptor("parametrized (${serializer.descriptor})", PrimitiveKind.STRING)
41+
42+
override fun deserialize(decoder: Decoder): ParametrizedWithCustom<T> = TODO("Not yet implemented")
43+
override fun serialize(encoder: Encoder, value: ParametrizedWithCustom<T>) = TODO("Not yet implemented")
44+
}
45+
46+
@Serializable(ParametrizedSerializer::class)
47+
class ParametrizedWithCustom<T>(val i: T) {
48+
companion object Named
49+
}
50+
51+
@Serializable
52+
sealed interface SealedInterface {
53+
companion object Named
54+
}
55+
56+
@Serializable
57+
sealed interface SealedInterfaceWithExplicitAnnotation {
58+
@NamedCompanion
59+
companion object Named
60+
}
61+
62+
63+
@Test
64+
fun test() {
65+
assertSame<KSerializer<*>>(Plain.serializer(), serializer(typeOf<Plain>()))
66+
67+
shouldFail<SerializationException>(beforeKotlin = "1.9.20", onJs = false, onNative = false) {
68+
assertSame<KSerializer<*>>(PlainSerializer, serializer(typeOf<PlainWithCustom>()))
69+
}
70+
71+
shouldFail<SerializationException>(beforeKotlin = "1.9.20", onJs = false, onNative = false) {
72+
assertEquals(
73+
Parametrized.serializer(Int.serializer()).descriptor.toString(),
74+
serializer(typeOf<Parametrized<Int>>()).descriptor.toString()
75+
)
76+
}
77+
78+
shouldFail<SerializationException>(beforeKotlin = "1.9.20", onJs = false, onNative = false) {
79+
assertEquals(
80+
ParametrizedWithCustom.serializer(Int.serializer()).descriptor.toString(),
81+
serializer(typeOf<ParametrizedWithCustom<Int>>()).descriptor.toString()
82+
)
83+
}
84+
85+
shouldFail<SerializationException>(beforeKotlin = "1.9.20", onJs = false, onNative = false) {
86+
assertEquals(
87+
SealedInterface.serializer().descriptor.toString(),
88+
serializer(typeOf<SealedInterface>()).descriptor.toString()
89+
)
90+
}
91+
92+
// should fail because annotation @NamedCompanion will be placed again by the compilation plugin
93+
// and they both will be placed into @Container annotation - thus they will be invisible to the runtime
94+
shouldFail<SerializationException>(sinceKotlin = "1.9.20", onJs = false, onNative = false) {
95+
serializer(typeOf<SealedInterfaceWithExplicitAnnotation>())
96+
}
97+
}
98+
99+
100+
}
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/*
2+
* Copyright 2017-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.serialization.test
6+
7+
import kotlin.test.*
8+
9+
private val currentKotlinVersion = KotlinVersion.CURRENT
10+
11+
private fun String.toKotlinVersion(): KotlinVersion {
12+
val parts = split(".")
13+
val intParts = parts.mapNotNull { it.toIntOrNull() }
14+
if (parts.size != 3 || intParts.size != 3) error("Illegal kotlin version, expected format is 1.2.3")
15+
16+
return KotlinVersion(intParts[0], intParts[1], intParts[2])
17+
}
18+
19+
internal fun runSince(kotlinVersion: String, test: () -> Unit) {
20+
if (currentKotlinVersion >= kotlinVersion.toKotlinVersion()) {
21+
test()
22+
}
23+
}
24+
25+
26+
internal inline fun <reified T : Throwable> shouldFail(
27+
sinceKotlin: String? = null,
28+
beforeKotlin: String? = null,
29+
onJvm: Boolean = true,
30+
onJs: Boolean = true,
31+
onNative: Boolean = true,
32+
test: () -> Unit
33+
) {
34+
val args = mapOf(
35+
"since" to sinceKotlin,
36+
"before" to beforeKotlin,
37+
"onJvm" to onJvm,
38+
"onJs" to onJs,
39+
"onNative" to onNative
40+
)
41+
42+
val sinceVersion = sinceKotlin?.toKotlinVersion()
43+
val beforeVersion = beforeKotlin?.toKotlinVersion()
44+
45+
val version = (sinceVersion != null && currentKotlinVersion >= sinceVersion)
46+
|| (beforeVersion != null && currentKotlinVersion < beforeVersion)
47+
48+
val platform = (isJvm() && onJvm) || (isJs() && onJs) || (isNative() && onNative)
49+
50+
var error: Throwable? = null
51+
try {
52+
test()
53+
} catch (e: Throwable) {
54+
error = e
55+
}
56+
57+
if (version && platform) {
58+
if (error == null) {
59+
throw AssertionError("Exception with type '${T::class.simpleName}' expected for $args")
60+
}
61+
if (error !is T) throw AssertionError(
62+
"Illegal exception type, expected '${T::class.simpleName}' actual '${error::class.simpleName}' for $args",
63+
error
64+
)
65+
} else {
66+
if (error != null) throw AssertionError(
67+
"Unexpected error for $args",
68+
error
69+
)
70+
}
71+
}
72+
73+
internal class CompilerVersionTest {
74+
@Test
75+
fun testSince() {
76+
var executed = false
77+
78+
runSince("1.0.0") {
79+
executed = true
80+
}
81+
assertTrue(executed)
82+
83+
executed = false
84+
runSince("255.255.255") {
85+
executed = true
86+
}
87+
assertFalse(executed)
88+
}
89+
90+
@Test
91+
fun testFailBefore() {
92+
// ok if there is no exception if current version greater is before of the specified
93+
shouldFail<IllegalArgumentException>(beforeKotlin = "0.0.0") {
94+
// no-op
95+
}
96+
97+
// error if there is no exception and if current version is before of the specified
98+
assertFails {
99+
shouldFail<IllegalArgumentException>(beforeKotlin = "255.255.255") {
100+
// no-op
101+
}
102+
}
103+
104+
// ok if thrown expected exception if current version is before of the specified
105+
shouldFail<IllegalArgumentException>(beforeKotlin = "255.255.255") {
106+
throw IllegalArgumentException()
107+
}
108+
109+
// ok if thrown unexpected exception if current version is before of the specified
110+
assertFails {
111+
shouldFail<IllegalArgumentException>(beforeKotlin = "255.255.255") {
112+
throw Exception()
113+
}
114+
}
115+
116+
}
117+
118+
@Test
119+
fun testFailSince() {
120+
// ok if there is no exception if current version less then specified
121+
shouldFail<IllegalArgumentException>(sinceKotlin = "255.255.255") {
122+
// no-op
123+
}
124+
125+
// error if there is no exception and if current version is greater or equals specified
126+
assertFails {
127+
shouldFail<IllegalArgumentException>(sinceKotlin = "0.0.0") {
128+
// no-op
129+
}
130+
}
131+
132+
// ok if thrown expected exception if current version is greater or equals specified
133+
shouldFail<IllegalArgumentException>(sinceKotlin = "0.0.0") {
134+
throw IllegalArgumentException()
135+
}
136+
137+
// ok if thrown unexpected exception if current version is greater or equals specified
138+
assertFails {
139+
shouldFail<IllegalArgumentException>(sinceKotlin = "0.0.0") {
140+
throw Exception()
141+
}
142+
}
143+
}
144+
145+
@Test
146+
fun testExcludePlatform() {
147+
if (isJvm()) {
148+
shouldFail<IllegalArgumentException>(beforeKotlin = "255.255.255", onJvm = false) {
149+
// no-op
150+
}
151+
shouldFail<IllegalArgumentException>(sinceKotlin = "0.0.0", onJvm = false) {
152+
// no-op
153+
}
154+
shouldFail<IllegalArgumentException>(sinceKotlin = "0.0.0", beforeKotlin = "255.255.255", onJvm = false) {
155+
// no-op
156+
}
157+
} else if (isJs()) {
158+
shouldFail<IllegalArgumentException>(beforeKotlin = "255.255.255", onJs = false) {
159+
// no-op
160+
}
161+
shouldFail<IllegalArgumentException>(sinceKotlin = "0.0.0", onJs = false) {
162+
// no-op
163+
}
164+
shouldFail<IllegalArgumentException>(sinceKotlin = "0.0.0", beforeKotlin = "255.255.255", onJs = false) {
165+
// no-op
166+
}
167+
} else if (isNative()) {
168+
shouldFail<IllegalArgumentException>(beforeKotlin = "255.255.255", onNative = false) {
169+
// no-op
170+
}
171+
shouldFail<IllegalArgumentException>(sinceKotlin = "0.0.0", onNative = false) {
172+
// no-op
173+
}
174+
shouldFail<IllegalArgumentException>(sinceKotlin = "0.0.0", beforeKotlin = "255.255.255", onNative = false) {
175+
// no-op
176+
}
177+
}
178+
}
179+
180+
}

core/jvmMain/src/kotlinx/serialization/internal/Platform.kt

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,26 +35,20 @@ internal actual fun <T : Any> KClass<T>.constructSerializerForGivenTypeArgs(vara
3535
return java.constructSerializerForGivenTypeArgs(*args)
3636
}
3737

38-
@Suppress("UNCHECKED_CAST")
3938
internal fun <T: Any> Class<T>.constructSerializerForGivenTypeArgs(vararg args: KSerializer<Any?>): KSerializer<T>? {
4039
if (isEnum && isNotAnnotated()) {
4140
return createEnumSerializer()
4241
}
4342
// Fall-through if the serializer is not found -- lookup on companions (for sealed interfaces) or fallback to polymorphic if applicable
4443
if (isInterface) interfaceSerializer()?.let { return it }
4544
// Search for serializer defined on companion object.
46-
val serializer = invokeSerializerOnCompanion<T>(this, *args)
45+
val serializer = invokeSerializerOnDefaultCompanion<T>(this, *args)
4746
if (serializer != null) return serializer
4847
// Check whether it's serializable object
4948
findObjectSerializer()?.let { return it }
5049
// Search for default serializer if no serializer is defined in companion object.
5150
// It is required for named companions
52-
val fromNamedCompanion = try {
53-
declaredClasses.singleOrNull { it.simpleName == ("\$serializer") }
54-
?.getField("INSTANCE")?.get(null) as? KSerializer<T>
55-
} catch (e: NoSuchFieldException) {
56-
null
57-
}
51+
val fromNamedCompanion = findInNamedCompanion(*args)
5852
if (fromNamedCompanion != null) return fromNamedCompanion
5953
// Check for polymorphic
6054
return if (isPolymorphicSerializer()) {
@@ -64,6 +58,30 @@ internal fun <T: Any> Class<T>.constructSerializerForGivenTypeArgs(vararg args:
6458
}
6559
}
6660

61+
@Suppress("UNCHECKED_CAST")
62+
private fun <T: Any> Class<T>.findInNamedCompanion(vararg args: KSerializer<Any?>): KSerializer<T>? {
63+
val namedCompanion = findNamedCompanionByAnnotation()
64+
if (namedCompanion != null) {
65+
invokeSerializerOnCompanion<T>(namedCompanion, *args)?.let { return it }
66+
}
67+
68+
// fallback strategy for old compiler - try to locate plugin-generated singleton (without type parameters) serializer
69+
return try {
70+
declaredClasses.singleOrNull { it.simpleName == ("\$serializer") }
71+
?.getField("INSTANCE")?.get(null) as? KSerializer<T>
72+
} catch (e: NoSuchFieldException) {
73+
null
74+
}
75+
}
76+
77+
private fun <T: Any> Class<T>.findNamedCompanionByAnnotation(): Any? {
78+
val companionClass = declaredClasses.firstOrNull { clazz ->
79+
clazz.getAnnotation(NamedCompanion::class.java) != null
80+
} ?: return null
81+
82+
return companionOrNull(companionClass.simpleName)
83+
}
84+
6785
private fun <T: Any> Class<T>.isNotAnnotated(): Boolean {
6886
/*
6987
* 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>? {
100118
return null
101119
}
102120

121+
private fun <T : Any> invokeSerializerOnDefaultCompanion(jClass: Class<*>, vararg args: KSerializer<Any?>): KSerializer<T>? {
122+
val companion = jClass.companionOrNull("Companion") ?: return null
123+
return invokeSerializerOnCompanion(companion, *args)
124+
}
125+
103126
@Suppress("UNCHECKED_CAST")
104-
private fun <T : Any> invokeSerializerOnCompanion(jClass: Class<*>, vararg args: KSerializer<Any?>): KSerializer<T>? {
105-
val companion = jClass.companionOrNull() ?: return null
127+
private fun <T : Any> invokeSerializerOnCompanion(companion: Any, vararg args: KSerializer<Any?>): KSerializer<T>? {
106128
return try {
107129
val types = if (args.isEmpty()) emptyArray() else Array(args.size) { KSerializer::class.java }
108130
companion.javaClass.getDeclaredMethod("serializer", *types)
@@ -115,9 +137,9 @@ private fun <T : Any> invokeSerializerOnCompanion(jClass: Class<*>, vararg args:
115137
}
116138
}
117139

118-
private fun Class<*>.companionOrNull() =
140+
private fun Class<*>.companionOrNull(companionName: String) =
119141
try {
120-
val companion = getDeclaredField("Companion")
142+
val companion = getDeclaredField(companionName)
121143
companion.isAccessible = true
122144
companion.get(null)
123145
} catch (e: Throwable) {

0 commit comments

Comments
 (0)