diff --git a/plugins/kotlinx-serialization/testData/matrix/enums.kt b/plugins/kotlinx-serialization/testData/matrix/enums.kt new file mode 100644 index 00000000000..bdd2198d462 --- /dev/null +++ b/plugins/kotlinx-serialization/testData/matrix/enums.kt @@ -0,0 +1,254 @@ +// WITH_STDLIB + +// +// NOTE: THIS FILE IS AUTO-GENERATED by the TestMatrixIntegration.kt, DO NOT EDIT! +// + +@file:UseSerializers(EnumWithUseSerializer::class, NestedEnumWithUseSerializer::class, ) +@file:UseContextualSerialization(EnumWithUseContextual::class, Container.NestedEnumWithUseContextual::class, ) + +import kotlinx.serialization.* +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.json.* +import kotlinx.serialization.encoding.* +import kotlinx.serialization.modules.* +import kotlin.reflect.typeOf + +enum class EnumWithDef { + A, + B, +} + +@Serializable +enum class Enum { + A, + B, +} + +@Serializable(EnumWithCustomSerializer::class) +enum class EnumWithCustom { + A, + B, +} + +@Serializable(EnumWithCustomClSerializer::class) +enum class EnumWithCustomCl { + A, + B, +} + +enum class EnumWithContextual { + A, + B, +} + +enum class EnumWithUseContextual { + A, + B, +} + +enum class EnumWithUse { + A, + B, +} + +@Serializable +@Extra("Enum1") +enum class Enum1 { + @Extra("A") A, + @Extra("B") B, +} + +class Container { + enum class NestedEnumWithDef { + A, + B, + } + + @Serializable + enum class NestedEnum { + A, + B, + } + + @Serializable(NestedEnumWithCustomSerializer::class) + enum class NestedEnumWithCustom { + A, + B, + } + + @Serializable(NestedEnumWithCustomClSerializer::class) + enum class NestedEnumWithCustomCl { + A, + B, + } + + enum class NestedEnumWithContextual { + A, + B, + } + + enum class NestedEnumWithUseContextual { + A, + B, + } + + enum class NestedEnumWithUse { + A, + B, + } + +} + +class Outer { +} + +object EnumWithCustomSerializer: ToDoSerializer("custom|EnumWithCustom") +object NestedEnumWithCustomSerializer: ToDoSerializer("custom|Container.NestedEnumWithCustom") +class EnumWithCustomClSerializer: ToDoSerializer("custom|EnumWithCustomCl") +class NestedEnumWithCustomClSerializer: ToDoSerializer("custom|Container.NestedEnumWithCustomCl") + +object EnumWithContextualSerializer: ToDoSerializer("contextual|EnumWithContextual") +object NestedEnumWithContextualSerializer: ToDoSerializer("contextual|Container.NestedEnumWithContextual") +object EnumWithUseContextualSerializer: ToDoSerializer("contextual|EnumWithUseContextual") +object NestedEnumWithUseContextualSerializer: ToDoSerializer("contextual|Container.NestedEnumWithUseContextual") + +class EnumWithUseSerializer: ToDoSerializer("useSerializer|EnumWithUse") +class NestedEnumWithUseSerializer: ToDoSerializer("useSerializer|Container.NestedEnumWithUse") + +@kotlinx.serialization.SerialInfo +@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY) +annotation class Extra(val value: String) + +fun box(): String { + val module = SerializersModule { + contextual(EnumWithContextualSerializer) + contextual(NestedEnumWithContextualSerializer) + contextual(EnumWithUseContextualSerializer) + contextual(NestedEnumWithUseContextualSerializer) + } + + serializer().checkElements("A", "B") + serializer().checkElements("A", "B") + serializer().checkElements("A", "B") + serializer().checkElements("A", "B") + serializer().checkElements("A", "B") + serializer().checkElements("A", "B") + serializer().checkElements("A", "B") + serializer().checkElements("A", "B") + serializer().checkElements("A", "B") + serializer().checkElements("A", "B") + serializer().checkElements("A", "B") + serializer().checkElements("A", "B") + serializer().checkElements("A", "B") + serializer().checkElements("A", "B") + serializer().checkElements("A", "B") + + // Call serializer factory function in companion + Enum.serializer().checkSerialName("Enum")?.let { return it } + Container.NestedEnum.serializer().checkSerialName("Container.NestedEnum")?.let { return it } + EnumWithCustom.serializer().checkSerialName("custom|EnumWithCustom")?.let { return it } + Container.NestedEnumWithCustom.serializer().checkSerialName("custom|Container.NestedEnumWithCustom")?.let { return it } + EnumWithCustomCl.serializer().checkSerialName("custom|EnumWithCustomCl")?.let { return it } + Container.NestedEnumWithCustomCl.serializer().checkSerialName("custom|Container.NestedEnumWithCustomCl")?.let { return it } + Enum1.serializer().checkSerialName("Enum1")?.let { return it } + + // Serializer lookup by generic parameter + serializer().checkSerialName("EnumWithDef")?.let { return it } + serializer().checkSerialName("Container.NestedEnumWithDef")?.let { return it } + serializer().checkSerialName("Enum")?.let { return it } + serializer().checkSerialName("Container.NestedEnum")?.let { return it } + serializer().checkSerialName("custom|EnumWithCustom")?.let { return it } + serializer().checkSerialName("custom|Container.NestedEnumWithCustom")?.let { return it } + serializer().checkSerialName("custom|EnumWithCustomCl")?.let { return it } + serializer().checkSerialName("custom|Container.NestedEnumWithCustomCl")?.let { return it } + // generated serializer used in lookup for the empty module in case of specifying of @file:UseContextualSerialization or @UseSerializers + serializer().checkSerialName("EnumWithUseContextual")?.let { return it } + // generated serializer used in lookup for the empty module in case of specifying of @file:UseContextualSerialization or @UseSerializers + serializer().checkSerialName("Container.NestedEnumWithUseContextual")?.let { return it } + // generated serializer used in lookup for the empty module in case of specifying of @file:UseContextualSerialization or @UseSerializers + serializer().checkSerialName("EnumWithUse")?.let { return it } + // generated serializer used in lookup for the empty module in case of specifying of @file:UseContextualSerialization or @UseSerializers + serializer().checkSerialName("Container.NestedEnumWithUse")?.let { return it } + serializer().checkSerialName("Enum1")?.let { return it } + + // Serializer lookup by typeOf function + serializer(typeOf()).checkSerialName("EnumWithDef")?.let { return it } + serializer(typeOf()).checkSerialName("Container.NestedEnumWithDef")?.let { return it } + serializer(typeOf()).checkSerialName("Enum")?.let { return it } + serializer(typeOf()).checkSerialName("Container.NestedEnum")?.let { return it } + serializer(typeOf()).checkSerialName("custom|EnumWithCustom")?.let { return it } + serializer(typeOf()).checkSerialName("custom|Container.NestedEnumWithCustom")?.let { return it } + serializer(typeOf()).checkSerialName("custom|EnumWithCustomCl")?.let { return it } + serializer(typeOf()).checkSerialName("custom|Container.NestedEnumWithCustomCl")?.let { return it } + // generated serializer used in lookup for the empty module in case of specifying of @file:UseContextualSerialization or @UseSerializers + serializer(typeOf()).checkSerialName("EnumWithUseContextual")?.let { return it } + // generated serializer used in lookup for the empty module in case of specifying of @file:UseContextualSerialization or @UseSerializers + serializer(typeOf()).checkSerialName("Container.NestedEnumWithUseContextual")?.let { return it } + // generated serializer used in lookup for the empty module in case of specifying of @file:UseContextualSerialization or @UseSerializers + serializer(typeOf()).checkSerialName("EnumWithUse")?.let { return it } + // generated serializer used in lookup for the empty module in case of specifying of @file:UseContextualSerialization or @UseSerializers + serializer(typeOf()).checkSerialName("Container.NestedEnumWithUse")?.let { return it } + serializer(typeOf()).checkSerialName("Enum1")?.let { return it } + + // Serializer lookup by generic parameter in custom module + module.serializer().checkSerialName("EnumWithDef")?.let { return it } + module.serializer().checkSerialName("Container.NestedEnumWithDef")?.let { return it } + module.serializer().checkSerialName("Enum")?.let { return it } + module.serializer().checkSerialName("Container.NestedEnum")?.let { return it } + module.serializer().checkSerialName("custom|EnumWithCustom")?.let { return it } + module.serializer().checkSerialName("custom|Container.NestedEnumWithCustom")?.let { return it } + module.serializer().checkSerialName("custom|EnumWithCustomCl")?.let { return it } + module.serializer().checkSerialName("custom|Container.NestedEnumWithCustomCl")?.let { return it } + // !!! for some reason, the generated serializer is still lookup for the custom module in case of specifying of @file:UseContextualSerialization or @UseSerializers + module.serializer().checkSerialName("EnumWithUseContextual")?.let { return it } + // !!! for some reason, the generated serializer is still lookup for the custom module in case of specifying of @file:UseContextualSerialization or @UseSerializers + module.serializer().checkSerialName("Container.NestedEnumWithUseContextual")?.let { return it } + // !!! for some reason, the generated serializer is still lookup for the custom module in case of specifying of @file:UseContextualSerialization or @UseSerializers + module.serializer().checkSerialName("EnumWithUse")?.let { return it } + // !!! for some reason, the generated serializer is still lookup for the custom module in case of specifying of @file:UseContextualSerialization or @UseSerializers + module.serializer().checkSerialName("Container.NestedEnumWithUse")?.let { return it } + module.serializer().checkSerialName("Enum1")?.let { return it } + + // Serializer lookup by typeOf function in custom module + module.serializer(typeOf()).checkSerialName("EnumWithDef")?.let { return it } + module.serializer(typeOf()).checkSerialName("Container.NestedEnumWithDef")?.let { return it } + module.serializer(typeOf()).checkSerialName("Enum")?.let { return it } + module.serializer(typeOf()).checkSerialName("Container.NestedEnum")?.let { return it } + module.serializer(typeOf()).checkSerialName("custom|EnumWithCustom")?.let { return it } + module.serializer(typeOf()).checkSerialName("custom|Container.NestedEnumWithCustom")?.let { return it } + module.serializer(typeOf()).checkSerialName("custom|EnumWithCustomCl")?.let { return it } + module.serializer(typeOf()).checkSerialName("custom|Container.NestedEnumWithCustomCl")?.let { return it } + // !!! for some reason, the generated serializer is still lookup for the custom module in case of specifying of @file:UseContextualSerialization or @UseSerializers + module.serializer(typeOf()).checkSerialName("EnumWithUseContextual")?.let { return it } + // !!! for some reason, the generated serializer is still lookup for the custom module in case of specifying of @file:UseContextualSerialization or @UseSerializers + module.serializer(typeOf()).checkSerialName("Container.NestedEnumWithUseContextual")?.let { return it } + // !!! for some reason, the generated serializer is still lookup for the custom module in case of specifying of @file:UseContextualSerialization or @UseSerializers + module.serializer(typeOf()).checkSerialName("EnumWithUse")?.let { return it } + // !!! for some reason, the generated serializer is still lookup for the custom module in case of specifying of @file:UseContextualSerialization or @UseSerializers + module.serializer(typeOf()).checkSerialName("Container.NestedEnumWithUse")?.let { return it } + module.serializer(typeOf()).checkSerialName("Enum1")?.let { return it } + + // Annotation on type should have value same as a class name + serializer().checkAnnotation("Enum1") + + // Annotation on enum entries should have value same as a entry names + serializer().checkElementAnnotations("A", "B") + + return "OK" +} + +fun KSerializer<*>.checkSerialName(name: String): String? = if (descriptor.serialName != name) "Wrong serial name: Expected '$name' actual '${descriptor.serialName}'" else null +fun KSerializer<*>.checkAnnotation(value: String): String? = descriptor.annotations.filterIsInstance().single().value.let { if (it != value) it else null } + +fun KSerializer<*>.checkElementAnnotations(vararg values: String): String? = (0 ..< descriptor.elementsCount).map { descriptor.getElementAnnotations(it).filterIsInstance().single().value}.let { if (it != values.toList()) it.toString() else null } + +fun KSerializer<*>.checkElements(vararg values: String): String? = (0 ..< descriptor.elementsCount).map { descriptor.getElementName(it) }.let { if (it != values.toList()) it.toString() else 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() +} + diff --git a/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/matrix/FirTestMatrixGenerated.java b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/matrix/FirTestMatrixGenerated.java new file mode 100644 index 00000000000..1ce2c621a87 --- /dev/null +++ b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/matrix/FirTestMatrixGenerated.java @@ -0,0 +1,33 @@ +/* + * Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlinx.serialization.matrix; + +import com.intellij.testFramework.TestDataPath; +import org.jetbrains.kotlin.test.util.KtTestUtil; +import org.jetbrains.kotlin.test.TargetBackend; +import org.jetbrains.kotlin.test.TestMetadata; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.regex.Pattern; + +/** This class is generated by {@link org.jetbrains.kotlinx.serialization.TestGeneratorKt}. DO NOT MODIFY MANUALLY */ +@SuppressWarnings("all") +@TestMetadata("plugins/kotlinx-serialization/testData/matrix") +@TestDataPath("$PROJECT_ROOT") +public class FirTestMatrixGenerated extends AbstractFirTestMatrix { + @Test + public void testAllFilesPresentInMatrix() throws Exception { + KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("plugins/kotlinx-serialization/testData/matrix"), Pattern.compile("^(.+)\\.kt$"), null, TargetBackend.JVM_IR, true); + } + + @Test + @TestMetadata("enums.kt") + public void testEnums() throws Exception { + runTest("plugins/kotlinx-serialization/testData/matrix/enums.kt"); + } +} diff --git a/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/matrix/TestMatrixGenerated.java b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/matrix/TestMatrixGenerated.java new file mode 100644 index 00000000000..945f5581223 --- /dev/null +++ b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/matrix/TestMatrixGenerated.java @@ -0,0 +1,33 @@ +/* + * Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlinx.serialization.matrix; + +import com.intellij.testFramework.TestDataPath; +import org.jetbrains.kotlin.test.util.KtTestUtil; +import org.jetbrains.kotlin.test.TargetBackend; +import org.jetbrains.kotlin.test.TestMetadata; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.regex.Pattern; + +/** This class is generated by {@link org.jetbrains.kotlinx.serialization.TestGeneratorKt}. DO NOT MODIFY MANUALLY */ +@SuppressWarnings("all") +@TestMetadata("plugins/kotlinx-serialization/testData/matrix") +@TestDataPath("$PROJECT_ROOT") +public class TestMatrixGenerated extends AbstractTestMatrix { + @Test + public void testAllFilesPresentInMatrix() throws Exception { + KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("plugins/kotlinx-serialization/testData/matrix"), Pattern.compile("^(.+)\\.kt$"), null, TargetBackend.JVM_IR, true); + } + + @Test + @TestMetadata("enums.kt") + public void testEnums() throws Exception { + runTest("plugins/kotlinx-serialization/testData/matrix/enums.kt"); + } +} diff --git a/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/TestGenerator.kt b/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/TestGenerator.kt index 85531edf45d..3f4ae83acb0 100644 --- a/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/TestGenerator.kt +++ b/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/TestGenerator.kt @@ -7,6 +7,8 @@ package org.jetbrains.kotlinx.serialization import org.jetbrains.kotlin.generators.generateTestGroupSuiteWithJUnit5 import org.jetbrains.kotlin.generators.util.TestGeneratorUtil +import org.jetbrains.kotlinx.serialization.matrix.cases.enumsTestMatrix +import org.jetbrains.kotlinx.serialization.matrix.testMatrix import org.jetbrains.kotlinx.serialization.runners.* fun main(args: Array) { @@ -77,6 +79,10 @@ fun main(args: Array) { testClass { model("boxIr") } + + testMatrix { + add("enums") { enumsTestMatrix() } + } } } } diff --git a/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/matrix/CombinationContext.kt b/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/matrix/CombinationContext.kt new file mode 100644 index 00000000000..18f44751417 --- /dev/null +++ b/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/matrix/CombinationContext.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlinx.serialization.matrix + + +interface CombinationContext { + fun defineEnums( + serializers: Set, + locations: Set, + optionsConfig: EnumOptionsBuilder.() -> Unit = {}, + ): Set + + fun function(name: String, block: FunctionContext.() -> Unit) + + fun box(block: FunctionContext.() -> Unit) + + fun generate(appendable: Appendable, generator: String) + + val TypeVariant.named: NamedTypeVariant +} + + +interface FunctionContext { + fun line(code: String = "") +} + +interface EnumOptionsBuilder { + fun serialInfo(vararg serialInfo: SerialInfo) + fun descriptorAccessing(vararg descriptorAccessing: DescriptorAccessing) + + fun entries(vararg entries: String) +} + + diff --git a/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/matrix/SerializationFunctions.kt b/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/matrix/SerializationFunctions.kt new file mode 100644 index 00000000000..8e634bc6b33 --- /dev/null +++ b/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/matrix/SerializationFunctions.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlinx.serialization.matrix + +import org.jetbrains.kotlinx.serialization.matrix.SerializerKind.BY_DEFAULT + +internal fun hasFactoryFun(type: TypeVariant): Boolean { + return type.features.serializer in setOf( + SerializerKind.GENERATED, + SerializerKind.CUSTOM_OBJECT, + SerializerKind.CUSTOM_CLASS + ) +} + +internal fun canBeUsedInLookup(type: TypeVariant): Boolean { + return type.features.serializer in setOf( + SerializerKind.GENERATED, + SerializerKind.CUSTOM_OBJECT, + SerializerKind.CUSTOM_CLASS, + SerializerKind.CLASS_USE_SERIALIZER, + SerializerKind.USE_CONTEXTUAL + ) || (type is EnumVariant && type.features.serializer == BY_DEFAULT) +} +internal fun canBeUsedInModuleLookup(type: TypeVariant): Boolean { + return type.features.serializer in setOf( + SerializerKind.GENERATED, + SerializerKind.CUSTOM_OBJECT, + SerializerKind.CUSTOM_CLASS, + SerializerKind.CLASS_USE_SERIALIZER, + SerializerKind.USE_CONTEXTUAL + ) || (type is EnumVariant && type.features.serializer == BY_DEFAULT) +} + +internal fun shouldAddContextualSerializerToModule(type: TypeVariant): Boolean { + return type.features.serializer == SerializerKind.CONTEXTUAL || type.features.serializer == SerializerKind.USE_CONTEXTUAL +} + +internal fun hasAnnotationOnType(type: TypeVariant): Boolean { + return type is EnumVariant && SerialInfo.ON_TYPE in type.options.serialInfo +} + +internal fun hasAnnotationOnElement(type: TypeVariant): Boolean { + return type is EnumVariant && SerialInfo.ON_ELEMENTS in type.options.serialInfo +} diff --git a/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/matrix/TestMatrixIntegration.kt b/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/matrix/TestMatrixIntegration.kt new file mode 100644 index 00000000000..fe319c72c36 --- /dev/null +++ b/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/matrix/TestMatrixIntegration.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlinx.serialization.matrix + +import org.jetbrains.kotlin.generators.TestGroup +import org.jetbrains.kotlin.incremental.deleteDirectoryContents +import org.jetbrains.kotlin.test.builders.TestConfigurationBuilder +import org.jetbrains.kotlin.test.runners.codegen.AbstractFirLightTreeBlackBoxCodegenTest +import org.jetbrains.kotlin.test.runners.codegen.AbstractIrBlackBoxCodegenTest +import org.jetbrains.kotlinx.serialization.matrix.impl.CombinationContextImpl +import org.jetbrains.kotlinx.serialization.configureForKotlinxSerialization +import java.io.File + +internal fun TestGroup.testMatrix(casesBlock: TestCaseContext.() -> Unit) { + val relativeRootPath = "matrix" + val dir = File("$testDataRoot/$relativeRootPath") + dir.mkdirs() + dir.deleteDirectoryContents() + + val caseContext = TestCaseContext() + caseContext.casesBlock() + + caseContext.testCases.forEach { (caseName, block) -> + val combinationContext = CombinationContextImpl() + combinationContext.block() + combinationContext.generateInto(dir.resolve("$caseName.kt")) + } + + testClass { + model(relativeRootPath) + } + testClass { + model(relativeRootPath) + } +} + +internal class TestCaseContext { + val testCases: MutableMap Unit> = mutableMapOf() + + fun add(name: String, block: CombinationContext.() -> Unit) { + testCases[name] = block + } +} + +internal open class AbstractTestMatrix : AbstractIrBlackBoxCodegenTest() { + override fun configure(builder: TestConfigurationBuilder) { + super.configure(builder) + builder.configureForKotlinxSerialization() + } +} + +internal open class AbstractFirTestMatrix : AbstractFirLightTreeBlackBoxCodegenTest() { + override fun configure(builder: TestConfigurationBuilder) { + super.configure(builder) + builder.configureForKotlinxSerialization() + } +} + +private fun CombinationContext.generateInto(file: File) { + file.writer().use { + generate(it, "TestMatrixIntegration.kt") + } +} diff --git a/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/matrix/Types.kt b/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/matrix/Types.kt new file mode 100644 index 00000000000..8101256e5b0 --- /dev/null +++ b/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/matrix/Types.kt @@ -0,0 +1,140 @@ +/* + * Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlinx.serialization.matrix + +import org.jetbrains.kotlinx.serialization.matrix.impl.CLASS_FOR_NESTED + + +internal fun Set.filterEnums(predicate: (EnumVariant) -> Boolean): Set { + val filtered = filterIsInstance().filter { variant -> predicate(variant) } + return filtered.toSet() +} + +/** + * A combination of mutually exclusive Kotlin language and serialization features. + */ +sealed class TypeFeatures { + abstract val serializer: SerializerKind + abstract val location: TypeLocation +} + +/** + * Description of the type used to generate type definitions and their uses. + * + * Parent type. + */ +sealed class TypeVariant { + /** + * A combination of mutually exclusive Kotlin language and serialization features. + */ + abstract val features: TypeFeatures +} + +/** + * Description of the enum types used to generate type definitions and their uses. + */ +data class EnumVariant( + override val features: EnumFeatures, + val options: EnumOptions, +) : TypeVariant() + +/** + * A combination of mutually exclusive Kotlin language and serialization features for enum types. + */ +data class EnumFeatures( + override val serializer: SerializerKind, + override val location: TypeLocation, +) : TypeFeatures() + +/** + * Optional features for the type. + */ +data class EnumOptions( + val serialInfo: Set, + val descriptorAccessing: Set, + val entries: Set, +) + + +enum class SerializerKind { + /** + * Serializable by default (Without @Serializable annotation: enum, interface, sealed interface) + */ + BY_DEFAULT, + + /** + * @Serializable + */ + GENERATED, + + /** + * @Serializable(CustomObjectSerializer::class) + */ + CUSTOM_OBJECT, + + /** + * @Serializable(CustomSerializer::class) + */ + CUSTOM_CLASS, + + /** + * Contextual by SerialModule. + */ + CONTEXTUAL, + + /** + * Classes marked by file-level annotation @file:UseContextualSerialization + */ + USE_CONTEXTUAL, + + /** + * Serializable by @UseSerializers + */ + CLASS_USE_SERIALIZER +} + +/** + * Location of the type definition. + */ +enum class TypeLocation { + FILE_ROOT, + LOCAL, + NESTED, +} + +enum class SerialInfo { + ON_TYPE, + ON_ELEMENTS +} + + +/** + * Kotlin and serialization feature that are not mutually exclusive and there may be several features of the same kind. + */ +interface TypeOptionalFeature + +enum class DescriptorAccessing : TypeOptionalFeature { + FROM_INIT, + FROM_COMPANION_INIT, + FROM_COMPANION_PROPERTY_INIT +} + + + + +/** + * A pair in the description of the type and the name that was assigned to it during generation. + */ +class NamedTypeVariant(val name: String, val variant: TypeVariant) { + internal val classUsage: String + get() { + return when (variant.features.location) { + TypeLocation.FILE_ROOT -> name + TypeLocation.LOCAL -> name + TypeLocation.NESTED -> "$CLASS_FOR_NESTED.$name" + } + } +} diff --git a/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/matrix/cases/EnumsTestMatrix.kt b/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/matrix/cases/EnumsTestMatrix.kt new file mode 100644 index 00000000000..7f5d31a578d --- /dev/null +++ b/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/matrix/cases/EnumsTestMatrix.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlinx.serialization.matrix.cases + +import org.jetbrains.kotlinx.serialization.matrix.SerializerKind.* +import org.jetbrains.kotlinx.serialization.matrix.TypeLocation.FILE_ROOT +import org.jetbrains.kotlinx.serialization.matrix.TypeLocation.NESTED +import org.jetbrains.kotlinx.serialization.matrix.* +import org.jetbrains.kotlinx.serialization.matrix.impl.* +import org.jetbrains.kotlinx.serialization.matrix.impl.elements +import org.jetbrains.kotlinx.serialization.matrix.impl.serialName + +fun CombinationContext.enumsTestMatrix() { + val enums = defineEnums( + SerializerKind.entries.toSet(), + setOf(FILE_ROOT, NESTED) + ) { + entries("A", "B") + descriptorAccessing(*DescriptorAccessing.entries.toTypedArray()) + } + + val enumsWithAnnotations = defineEnums( + setOf(GENERATED), + setOf(FILE_ROOT) + ) { + entries("A", "B") + serialInfo(*SerialInfo.entries.toTypedArray()) + } + + val allTypes = enums + enumsWithAnnotations + + val withCompanion = allTypes.filter(::hasFactoryFun) + val lookupTypes = allTypes.filter(::canBeUsedInLookup) + + val moduleLookupTypes = allTypes.filter(::canBeUsedInModuleLookup) + + val annotatedTypes = allTypes.filter(::hasAnnotationOnType) + val annotatedElementsTypes = allTypes.filter(::hasAnnotationOnElement) + + box() { + line("val module = SerializersModule {") + allTypes.filter(::shouldAddContextualSerializerToModule).forEach { type -> + line(" contextual(${type.named.serializerName})") + } + line("}") + line() + + allTypes.forEach { type -> + line("serializer<${type.named.classUsage}>().checkElements(${ + type.elements.joinToString(", ") { "\"$it\"" } + })") + } + line() + + line("// Call serializer factory function in companion") + withCompanion.forEach { type -> + line("${type.named.classUsage}.serializer().checkSerialName(\"${type.named.serialName}\")?.let { return it }") + } + line() + + line("// Serializer lookup by generic parameter") + lookupTypes.forEach { type -> + val serialName = if (type.features.serializer == CLASS_USE_SERIALIZER || type.features.serializer == USE_CONTEXTUAL) { + line("// generated serializer used in lookup for the empty module in case of specifying of @file:UseContextualSerialization or @UseSerializers") + type.named.classUsage + } else { + type.named.serialName + } + line("serializer<${type.named.classUsage}>().checkSerialName(\"$serialName\")?.let { return it }") + } + line() + + line("// Serializer lookup by typeOf function") + lookupTypes.forEach { type -> + val serialName = if (type.features.serializer == CLASS_USE_SERIALIZER || type.features.serializer == USE_CONTEXTUAL) { + line("// generated serializer used in lookup for the empty module in case of specifying of @file:UseContextualSerialization or @UseSerializers") + type.named.classUsage + } else { + type.named.serialName + } + line("serializer(typeOf<${type.named.classUsage}>()).checkSerialName(\"$serialName\")?.let { return it }") + } + line() + + line("// Serializer lookup by generic parameter in custom module") + moduleLookupTypes.forEach { type -> + val serialName = if (type.features.serializer == CLASS_USE_SERIALIZER || type.features.serializer == USE_CONTEXTUAL) { + line("// !!! for some reason, the generated serializer is still lookup for the custom module in case of specifying of @file:UseContextualSerialization or @UseSerializers") + type.named.classUsage + } else { + type.named.serialName + } + line("module.serializer<${type.named.classUsage}>().checkSerialName(\"$serialName\")?.let { return it }") + } + line() + + line("// Serializer lookup by typeOf function in custom module") + moduleLookupTypes.forEach { type -> + val serialName = if (type.features.serializer == CLASS_USE_SERIALIZER || type.features.serializer == USE_CONTEXTUAL) { + line("// !!! for some reason, the generated serializer is still lookup for the custom module in case of specifying of @file:UseContextualSerialization or @UseSerializers") + type.named.classUsage + } else { + type.named.serialName + } + line("module.serializer(typeOf<${type.named.classUsage}>()).checkSerialName(\"$serialName\")?.let { return it }") + } + line() + + line("// Annotation on type should have value same as a class name") + annotatedTypes.forEach { type -> + line("serializer<${type.named.classUsage}>().checkAnnotation(\"${type.named.classUsage}\")") + } + line() + + line("// Annotation on enum entries should have value same as a entry names") + annotatedElementsTypes.forEach { type -> + line("serializer<${type.named.classUsage}>().checkElementAnnotations(${ + type.elements.joinToString(", ") { "\"$it\"" } + })") + } + line() + } +} \ No newline at end of file diff --git a/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/matrix/impl/CombinationContextImpl.kt b/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/matrix/impl/CombinationContextImpl.kt new file mode 100644 index 00000000000..036602e199a --- /dev/null +++ b/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/matrix/impl/CombinationContextImpl.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlinx.serialization.matrix.impl + +import org.jetbrains.kotlinx.serialization.matrix.* + +internal class CombinationContextImpl : CombinationContext { + private val typesByFeatures = mutableMapOf>() + private val types: MutableMap = mutableMapOf() + private val functions: MutableMap Unit> = mutableMapOf() + + override fun defineEnums( + serializers: Set, + locations: Set, + optionsConfig: EnumOptionsBuilder.() -> Unit, + ): Set { + val enumTypes: MutableSet = mutableSetOf() + val options = EnumOptionsBuilderImpl().also { it.optionsConfig() }.build() + for (serializer in serializers) { + for (location in locations) { + val features = EnumFeatures(serializer, location) + enumTypes += EnumVariant(features, options) + } + } + addTypes(enumTypes) + return enumTypes + } + + override fun function(name: String, block: FunctionContext.() -> Unit) { + functions[name] = block + } + + override fun box(block: FunctionContext.() -> Unit) { + functions["box(): String"] = { + block() + line("return \"OK\"") + } + } + + override fun generate(appendable: Appendable, generator: String) { + val namedTypes = types.map { (type, name) -> NamedTypeVariant(name, type) } + appendable.writeHeader(namedTypes, generator) + appendable.writeTypes(namedTypes) + + functions.forEach { (signature, builder) -> + appendable.writeFunction(signature, builder) + } + + appendable.writeUtils() + } + + override val TypeVariant.named: NamedTypeVariant + get() = NamedTypeVariant(types[this] ?: throw Exception("Type variant wasn't defined properly $this"), this) + + private fun addTypes(types: Iterable) { + for (type in types) { + val typesForFeatures = typesByFeatures.getOrPut(type.features) { mutableListOf() } + val className = type.className + if (typesForFeatures.size > 0) typesForFeatures.size.toString() else "" + typesForFeatures += type + this.types[type] = className + } + } +} + +private class EnumOptionsBuilderImpl : EnumOptionsBuilder { + private val serialInfoP: MutableSet = mutableSetOf() + private val descriptorAccessingP: MutableSet = mutableSetOf() + private val entriesP: MutableSet = mutableSetOf() + + + override fun serialInfo(vararg serialInfo: SerialInfo) { + serialInfo.forEach { serialInfoP.add(it) } + } + + override fun descriptorAccessing(vararg descriptorAccessing: DescriptorAccessing) { + descriptorAccessing.forEach { descriptorAccessingP.add(it) } + } + + override fun entries(vararg entries: String) { + entries.forEach { entriesP.add(it) } + } + + fun build(): EnumOptions { + return EnumOptions(serialInfoP, descriptorAccessingP, entriesP) + } + +} \ No newline at end of file diff --git a/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/matrix/impl/Functions.kt b/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/matrix/impl/Functions.kt new file mode 100644 index 00000000000..0871f1b3db9 --- /dev/null +++ b/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/matrix/impl/Functions.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlinx.serialization.matrix.impl + +import org.jetbrains.kotlinx.serialization.matrix.FunctionContext + +internal fun Appendable.writeFunction(signature: String, builder: FunctionContext.() -> Unit) { + appendLine("fun $signature {") + + val context = object : FunctionContext { + override fun line(code: String) { + append(" ") + appendLine(code) + } + } + + context.builder() + + appendLine("}") + appendLine() +} \ No newline at end of file diff --git a/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/matrix/impl/Generation.kt b/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/matrix/impl/Generation.kt new file mode 100644 index 00000000000..0a87a434bd6 --- /dev/null +++ b/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/matrix/impl/Generation.kt @@ -0,0 +1,234 @@ +/* + * Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlinx.serialization.matrix.impl + +import org.jetbrains.kotlinx.serialization.matrix.EnumVariant +import org.jetbrains.kotlinx.serialization.matrix.NamedTypeVariant +import org.jetbrains.kotlinx.serialization.matrix.SerialInfo +import org.jetbrains.kotlinx.serialization.matrix.SerializerKind.* +import org.jetbrains.kotlinx.serialization.matrix.TypeLocation.* + +/* + + + +class Container { + +} + +class Outer { + +} + +fun () { + + +} + +*/ +internal fun Appendable.writeTypes(types: List) { + // file-level + types.filter { type -> type.variant.features.location == FILE_ROOT }.forEach { type -> + writeTypeDef(type) + } + + // nested + append("class ") + append(CLASS_FOR_NESTED) + append(" {\n") + types.filter { named -> named.variant.features.location == NESTED }.forEach { type -> + writeTypeDef(type, " ") + } + append("}\n\n") + + // write inner + append("class ") + append(CLASS_FOR_INNER) + append(" {\n") + types.filter { named -> named.variant.features.location == LOCAL }.forEach { type -> + writeTypeDef(type, " ") + } + append("}\n\n") + + // write custom serializers + types.forEach { type -> + writeCustomSerializer(type) + } + + appendLine() + + // write contextual serializers + types.forEach { type -> + writeContextualSerializer(type) + } + + appendLine() + + // write use serializers + types.forEach { type -> + writeUseSerializer(type) + } + + appendLine() + + writeSerialInfo() + + appendLine() +} + +internal fun Appendable.writeHeader(types: List, generator: String) { + appendLine("// WITH_STDLIB") + appendLine() + appendLine("//") + appendLine("// NOTE: THIS FILE IS AUTO-GENERATED by the $generator, DO NOT EDIT!") + appendLine("//") + appendLine() + writeUseSerializers(types) + writeUseContextualSerializers(types) + appendLine() + appendLine("import kotlinx.serialization.*") + appendLine("import kotlinx.serialization.descriptors.*") + appendLine("import kotlinx.serialization.json.*") + appendLine("import kotlinx.serialization.encoding.*") + appendLine("import kotlinx.serialization.modules.*") + appendLine("import kotlin.reflect.typeOf") + appendLine() +} + +internal fun Appendable.writeUtils() { + append("fun KSerializer<*>.checkSerialName(name: String): String? = if (descriptor.serialName != name) \"Wrong serial name: Expected '\$name' actual '\${descriptor.serialName}'\" else null\n") + append("fun KSerializer<*>.checkAnnotation(value: String): String? = descriptor.annotations.filterIsInstance().single().value.let { if (it != value) it else null }\n\n") + append("fun KSerializer<*>.checkElementAnnotations(vararg values: String): String? = (0 ..< descriptor.elementsCount).map { descriptor.getElementAnnotations(it).filterIsInstance().single().value}.let { if (it != values.toList()) it.toString() else null }\n\n") + append("fun KSerializer<*>.checkElements(vararg values: String): String? = (0 ..< descriptor.elementsCount).map { descriptor.getElementName(it) }.let { if (it != values.toList()) it.toString() else null }\n\n") + + // write abstract serializer + appendLine(TODO_SERIALIZER) +} + + + + +private fun Appendable.writeTypeDef(named: NamedTypeVariant, indent: String = "") { + when (named.variant) { + is EnumVariant -> writeEnumDef(named, indent) + } +} + +private fun Appendable.writeEnumDef(named: NamedTypeVariant, indent: String) { + val enum = named.variant as EnumVariant + + if (enum.features.location == LOCAL) { + throw IllegalArgumentException("Local enums are not allowed, can't generate test code.") + } + + val classUsage = named.classUsage + + if (enum.features.serializer == GENERATED) { + append(indent) + appendLine("@Serializable") + } + if (enum.features.serializer == CUSTOM_OBJECT || enum.features.serializer == CUSTOM_CLASS) { + append(indent) + appendLine("@Serializable(${named.serializerName}::class)") + } + + if (SerialInfo.ON_TYPE in enum.options.serialInfo) { + append(indent) + appendLine("@$SERIAL_ANNOTATION(\"$classUsage\")") + } + + append(indent) + appendLine("enum class ${named.name} {") + enum.options.entries.forEach { entry -> + append(indent) + append(" ") + + if (SerialInfo.ON_ELEMENTS in enum.options.serialInfo) { + append("@$SERIAL_ANNOTATION(\"$entry\") ") + } + append(entry) + appendLine(",") + } + + append(indent) + appendLine("}") + appendLine() +} + + +private fun Appendable.writeUseSerializers(types: List) { + val serializers = types.mapNotNull { type -> type.useSerializer } + if (serializers.isEmpty()) { + return + } + append("@file:UseSerializers(") + serializers.forEach { name -> + append(name) + append("::class, ") + } + appendLine(")") +} + +private fun Appendable.writeUseContextualSerializers(types: List) { + val types = types.filter { type -> type.useContextualSerializer != null } + if (types.isEmpty()) { + return + } + append("@file:UseContextualSerialization(") + types.forEach { type -> + append(type.classUsage) + append("::class, ") + } + appendLine(")") +} + +private fun Appendable.writeSerialInfo() { + appendLine("@kotlinx.serialization.SerialInfo") + appendLine("@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)") + appendLine("annotation class $SERIAL_ANNOTATION(val value: String)") +} + +private fun Appendable.writeCustomSerializer(type: NamedTypeVariant) { + when (type.variant.features.serializer) { + CUSTOM_CLASS -> { + appendLine("class ${type.serializerName}: ToDoSerializer<${type.classUsage}>(\"${type.serialName}\")") + } + CUSTOM_OBJECT -> { + appendLine("object ${type.serializerName}: ToDoSerializer<${type.classUsage}>(\"${type.serialName}\")") + } + else -> Unit // no-op + } +} + +private fun Appendable.writeContextualSerializer(type: NamedTypeVariant) { + when (type.variant.features.serializer) { + CONTEXTUAL, USE_CONTEXTUAL -> { + val usage = type.classUsage + appendLine("object ${type.serializerName}: ToDoSerializer<$usage>(\"${type.serialName}\")") + } + else -> Unit // no-op + } +} + +private fun Appendable.writeUseSerializer(type: NamedTypeVariant) { + when (type.variant.features.serializer) { + CLASS_USE_SERIALIZER -> { + val usage = type.classUsage + appendLine("class ${type.useSerializer}: ToDoSerializer<$usage>(\"${type.serialName}\")") + } + else -> Unit // no-op + } +} + +private const val TODO_SERIALIZER_NAME = "ToDoSerializer" + +private const val TODO_SERIALIZER = """ +abstract class $TODO_SERIALIZER_NAME(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/org/jetbrains/kotlinx/serialization/matrix/impl/Names.kt b/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/matrix/impl/Names.kt new file mode 100644 index 00000000000..cb3beeb1c22 --- /dev/null +++ b/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/matrix/impl/Names.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlinx.serialization.matrix.impl + +import org.jetbrains.kotlinx.serialization.matrix.* +import org.jetbrains.kotlinx.serialization.matrix.SerializerKind.* + +internal const val CLASS_FOR_NESTED = "Container" + +internal const val CLASS_FOR_INNER = "Outer" + +internal const val SERIAL_ANNOTATION = "Extra" + + +internal val TypeVariant.className: String + get() = when (this) { + is EnumVariant -> features.location.namePart + "Enum" + features.serializer.namePart + } + +internal val TypeVariant.elements: List + get() = when (this) { + is EnumVariant -> options.entries.toList() + } + +internal val NamedTypeVariant.serialName: String + get() = when (variant.features.serializer) { + CUSTOM_CLASS, CUSTOM_OBJECT -> "custom|$classUsage" + CONTEXTUAL, USE_CONTEXTUAL -> "contextual|$classUsage" + CLASS_USE_SERIALIZER -> "useSerializer|$classUsage" + else -> classUsage + } + + +internal val NamedTypeVariant.useSerializer: String? + get() { + if (variant.features.serializer != CLASS_USE_SERIALIZER) return null + return serializerName + } + +internal val NamedTypeVariant.useContextualSerializer: String? + get() { + if (variant.features.serializer != USE_CONTEXTUAL) return null + return serializerName + } + +internal val NamedTypeVariant.serializerName: String + get() = when (variant.features.serializer) { + CUSTOM_CLASS, CUSTOM_OBJECT, CLASS_USE_SERIALIZER, CONTEXTUAL, USE_CONTEXTUAL -> name + "Serializer" + BY_DEFAULT -> throw IllegalStateException("No named serializer for type serializable by default '$name'") + GENERATED -> throw IllegalStateException("No named serializer for type '$name' with automatically generated serializer") + } + +private val TypeLocation.namePart: String + get() = when (this) { + TypeLocation.FILE_ROOT -> "" + TypeLocation.LOCAL -> "Local" + TypeLocation.NESTED -> "Nested" + } + +private val SerializerKind.namePart: String + get() = when (this) { + BY_DEFAULT -> "WithDef" + GENERATED -> "" + CUSTOM_OBJECT -> "WithCustom" + CUSTOM_CLASS -> "WithCustomCl" + CONTEXTUAL -> "WithContextual" + USE_CONTEXTUAL -> "WithUseContextual" + CLASS_USE_SERIALIZER -> "WithUse" + }