diff --git a/plugins/kotlinx-serialization/kotlinx-serialization.backend/src/org/jetbrains/kotlinx/serialization/compiler/backend/ir/IrPredicates.kt b/plugins/kotlinx-serialization/kotlinx-serialization.backend/src/org/jetbrains/kotlinx/serialization/compiler/backend/ir/IrPredicates.kt index 143c46cfa3c..71ecb1fb9dc 100644 --- a/plugins/kotlinx-serialization/kotlinx-serialization.backend/src/org/jetbrains/kotlinx/serialization/compiler/backend/ir/IrPredicates.kt +++ b/plugins/kotlinx-serialization/kotlinx-serialization.backend/src/org/jetbrains/kotlinx/serialization/compiler/backend/ir/IrPredicates.kt @@ -79,6 +79,9 @@ internal fun IrClass.findEnumLegacySerializer(): IrClass? { internal val IrClass.isSealedSerializableInterface: Boolean get() = kind == ClassKind.INTERFACE && modality == Modality.SEALED && hasSerializableOrMetaAnnotation() +internal val IrClass.isSerializableInterfaceWithCustom: Boolean + get() = kind == ClassKind.INTERFACE && hasSerializableAnnotationWithArgs() + internal fun IrClass.isInternallySerializableEnum(): Boolean = kind == ClassKind.ENUM_CLASS && hasSerializableOrMetaAnnotationWithoutArgs() @@ -93,6 +96,11 @@ internal fun IrClass.hasSerializableOrMetaAnnotationWithoutArgs(): Boolean = che fun IrClass.hasSerializableOrMetaAnnotation() = checkSerializableOrMetaAnnotationArgs(mustDoNotHaveArgs = false) +private fun IrClass.hasSerializableAnnotationWithArgs(): Boolean { + val annot = getAnnotation(SerializationAnnotations.serializableAnnotationFqName) + return annot?.getValueArgument(0) != null +} + private fun IrClass.checkSerializableOrMetaAnnotationArgs(mustDoNotHaveArgs: Boolean): Boolean { val annot = getAnnotation(SerializationAnnotations.serializableAnnotationFqName) if (annot != null) { // @Serializable have higher priority @@ -118,7 +126,7 @@ internal fun IrClass.shouldHaveGeneratedSerializer(): Boolean = || isEnumWithLegacyGeneratedSerializer() internal val IrClass.shouldHaveGeneratedMethodsInCompanion: Boolean - get() = this.isSerializableObject || this.isSerializableEnum() || (this.kind == ClassKind.CLASS && hasSerializableOrMetaAnnotation()) || this.isSealedSerializableInterface + get() = this.isSerializableObject || this.isSerializableEnum() || (this.kind == ClassKind.CLASS && hasSerializableOrMetaAnnotation()) || this.isSealedSerializableInterface || this.isSerializableInterfaceWithCustom internal fun IrClass.isSerializableEnum(): Boolean = kind == ClassKind.ENUM_CLASS && hasSerializableOrMetaAnnotation() @@ -193,6 +201,7 @@ internal fun IrClass.needSerializerFactory(compilerContext: SerializationPluginC if (serializableClass.isSerializableEnum()) return true if (serializableClass.isAbstractOrSealedSerializableClass) return true if (serializableClass.isSealedSerializableInterface) return true + if (serializableClass.isSerializableInterfaceWithCustom) return true if (serializableClass.typeParameters.isEmpty()) return false return true } diff --git a/plugins/kotlinx-serialization/kotlinx-serialization.k1/src/org/jetbrains/kotlinx/serialization/compiler/resolve/KSerializationUtil.kt b/plugins/kotlinx-serialization/kotlinx-serialization.k1/src/org/jetbrains/kotlinx/serialization/compiler/resolve/KSerializationUtil.kt index d4116221acc..e5e77d44004 100644 --- a/plugins/kotlinx-serialization/kotlinx-serialization.k1/src/org/jetbrains/kotlinx/serialization/compiler/resolve/KSerializationUtil.kt +++ b/plugins/kotlinx-serialization/kotlinx-serialization.k1/src/org/jetbrains/kotlinx/serialization/compiler/resolve/KSerializationUtil.kt @@ -113,7 +113,11 @@ val KotlinType?.toClassDescriptor: ClassDescriptor? } val ClassDescriptor.shouldHaveGeneratedMethodsInCompanion: Boolean - get() = this.isSerializableObject || this.isSerializableEnum() || (this.kind == ClassKind.CLASS && hasSerializableOrMetaAnnotation) || this.isSealedSerializableInterface + get() = this.isSerializableObject + || this.isSerializableEnum() + || (this.kind == ClassKind.CLASS && hasSerializableOrMetaAnnotation) + || this.isSealedSerializableInterface + || this.isSerializableInterfaceWithCustom val ClassDescriptor.isSerializableObject: Boolean get() = kind == ClassKind.OBJECT && hasSerializableOrMetaAnnotation @@ -124,6 +128,9 @@ val ClassDescriptor.isInternallySerializableObject: Boolean val ClassDescriptor.isSealedSerializableInterface: Boolean get() = kind == ClassKind.INTERFACE && modality == Modality.SEALED && hasSerializableOrMetaAnnotation +val ClassDescriptor.isSerializableInterfaceWithCustom: Boolean + get() = kind == ClassKind.INTERFACE && hasSerializableAnnotationWithArgs + val ClassDescriptor.isAbstractOrSealedOrInterface: Boolean get() = kind == ClassKind.INTERFACE || modality == Modality.SEALED || modality == Modality.ABSTRACT @@ -196,6 +203,15 @@ private val ClassDescriptor.hasSerializableAnnotationWithoutArgs: Boolean return psi.valueArguments.isEmpty() } +private val ClassDescriptor.hasSerializableAnnotationWithArgs: Boolean + get() { + if (!hasSerializableAnnotation) return false + // If provided descriptor is lazy, carefully look at psi in order not to trigger full resolve which may be recursive. + // Otherwise, this descriptor is deserialized from another module, and it is OK to check value right away. + val psi = findSerializableAnnotationDeclaration() ?: return (serializableWith != null) + return psi.valueArguments.isNotEmpty() + } + private fun Annotated.findSerializableAnnotationDeclaration(): KtAnnotationEntry? { val lazyDesc = annotations.findAnnotation(serializableAnnotationFqName) as? LazyAnnotationDescriptor return lazyDesc?.annotationEntry @@ -277,6 +293,7 @@ fun ClassDescriptor.needSerializerFactory(): Boolean { if (serializableClass.isSerializableEnum()) return true if (serializableClass.isAbstractOrSealedSerializableClass()) return true if (serializableClass.isSealedSerializableInterface) return true + if (serializableClass.isSerializableInterfaceWithCustom) return true if (serializableClass.declaredTypeParameters.isEmpty()) return false return true } diff --git a/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/SerializationFirUtils.kt b/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/SerializationFirUtils.kt index fb012077bd8..09cdfb8b22b 100644 --- a/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/SerializationFirUtils.kt +++ b/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/SerializationFirUtils.kt @@ -98,6 +98,15 @@ fun FirClassSymbol<*>.hasSerializableAnnotationWithoutArgs(session: FirSession): } } ?: false +fun FirClassSymbol<*>.hasSerializableAnnotationWithArgs(session: FirSession): Boolean { + val annotation = serializableAnnotation(needArguments = false, session) ?: return false + return if (annotation is FirAnnotationCall) { + annotation.arguments.isNotEmpty() + } else { + annotation.argumentMapping.mapping.isNotEmpty() + } +} + internal fun FirBasedSymbol<*>.getSerializableWith(session: FirSession): ConeKotlinType? = serializableAnnotation(needArguments = true, session)?.getKClassArgument(AnnotationParameterNames.WITH) @@ -132,6 +141,10 @@ context(FirSession) internal val FirClassSymbol<*>.isSealedSerializableInterface: Boolean get() = classKind.isInterface && rawStatus.modality == Modality.SEALED && hasSerializableOrMetaAnnotation +context(FirSession) +internal val FirClassSymbol<*>.isSerializableInterfaceWithCustom: Boolean + get() = classKind.isInterface && hasSerializableAnnotationWithArgs(this@FirSession) + context(FirSession) val FirClassSymbol<*>.hasSerializableOrMetaAnnotation: Boolean get() = hasSerializableAnnotation || hasMetaSerializableAnnotation @@ -146,6 +159,7 @@ internal val FirClassSymbol<*>.shouldHaveGeneratedMethodsInCompanion: Boolean || isSerializableEnum || (classKind == ClassKind.CLASS && hasSerializableOrMetaAnnotation) || isSealedSerializableInterface + || isSerializableInterfaceWithCustom context(FirSession) internal val FirClassSymbol<*>.companionNeedsSerializerFactory: Boolean @@ -155,6 +169,7 @@ internal val FirClassSymbol<*>.companionNeedsSerializerFactory: Boolean if (isSerializableEnum) return true if (isAbstractOrSealedSerializableClass) return true if (isSealedSerializableInterface) return true + if (isSerializableInterfaceWithCustom) return true if (typeParameterSymbols.isEmpty()) return false return true } diff --git a/plugins/kotlinx-serialization/testData/boxIr/interfaces.kt b/plugins/kotlinx-serialization/testData/boxIr/interfaces.kt new file mode 100644 index 00000000000..3292d02bac1 --- /dev/null +++ b/plugins/kotlinx-serialization/testData/boxIr/interfaces.kt @@ -0,0 +1,90 @@ +// WITH_STDLIB + +package a + +import kotlinx.serialization.* +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.* +import kotlin.reflect.* + + +object InterfaceCustomSerializer: ToDoSerializer("interface") + +@Serializable(InterfaceCustomSerializer::class) +interface InterfaceWithCustom + + +object NamedInterfaceSerializer: ToDoSerializer("interface.named") + +@Serializable(NamedInterfaceSerializer::class) +interface NamedInterfaceWithCustom { + companion object Named { + + } +} + + +@Serializable +sealed interface SealedInterface + +@Serializable +sealed interface NamedSealedInterface { + companion object Named { + + } +} + +object SealedInterfaceCustomSerializer: ToDoSerializer("sealed interface") + +@Serializable(SealedInterfaceCustomSerializer::class) +sealed interface SealedInterfaceWithCustom + +object NamedSealedInterfaceCustomSerializer: ToDoSerializer("sealed interface.named") + +@Serializable(NamedSealedInterfaceCustomSerializer::class) +sealed interface NamedSealedInterfaceWithCustom { + companion object Named { + + } +} + + + + +@Serializable +class Holder( + val a: InterfaceWithCustom, + val b: NamedInterfaceWithCustom, + val c: SealedInterface, + val d: NamedSealedInterface, + val e: SealedInterfaceWithCustom, + val f: NamedSealedInterfaceWithCustom, +) + +fun box(): String { + check(InterfaceWithCustom.serializer(), "interface", 0)?.let { return it } + check(NamedInterfaceWithCustom.Named.serializer(), "interface.named", 1, true)?.let { return it } + + check(SealedInterface.serializer(), "a.SealedInterface", 2)?.let { return it } + check(NamedSealedInterface.serializer(), "a.NamedSealedInterface", 3, true)?.let { return it } + + check(SealedInterfaceWithCustom.serializer(), "sealed interface", 4)?.let { return it } + check(NamedSealedInterfaceWithCustom.serializer(), "sealed interface.named", 5, true)?.let { return it } + + return "OK" +} + +inline fun check(serializer: KSerializer, serializerName: String, index: Int, namedCompanion: Boolean = false): String? { + if (serializer.descriptor.serialName != serializerName) return "${T::class.simpleName}.serializer().descriptor.serialName=" + serializer.descriptor.serialName + if (serializer().descriptor.serialName != serializerName) return "serializer<${T::class.simpleName}>().descriptor.serialName=" + serializer().descriptor.serialName + if (!namedCompanion && serializer(typeOf()).descriptor.serialName != serializerName) return "serializer(typeOf<${T::class.simpleName}>()).descriptor.serialName=" + serializer(typeOf()).descriptor.serialName + if (Holder.serializer().descriptor.getElementDescriptor(index).serialName != serializerName) return "holderDescriptor.getElementDescriptor($index).serialName=" + Holder.serializer().descriptor.getElementDescriptor(index).serialName + + return null +} + +abstract class ToDoSerializer(descriptorName: String): KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(descriptorName, PrimitiveKind.STRING) + override fun deserialize(decoder: Decoder): T = TODO() + override fun serialize(encoder: Encoder, value: T) = TODO() +} \ No newline at end of file diff --git a/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationFirJsBoxTestGenerated.java b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationFirJsBoxTestGenerated.java index acf4eae0893..d2416c20ec9 100644 --- a/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationFirJsBoxTestGenerated.java +++ b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationFirJsBoxTestGenerated.java @@ -49,6 +49,12 @@ public class SerializationFirJsBoxTestGenerated extends AbstractSerializationFir runTest("plugins/kotlinx-serialization/testData/boxIr/excludedFromFileExport.kt"); } + @Test + @TestMetadata("interfaces.kt") + public void testInterfaces() throws Exception { + runTest("plugins/kotlinx-serialization/testData/boxIr/interfaces.kt"); + } + @Test @TestMetadata("serializerFactory.kt") public void testSerializerFactory() throws Exception { diff --git a/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationFirLightTreeBlackBoxTestGenerated.java b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationFirLightTreeBlackBoxTestGenerated.java index 2f5a114d0b7..f88f914eac2 100644 --- a/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationFirLightTreeBlackBoxTestGenerated.java +++ b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationFirLightTreeBlackBoxTestGenerated.java @@ -141,6 +141,12 @@ public class SerializationFirLightTreeBlackBoxTestGenerated extends AbstractSeri runTest("plugins/kotlinx-serialization/testData/boxIr/inlineClasses.kt"); } + @Test + @TestMetadata("interfaces.kt") + public void testInterfaces() throws Exception { + runTest("plugins/kotlinx-serialization/testData/boxIr/interfaces.kt"); + } + @Test @TestMetadata("intrinsicAnnotations.kt") public void testIntrinsicAnnotations() throws Exception { diff --git a/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationIrBoxTestGenerated.java b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationIrBoxTestGenerated.java index d9fef509799..3d43947e9ad 100644 --- a/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationIrBoxTestGenerated.java +++ b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationIrBoxTestGenerated.java @@ -139,6 +139,12 @@ public class SerializationIrBoxTestGenerated extends AbstractSerializationIrBoxT runTest("plugins/kotlinx-serialization/testData/boxIr/inlineClasses.kt"); } + @Test + @TestMetadata("interfaces.kt") + public void testInterfaces() throws Exception { + runTest("plugins/kotlinx-serialization/testData/boxIr/interfaces.kt"); + } + @Test @TestMetadata("intrinsicAnnotations.kt") public void testIntrinsicAnnotations() throws Exception { diff --git a/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationIrJsBoxTestGenerated.java b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationIrJsBoxTestGenerated.java index d9e2e2f3fd0..6057c3ad7ec 100644 --- a/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationIrJsBoxTestGenerated.java +++ b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationIrJsBoxTestGenerated.java @@ -49,6 +49,12 @@ public class SerializationIrJsBoxTestGenerated extends AbstractSerializationIrJs runTest("plugins/kotlinx-serialization/testData/boxIr/excludedFromFileExport.kt"); } + @Test + @TestMetadata("interfaces.kt") + public void testInterfaces() throws Exception { + runTest("plugins/kotlinx-serialization/testData/boxIr/interfaces.kt"); + } + @Test @TestMetadata("serializerFactory.kt") public void testSerializerFactory() throws Exception {