From c4939a74465bd530283e9bf6d1c08bc3cf707753 Mon Sep 17 00:00:00 2001 From: Him188 Date: Wed, 25 Aug 2021 13:53:26 +0800 Subject: [PATCH] Add ConstructorCallCodegen --- .../codegen/ConstructorCallCodegenFacade.kt | 105 ++++++++++++ .../kotlin/utils/codegen/ValueCodegen.kt | 137 ++++++++++++++++ .../kotlin/utils/codegen/ValueDesc.kt | 152 ++++++++++++++++++ .../kotlin/utils/codegen/ValueDescVisitor.kt | 140 ++++++++++++++++ .../test/ConstructorCallCodegenTest.kt | 112 +++++++++++++ 5 files changed, 646 insertions(+) create mode 100644 mirai-core/src/commonTest/kotlin/utils/codegen/ConstructorCallCodegenFacade.kt create mode 100644 mirai-core/src/commonTest/kotlin/utils/codegen/ValueCodegen.kt create mode 100644 mirai-core/src/commonTest/kotlin/utils/codegen/ValueDesc.kt create mode 100644 mirai-core/src/commonTest/kotlin/utils/codegen/ValueDescVisitor.kt create mode 100644 mirai-core/src/commonTest/kotlin/utils/codegen/test/ConstructorCallCodegenTest.kt diff --git a/mirai-core/src/commonTest/kotlin/utils/codegen/ConstructorCallCodegenFacade.kt b/mirai-core/src/commonTest/kotlin/utils/codegen/ConstructorCallCodegenFacade.kt new file mode 100644 index 000000000..796bf8bf4 --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/utils/codegen/ConstructorCallCodegenFacade.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2019-2021 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +package net.mamoe.mirai.internal.utils.codegen + +import kotlinx.serialization.Serializable +import net.mamoe.mirai.utils.cast +import kotlin.reflect.KParameter +import kotlin.reflect.KProperty1 +import kotlin.reflect.KType +import kotlin.reflect.full.declaredMemberProperties +import kotlin.reflect.full.hasAnnotation +import kotlin.reflect.full.primaryConstructor +import kotlin.reflect.full.valueParameters +import kotlin.reflect.typeOf + +object ConstructorCallCodegenFacade { + /** + * Analyze [value] and give its correspondent [ValueDesc]. + */ + fun analyze(value: Any?, type: KType): ValueDesc { + if (value == null) return PlainValueDesc("null", null) + + val clazz = value::class + + if (clazz.isData || clazz.hasAnnotation()) { + val clazz1 = value::class + val primaryConstructor = + clazz1.primaryConstructor ?: error("$value does not have a primary constructor.") + val properties = clazz1.declaredMemberProperties + + val map = mutableMapOf() + + for (valueParameter in primaryConstructor.valueParameters) { + val prop = properties.find { it.name == valueParameter.name } + ?: error("Could not find corresponding property for parameter ${valueParameter.name}") + + prop.cast>() + map[valueParameter] = analyze(prop.get(value), prop.returnType) + } + return ClassValueDesc(value, map) + } + + ArrayValueDesc.createOrNull(value, type)?.let { return it } + if (value is Collection<*>) { + return CollectionValueDesc(value, arrayType = type, elementType = type.arguments.first().type!!) + } else if (value is Map<*, *>) { + return MapValueDesc( + value.cast(), + value.cast(), + type, + type.arguments.first().type!!, + type.arguments[1].type!! + ) + } + + return when (value) { + is CharSequence -> { + PlainValueDesc('"' + value.toString() + '"', value) + } + is Char -> { + PlainValueDesc("'$value'", value) + } + else -> PlainValueDesc(value.toString(), value) + } + } + + /** + * Generate source code to construct the value represented by [desc]. + */ + fun generate(desc: ValueDesc, context: CodegenContext = CodegenContext()): String { + if (context.configuration.removeDefaultValues) { + val def = AnalyzeDefaultValuesMappingVisitor() + desc.accept(def) + desc.accept(RemoveDefaultValuesVisitor(def.mappings)) + } + + ValueCodegen(context).generate(desc) + return context.getResult() + } + + fun analyzeAndGenerate(value: Any?, type: KType, context: CodegenContext = CodegenContext()): String { + return generate(analyze(value, type), context) + } +} + +@OptIn(ExperimentalStdlibApi::class) +inline fun ConstructorCallCodegenFacade.analyze(value: T): ValueDesc { + return analyze(value, typeOf()) +} + +@OptIn(ExperimentalStdlibApi::class) +inline fun ConstructorCallCodegenFacade.analyzeAndGenerate( + value: T, + context: CodegenContext = CodegenContext() +): String { + return analyzeAndGenerate(value, typeOf(), context) +} + diff --git a/mirai-core/src/commonTest/kotlin/utils/codegen/ValueCodegen.kt b/mirai-core/src/commonTest/kotlin/utils/codegen/ValueCodegen.kt new file mode 100644 index 000000000..f8365ae9a --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/utils/codegen/ValueCodegen.kt @@ -0,0 +1,137 @@ +/* + * Copyright 2019-2021 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +package net.mamoe.mirai.internal.utils.codegen + +import net.mamoe.mirai.utils.encodeToString +import net.mamoe.mirai.utils.toUHexString + +class ValueCodegen( + val context: CodegenContext +) { + fun generate(desc: ValueDesc) { + when (desc) { + is PlainValueDesc -> generate(desc) + is ObjectArrayValueDesc -> generate(desc) + is PrimitiveArrayValueDesc -> generate(desc) + is CollectionValueDesc -> generate(desc) + is ClassValueDesc<*> -> generate(desc) + is MapValueDesc -> generate(desc) + } + } + + fun generate(desc: PlainValueDesc) { + context.append(desc.value) + } + + fun generate(desc: MapValueDesc) { + context.run { + appendLine("mutableMapOf(") + for ((key, value) in desc.elements) { + generate(key) + append(" to ") + generate(value) + appendLine(",") + } + append(")") + } + } + + fun generate(desc: ClassValueDesc) { + context.run { + appendLine("${desc.type.qualifiedName}(") + for ((param, valueDesc) in desc.properties) { + append(param.name) + append("=") + generate(valueDesc) + appendLine(",") + } + append(")") + } + } + + fun generate(desc: ArrayValueDesc) { + val array = desc.value + + fun impl(funcName: String, elements: List) { + context.run { + append(funcName) + append('(') + val list = elements.toList() + list.forEachIndexed { index, desc -> + generate(desc) + if (index != list.lastIndex) append(", ") + } + append(')') + } + } + + return when (array) { + is Array<*> -> impl("arrayOf", desc.elements) + is IntArray -> impl("intArrayOf", desc.elements) + is ByteArray -> { + if (array.size == 0) { + context.append("net.mamoe.mirai.utils.EMPTY_BYTE_ARRAY") // let IDE to shorten references. + return + } else { + if (array.encodeToString().all { Character.isUnicodeIdentifierPart(it) || it.isWhitespace() }) { + // prefers to show readable string + context.append( + "\"${ + array.encodeToString().escapeQuotation() + }\".toByteArray() /* ${array.toUHexString()} */" + ) + } else { + context.append("\"${array.toUHexString()}\".hexToBytes()") + } + return + } + } + is ShortArray -> impl("shortArrayOf", desc.elements) + is CharArray -> impl("charArrayOf", desc.elements) + is LongArray -> impl("longArrayOf", desc.elements) + is FloatArray -> impl("floatArrayOf", desc.elements) + is DoubleArray -> impl("doubleArrayOf", desc.elements) + is BooleanArray -> impl("booleanArrayOf", desc.elements) + is List<*> -> impl("mutableListOf", desc.elements) + is Set<*> -> impl("mutableSetOf", desc.elements) + else -> error("$array is not an array.") + } + } +} + +class CodegenContext( + val sb: StringBuilder = StringBuilder(), + val configuration: CodegenConfiguration = CodegenConfiguration() +) : Appendable by sb { + fun getResult(): String { + return sb.toString() + } +} + +class CodegenConfiguration( + var removeDefaultValues: Boolean = true, +) + + +private fun String.escapeQuotation(): String = buildString { this@escapeQuotation.escapeQuotationTo(this) } + +private fun String.escapeQuotationTo(out: StringBuilder) { + for (i in 0 until length) { + when (val ch = this[i]) { + '\\' -> out.append("\\\\") + '\n' -> out.append("\\n") + '\r' -> out.append("\\r") + '\t' -> out.append("\\t") + '\"' -> out.append("\\\"") + else -> out.append(ch) + } + } +} + diff --git a/mirai-core/src/commonTest/kotlin/utils/codegen/ValueDesc.kt b/mirai-core/src/commonTest/kotlin/utils/codegen/ValueDesc.kt new file mode 100644 index 000000000..e9db2d249 --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/utils/codegen/ValueDesc.kt @@ -0,0 +1,152 @@ +/* + * Copyright 2019-2021 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +package net.mamoe.mirai.internal.utils.codegen + +import kotlin.reflect.KClass +import kotlin.reflect.KParameter +import kotlin.reflect.KType +import kotlin.reflect.full.createType +import kotlin.reflect.typeOf + +sealed interface ValueDesc { + val origin: Any? + + fun accept(visitor: ValueDescVisitor) +} + +sealed interface ArrayValueDesc : ValueDesc { + val value: Any + + val arrayType: KType + val elementType: KType + val elements: MutableList + + companion object { + @OptIn(ExperimentalStdlibApi::class) + fun createOrNull(array: Any, type: KType): ArrayValueDesc? { + if (array is Array<*>) return ObjectArrayValueDesc(array, arrayType = type) + return when (array) { + is IntArray -> PrimitiveArrayValueDesc(array, arrayType = type, elementType = typeOf()) + is ByteArray -> PrimitiveArrayValueDesc(array, arrayType = type, elementType = typeOf()) + is ShortArray -> PrimitiveArrayValueDesc(array, arrayType = type, elementType = typeOf()) + is CharArray -> PrimitiveArrayValueDesc(array, arrayType = type, elementType = typeOf()) + is LongArray -> PrimitiveArrayValueDesc(array, arrayType = type, elementType = typeOf()) + is FloatArray -> PrimitiveArrayValueDesc(array, arrayType = type, elementType = typeOf()) + is DoubleArray -> PrimitiveArrayValueDesc(array, arrayType = type, elementType = typeOf()) + is BooleanArray -> PrimitiveArrayValueDesc(array, arrayType = type, elementType = typeOf()) + else -> return null + } + } + } +} + +class ObjectArrayValueDesc( + override var value: Array<*>, + override val origin: Array<*> = value, + override val arrayType: KType, + override val elementType: KType = arrayType.arguments.first().type ?: Any::class.createType() +) : ArrayValueDesc { + override val elements: MutableList by lazy { + value.mapTo(mutableListOf()) { + ConstructorCallCodegenFacade.analyze(it, elementType) + } + } + + override fun accept(visitor: ValueDescVisitor) { + visitor.visitObjectArray(this) + } +} + +class CollectionValueDesc( + override var value: Collection<*>, + override val origin: Collection<*> = value, + override val arrayType: KType, + override val elementType: KType = arrayType.arguments.first().type ?: Any::class.createType() +) : ArrayValueDesc { + override val elements: MutableList by lazy { + value.mapTo(mutableListOf()) { + ConstructorCallCodegenFacade.analyze(it, elementType) + } + } + + override fun accept(visitor: ValueDescVisitor) { + visitor.visitCollection(this) + } +} + +class MapValueDesc( + var value: Map, + override val origin: Map = value, + val mapType: KType, + val keyType: KType = mapType.arguments.first().type ?: Any::class.createType(), + val valueType: KType = mapType.arguments[1].type ?: Any::class.createType(), +) : ValueDesc { + val elements: MutableMap by lazy { + value.map { + ConstructorCallCodegenFacade.analyze(it.key, keyType) to ConstructorCallCodegenFacade.analyze( + it.value, + valueType + ) + }.toMap(mutableMapOf()) + } + + override fun accept(visitor: ValueDescVisitor) { + visitor.visitMap(this) + } +} + +class PrimitiveArrayValueDesc( + override var value: Any, + override val origin: Any = value, + override val arrayType: KType, + override val elementType: KType +) : ArrayValueDesc { + override val elements: MutableList by lazy { + when (val value = value) { + is IntArray -> value.mapTo(mutableListOf()) { ConstructorCallCodegenFacade.analyze(it, elementType) } + is ByteArray -> value.mapTo(mutableListOf()) { ConstructorCallCodegenFacade.analyze(it, elementType) } + is ShortArray -> value.mapTo(mutableListOf()) { ConstructorCallCodegenFacade.analyze(it, elementType) } + is CharArray -> value.mapTo(mutableListOf()) { ConstructorCallCodegenFacade.analyze(it, elementType) } + is LongArray -> value.mapTo(mutableListOf()) { ConstructorCallCodegenFacade.analyze(it, elementType) } + is FloatArray -> value.mapTo(mutableListOf()) { ConstructorCallCodegenFacade.analyze(it, elementType) } + is DoubleArray -> value.mapTo(mutableListOf()) { ConstructorCallCodegenFacade.analyze(it, elementType) } + is BooleanArray -> value.mapTo(mutableListOf()) { ConstructorCallCodegenFacade.analyze(it, elementType) } + else -> error("$value is not an array.") + } + } + + override fun accept(visitor: ValueDescVisitor) { + visitor.visitPrimitiveArray(this) + } +} + +class PlainValueDesc( + var value: String, + override val origin: Any? +) : ValueDesc { + init { + require(value.isNotBlank()) + } + + override fun accept(visitor: ValueDescVisitor) { + visitor.visitPlain(this) + } +} + +class ClassValueDesc( + override val origin: T, + val properties: MutableMap, +) : ValueDesc { + val type: KClass by lazy { origin::class } + + override fun accept(visitor: ValueDescVisitor) { + visitor.visitClass(this) + } +} \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/utils/codegen/ValueDescVisitor.kt b/mirai-core/src/commonTest/kotlin/utils/codegen/ValueDescVisitor.kt new file mode 100644 index 000000000..6b6ef5f98 --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/utils/codegen/ValueDescVisitor.kt @@ -0,0 +1,140 @@ +/* + * Copyright 2019-2021 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +package net.mamoe.mirai.internal.utils.codegen + +import net.mamoe.mirai.utils.cast +import kotlin.reflect.KClass +import kotlin.reflect.KParameter +import kotlin.reflect.KProperty +import kotlin.reflect.KProperty1 +import kotlin.reflect.full.memberProperties +import kotlin.reflect.full.primaryConstructor + +interface ValueDescVisitor { + fun visitValue(desc: ValueDesc) {} + + fun visitPlain(desc: PlainValueDesc) { + visitValue(desc) + } + + fun visitArray(desc: ArrayValueDesc) { + visitValue(desc) + for (element in desc.elements) { + element.accept(this) + } + } + + fun visitObjectArray(desc: ObjectArrayValueDesc) { + visitArray(desc) + } + + fun visitCollection(desc: CollectionValueDesc) { + visitArray(desc) + } + + fun visitMap(desc: MapValueDesc) { + visitValue(desc) + for ((key, value) in desc.elements.entries) { + key.accept(this) + value.accept(this) + } + } + + fun visitPrimitiveArray(desc: PrimitiveArrayValueDesc) { + visitArray(desc) + } + + fun visitClass(desc: ClassValueDesc) { + visitValue(desc) + desc.properties.forEach { (_, u) -> + u.accept(this) + } + } +} + + +class DefaultValuesMapping( + val forClass: KClass<*>, + val mapping: MutableMap = mutableMapOf() +) { + operator fun get(property: KProperty<*>): Any? = mapping[property.name] +} + +class AnalyzeDefaultValuesMappingVisitor : ValueDescVisitor { + val mappings: MutableList = mutableListOf() + + override fun visitClass(desc: ClassValueDesc) { + super.visitClass(desc) + + if (mappings.any { it.forClass == desc.type }) return + + val defaultInstance = + createInstanceWithMostDefaultValues(desc.type, desc.properties.mapValues { it.value.origin }) + + val optionalParameters = desc.type.primaryConstructor!!.parameters.filter { it.isOptional } + + mappings.add( + DefaultValuesMapping( + desc.type, + optionalParameters.associateTo(mutableMapOf()) { param -> + val value = findCorrespondingProperty(desc, param).get(defaultInstance) + param.name!! to value + } + ) + ) + } + + + private fun findCorrespondingProperty( + desc: ClassValueDesc, + param: KParameter + ) = desc.type.memberProperties.single { it.name == param.name }.cast>() + + private fun createInstanceWithMostDefaultValues(clazz: KClass, arguments: Map): T { + val primaryConstructor = clazz.primaryConstructor ?: error("Type $clazz does not have primary constructor.") + return primaryConstructor.callBy(arguments.filter { !it.key.isOptional }) + } +} + +class RemoveDefaultValuesVisitor( + private val mappings: MutableList, +) : ValueDescVisitor { + override fun visitClass(desc: ClassValueDesc) { + super.visitClass(desc) + val mapping = mappings.find { it.forClass == desc.type }?.mapping ?: return + + // remove properties who have the same values as their default values, this would significantly reduce code size. + mapping.forEach { (name, defaultValue) -> + if (desc.properties.entries.removeIf { + it.key.name == name && equals(it.value.origin, defaultValue) + } + ) { + return@forEach // by removing one property, there will not by any other matches + } + } + } + + fun equals(a: Any?, b: Any?): Boolean { + return when { + a === b -> true + a == b -> true + a is Array<*> && b is Array<*> -> a.contentEquals(b) + a is IntArray && b is IntArray -> a.contentEquals(b) + a is ByteArray && b is ByteArray -> a.contentEquals(b) + a is ShortArray && b is ShortArray -> a.contentEquals(b) + a is LongArray && b is LongArray -> a.contentEquals(b) + a is CharArray && b is CharArray -> a.contentEquals(b) + a is FloatArray && b is FloatArray -> a.contentEquals(b) + a is DoubleArray && b is DoubleArray -> a.contentEquals(b) + a is BooleanArray && b is BooleanArray -> a.contentEquals(b) + else -> false + } + } +} diff --git a/mirai-core/src/commonTest/kotlin/utils/codegen/test/ConstructorCallCodegenTest.kt b/mirai-core/src/commonTest/kotlin/utils/codegen/test/ConstructorCallCodegenTest.kt new file mode 100644 index 000000000..fec8533cf --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/utils/codegen/test/ConstructorCallCodegenTest.kt @@ -0,0 +1,112 @@ +/* + * Copyright 2019-2021 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +package net.mamoe.mirai.internal.utils.codegen.test + +import net.mamoe.mirai.internal.utils.codegen.ConstructorCallCodegenFacade +import net.mamoe.mirai.internal.utils.codegen.analyzeAndGenerate +import kotlin.test.Test +import kotlin.test.assertEquals + +class ConstructorCallCodegenTest { + + @Test + fun `test plain`() { + assertEquals( + "\"test\"", + ConstructorCallCodegenFacade.analyzeAndGenerate("test") + ) + assertEquals( + "1", + ConstructorCallCodegenFacade.analyzeAndGenerate(1) + ) + assertEquals( + "1.0", + ConstructorCallCodegenFacade.analyzeAndGenerate(1.0) + ) + } + + @Test + fun `test array`() { + assertEquals( + "arrayOf(1, 2)", + ConstructorCallCodegenFacade.analyzeAndGenerate(arrayOf(1, 2)) + ) + assertEquals( + "arrayOf(5.0)", + ConstructorCallCodegenFacade.analyzeAndGenerate(arrayOf(5.0)) + ) + assertEquals( + "arrayOf(\"1\")", + ConstructorCallCodegenFacade.analyzeAndGenerate(arrayOf("1")) + ) + assertEquals( + "arrayOf(arrayOf(1))", + ConstructorCallCodegenFacade.analyzeAndGenerate(arrayOf(arrayOf(1))) + ) + } + + data class TestClass( + val value: String + ) + + data class TestClass2( + val value: Any + ) + + @Test + fun `test class`() { + assertEquals( + """ + ${TestClass::class.qualifiedName!!}( + value="test", + ) + """.trimIndent(), + ConstructorCallCodegenFacade.analyzeAndGenerate(TestClass("test")) + ) + assertEquals( + """ + ${TestClass2::class.qualifiedName!!}( + value="test", + ) + """.trimIndent(), + ConstructorCallCodegenFacade.analyzeAndGenerate(TestClass2("test")) + ) + assertEquals( + """ + ${TestClass2::class.qualifiedName!!}( + value=1, + ) + """.trimIndent(), + ConstructorCallCodegenFacade.analyzeAndGenerate(TestClass2(1)) + ) + } + + data class TestNesting( + val nested: Nested + ) { + data class Nested( + val value: String + ) + } + + @Test + fun `test nesting`() { + assertEquals( + """ + net.mamoe.mirai.internal.utils.codegen.test.ConstructorCallCodegenTest.TestNesting( + nested=net.mamoe.mirai.internal.utils.codegen.test.ConstructorCallCodegenTest.TestNesting.Nested( + value="test", + ), + ) + """.trimIndent(), + ConstructorCallCodegenFacade.analyzeAndGenerate(TestNesting(TestNesting.Nested("test"))) + ) + } +} \ No newline at end of file