diff --git a/build.gradle.kts b/build.gradle.kts index cc48ada5..beb225bb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,6 +14,7 @@ repositories { dependencies { implementation(kotlin("stdlib")) implementation(kotlin("reflect")) + implementation("org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.5.0") implementation(platform("com.fasterxml.jackson:jackson-bom:2.13.3")) // https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/Exceptions.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/Exceptions.kt index 8dc910db..79115bef 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/Exceptions.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/Exceptions.kt @@ -3,14 +3,14 @@ package com.fasterxml.jackson.module.kotlin import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.databind.JsonMappingException import com.fasterxml.jackson.databind.exc.MismatchedInputException -import kotlin.reflect.KParameter +import com.fasterxml.jackson.module.kotlin.deser.value_instantiator.creator.ValueParameter /** * Specialized [JsonMappingException] sub-class used to indicate that a mandatory Kotlin constructor * parameter was missing or null. */ public class MissingKotlinParameterException internal constructor( - public val parameter: KParameter, + internal val parameter: ValueParameter, processor: JsonParser? = null, msg: String ) : MismatchedInputException(processor, msg) diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/InternalCommons.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/InternalCommons.kt index 47c1a49e..36fa0feb 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/InternalCommons.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/InternalCommons.kt @@ -1,6 +1,13 @@ package com.fasterxml.jackson.module.kotlin import com.fasterxml.jackson.databind.JsonMappingException +import kotlinx.metadata.KmClass +import kotlinx.metadata.KmValueParameter +import kotlinx.metadata.jvm.JvmMethodSignature +import kotlinx.metadata.jvm.KotlinClassHeader +import kotlinx.metadata.jvm.KotlinClassMetadata +import java.lang.reflect.Constructor +import java.lang.reflect.Method import java.util.* import kotlin.reflect.KType import kotlin.reflect.jvm.jvmErasure @@ -26,3 +33,57 @@ internal fun Class<*>.isKotlinClass(): Boolean = declaredAnnotations.any { it is internal fun Class<*>.isUnboxableValueClass() = annotations.any { it is JvmInline } internal fun KType.erasedType(): Class = this.jvmErasure.java + +internal fun Class<*>.toKmClass(): KmClass = annotations + .filterIsInstance() + .first() + .let { + KotlinClassMetadata.read( + KotlinClassHeader( + it.kind, + it.metadataVersion, + it.data1, + it.data2, + it.extraString, + it.packageName, + it.extraInt + ) + ) as KotlinClassMetadata.Class + }.toKmClass() + +private val primitiveClassToDesc by lazy { + mapOf( + Byte::class.javaPrimitiveType to 'B', + Char::class.javaPrimitiveType to 'C', + Double::class.javaPrimitiveType to 'D', + Float::class.javaPrimitiveType to 'F', + Int::class.javaPrimitiveType to 'I', + Long::class.javaPrimitiveType to 'J', + Short::class.javaPrimitiveType to 'S', + Boolean::class.javaPrimitiveType to 'Z', + Void::class.javaPrimitiveType to 'V' + ) +} + +private val Class<*>.descriptor: String + get() = when { + isPrimitive -> primitiveClassToDesc.getValue(this).toString() + isArray -> "[${componentType.descriptor}" + else -> "L${name.replace('.', '/')};" + } + +internal fun Array>.toDescString(): String = + joinToString(separator = "", prefix = "(", postfix = ")") { it.descriptor } + +internal fun Constructor<*>.toSignature(): JvmMethodSignature = + JvmMethodSignature("", parameterTypes.toDescString() + "V") + +internal fun Method.toSignature(): JvmMethodSignature = + JvmMethodSignature(this.name, parameterTypes.toDescString() + this.returnType.descriptor) + +internal fun List.hasVarargParam(): Boolean = + lastOrNull()?.let { it.varargElementType != null } ?: false + +internal val defaultConstructorMarker: Class<*> by lazy { + Class.forName("kotlin.jvm.internal.DefaultConstructorMarker") +} diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ReflectionCache.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ReflectionCache.kt index 9fcd5a79..d1332241 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ReflectionCache.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ReflectionCache.kt @@ -65,8 +65,8 @@ internal class ReflectionCache(reflectionCacheSize: Int) { val constructor = _withArgsCreator.annotated as Constructor javaConstructorToValueCreator.get(constructor) - ?: kotlinFromJava(constructor)?.let { - val value = ConstructorValueCreator(it) + ?: run { + val value = ConstructorValueCreator(constructor) javaConstructorToValueCreator.putIfAbsent(constructor, value) ?: value } } @@ -74,8 +74,8 @@ internal class ReflectionCache(reflectionCacheSize: Int) { val method = _withArgsCreator.annotated as Method javaMethodToValueCreator.get(method) - ?: kotlinFromJava(method)?.let { - val value = MethodValueCreator.of(it) + ?: kotlin.run { + val value = MethodValueCreator(method) javaMethodToValueCreator.putIfAbsent(method, value) ?: value } } diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/deser/value_instantiator/KotlinValueInstantiator.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/deser/value_instantiator/KotlinValueInstantiator.kt index c764c22a..920e3a6d 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/deser/value_instantiator/KotlinValueInstantiator.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/deser/value_instantiator/KotlinValueInstantiator.kt @@ -11,14 +11,11 @@ import com.fasterxml.jackson.databind.deser.impl.PropertyValueBuffer import com.fasterxml.jackson.databind.deser.std.StdValueInstantiator import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException import com.fasterxml.jackson.module.kotlin.ReflectionCache -import com.fasterxml.jackson.module.kotlin.deser.value_instantiator.creator.ConstructorValueCreator -import com.fasterxml.jackson.module.kotlin.deser.value_instantiator.creator.MethodValueCreator import com.fasterxml.jackson.module.kotlin.deser.value_instantiator.creator.ValueCreator +import com.fasterxml.jackson.module.kotlin.deser.value_instantiator.creator.ValueParameter import com.fasterxml.jackson.module.kotlin.isKotlinClass import com.fasterxml.jackson.module.kotlin.wrapWithPath -import java.lang.reflect.TypeVariable import kotlin.reflect.KParameter -import kotlin.reflect.KType import kotlin.reflect.jvm.javaType internal class KotlinValueInstantiator( @@ -29,6 +26,10 @@ internal class KotlinValueInstantiator( private val nullIsSameAsDefault: Boolean, private val strictNullChecks: Boolean ) : StdValueInstantiator(src) { + // If the collection type argument cannot be obtained, treat it as nullable + // @see com.fasterxml.jackson.module.kotlin._ported.test.StrictNullChecksTest#testListOfGenericWithNullValue + private fun ValueParameter.isNullishTypeAt(index: Int) = arguments.getOrNull(index)?.isNullable ?: true + override fun createFromObjectWith( ctxt: DeserializationContext, props: Array, @@ -37,24 +38,7 @@ internal class KotlinValueInstantiator( val valueCreator: ValueCreator<*> = cache.valueCreatorFromJava(_withArgsCreator) ?: return super.createFromObjectWith(ctxt, props, buffer) - val propCount: Int - var numCallableParameters: Int - val callableParameters: Array - val jsonParamValueList: Array - - if (valueCreator is MethodValueCreator) { - propCount = props.size + 1 - numCallableParameters = 1 - callableParameters = arrayOfNulls(propCount) - .apply { this[0] = valueCreator.instanceParameter } - jsonParamValueList = arrayOfNulls(propCount) - .apply { this[0] = valueCreator.companionObjectInstance } - } else { - propCount = props.size - numCallableParameters = 0 - callableParameters = arrayOfNulls(propCount) - jsonParamValueList = arrayOfNulls(propCount) - } + val bucket = valueCreator.generateBucket() valueCreator.valueParameters.forEachIndexed { idx, paramDef -> val jsonProp = props[idx] @@ -64,14 +48,14 @@ internal class KotlinValueInstantiator( return@forEachIndexed } - var paramVal = if (!isMissing || paramDef.isPrimitive() || jsonProp.hasInjectableValueId()) { + var paramVal = if (!isMissing || paramDef.isPrimitive || jsonProp.hasInjectableValueId()) { val tempParamVal = buffer.getParameter(jsonProp) if (nullIsSameAsDefault && tempParamVal == null && paramDef.isOptional) { return@forEachIndexed } tempParamVal } else { - if (paramDef.type.isMarkedNullable) { + if (paramDef.isNullable) { // do not try to create any object if it is nullable and the value is missing null } else { @@ -84,11 +68,8 @@ internal class KotlinValueInstantiator( paramVal = NullsAsEmptyProvider(jsonProp.valueDeserializer).getNullValue(ctxt) } - val isGenericTypeVar = paramDef.type.javaType is TypeVariable<*> val isMissingAndRequired = paramVal == null && isMissing && jsonProp.isRequired - if (isMissingAndRequired || - (!isGenericTypeVar && paramVal == null && !paramDef.type.isMarkedNullable) - ) { + if (isMissingAndRequired || (!paramDef.isGenericType && paramVal == null && !paramDef.isNullable)) { throw MissingKotlinParameterException( parameter = paramDef, processor = ctxt.parser, @@ -97,24 +78,17 @@ internal class KotlinValueInstantiator( } if (strictNullChecks && paramVal != null) { - var paramType: String? = null - var itemType: KType? = null - if (jsonProp.type.isCollectionLikeType && paramDef.type.arguments.getOrNull(0)?.type?.isMarkedNullable == false && (paramVal as Collection<*>).any { it == null }) { - paramType = "collection" - itemType = paramDef.type.arguments[0].type - } - - if (jsonProp.type.isMapLikeType && paramDef.type.arguments.getOrNull(1)?.type?.isMarkedNullable == false && (paramVal as Map<*, *>).any { it.value == null }) { - paramType = "map" - itemType = paramDef.type.arguments[1].type - } - - if (jsonProp.type.isArrayType && paramDef.type.arguments.getOrNull(0)?.type?.isMarkedNullable == false && (paramVal as Array<*>).any { it == null }) { - paramType = "array" - itemType = paramDef.type.arguments[0].type - } - - if (paramType != null && itemType != null) { + // If an error occurs, Argument.name is always non-null + // @see com.fasterxml.jackson.module.kotlin.deser.value_instantiator.creator.Argument + when { + paramVal is Collection<*> && !paramDef.isNullishTypeAt(0) && paramVal.any { it == null } -> + "collection" to paramDef.arguments[0].name!! + paramVal is Array<*> && !paramDef.isNullishTypeAt(0) && paramVal.any { it == null } -> + "array" to paramDef.arguments[0].name!! + paramVal is Map<*, *> && !paramDef.isNullishTypeAt(1) && paramVal.values.any { it == null } -> + "map" to paramDef.arguments[1].name!! + else -> null + }?.let { (paramType, itemType) -> throw MissingKotlinParameterException( parameter = paramDef, processor = ctxt.parser, @@ -123,25 +97,11 @@ internal class KotlinValueInstantiator( } } - jsonParamValueList[numCallableParameters] = paramVal - callableParameters[numCallableParameters] = paramDef - numCallableParameters++ + bucket[idx] = paramVal } - return if (numCallableParameters == jsonParamValueList.size && valueCreator is ConstructorValueCreator) { - // we didn't do anything special with default parameters, do a normal call - super.createFromObjectWith(ctxt, jsonParamValueList) - } else { - valueCreator.checkAccessibility(ctxt) - - val callableParametersByName = linkedMapOf() - callableParameters.mapIndexed { idx, paramDef -> - if (paramDef != null) { - callableParametersByName[paramDef] = jsonParamValueList[idx] - } - } - valueCreator.callBy(callableParametersByName) - } + valueCreator.checkAccessibility(ctxt) + return valueCreator.callBy(bucket) } private fun KParameter.isPrimitive(): Boolean { diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/deser/value_instantiator/argument_bucket/ArgumentBucket.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/deser/value_instantiator/argument_bucket/ArgumentBucket.kt new file mode 100644 index 00000000..fc6ca229 --- /dev/null +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/deser/value_instantiator/argument_bucket/ArgumentBucket.kt @@ -0,0 +1,81 @@ +package com.fasterxml.jackson.module.kotlin.deser.value_instantiator.argument_bucket + +import java.lang.reflect.Array as ReflectArray + +private fun defaultPrimitiveValue(type: Class<*>): Any = when (type) { + Boolean::class.java -> false + Char::class.java -> 0.toChar() + Byte::class.java -> 0.toByte() + Short::class.java -> 0.toShort() + Int::class.java -> 0 + Float::class.java -> 0f + Long::class.java -> 0L + Double::class.java -> 0.0 + Void.TYPE -> throw IllegalStateException("Parameter with void type is illegal") + else -> throw UnsupportedOperationException("Unknown primitive: $type") +} + +private fun defaultEmptyArray(arrayType: Class<*>): Any = + ReflectArray.newInstance(arrayType.componentType, 0) + +// @see https://github.com/JetBrains/kotlin/blob/4c925d05883a8073e6732bca95bf575beb031a59/core/reflection.jvm/src/kotlin/reflect/jvm/internal/KCallableImpl.kt#L114 +internal class BucketGenerator( + parameterTypes: List>, + hasVarargParam: Boolean +) { + private val valueParameterSize: Int = parameterTypes.size + private val originalAbsentArgs: Array + + init { + val maskSize = (parameterTypes.size + Integer.SIZE - 1) / Integer.SIZE + + // Array containing the actual function arguments, masks, and +1 for DefaultConstructorMarker or MethodHandle. + originalAbsentArgs = arrayOfNulls(valueParameterSize + maskSize + 1) + + // Set values of primitive arguments to the boxed default values (such as 0, 0.0, false) instead of nulls. + parameterTypes.forEachIndexed { i, clazz -> + if (clazz.isPrimitive) { + originalAbsentArgs[i] = defaultPrimitiveValue(clazz) + } + } + + if (hasVarargParam) { + // vararg argument is always at the end of the arguments. + val i = valueParameterSize - 1 + originalAbsentArgs[i] = defaultEmptyArray(parameterTypes[i]) + } + + for (i in 0 until maskSize) { + originalAbsentArgs[valueParameterSize + i] = -1 + } + } + + fun generate(): ArgumentBucket = ArgumentBucket(valueParameterSize, originalAbsentArgs.clone()) +} + +internal class ArgumentBucket( + private val valueParameterSize: Int, + private val arguments: Array +) { + companion object { + // List of Int with only 1 bit enabled. + private val BIT_FLAGS: List = IntArray(Int.SIZE_BITS) { (1 shl it).inv() }.asList() + } + + private var count = 0 + + operator fun set(index: Int, arg: Any?) { + // Since there is no multiple initialization in the use case, the key check is omitted. + arguments[index] = arg + + val maskIndex = valueParameterSize + (index / Integer.SIZE) + arguments[maskIndex] = (arguments[maskIndex] as Int) and BIT_FLAGS[index % Integer.SIZE] + + count++ + } + + val isFullInitialized: Boolean get() = count == valueParameterSize + + fun getArgs(): Array = arguments.copyOf(valueParameterSize) + fun getDefaultArgs(): Array = arguments +} diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/deser/value_instantiator/creator/ConstructorValueCreator.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/deser/value_instantiator/creator/ConstructorValueCreator.kt index 2b8703d0..4ace05c6 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/deser/value_instantiator/creator/ConstructorValueCreator.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/deser/value_instantiator/creator/ConstructorValueCreator.kt @@ -1,13 +1,60 @@ package com.fasterxml.jackson.module.kotlin.deser.value_instantiator.creator -import kotlin.reflect.KFunction -import kotlin.reflect.jvm.isAccessible +import com.fasterxml.jackson.module.kotlin.defaultConstructorMarker +import com.fasterxml.jackson.module.kotlin.deser.value_instantiator.argument_bucket.ArgumentBucket +import com.fasterxml.jackson.module.kotlin.deser.value_instantiator.argument_bucket.BucketGenerator +import com.fasterxml.jackson.module.kotlin.hasVarargParam +import com.fasterxml.jackson.module.kotlin.toKmClass +import com.fasterxml.jackson.module.kotlin.toSignature +import kotlinx.metadata.KmClass +import kotlinx.metadata.jvm.signature +import java.lang.reflect.Constructor -internal class ConstructorValueCreator(override val callable: KFunction) : ValueCreator() { - override val accessible: Boolean = callable.isAccessible +internal class ConstructorValueCreator(private val constructor: Constructor) : ValueCreator() { + private val declaringClass: Class = constructor.declaringClass + + override val isAccessible: Boolean = constructor.isAccessible + override val callableName: String = constructor.name + override val valueParameters: List + override val bucketGenerator: BucketGenerator init { // To prevent the call from failing, save the initial value and then rewrite the flag. - if (!accessible) callable.isAccessible = true + if (!isAccessible) constructor.isAccessible = true + + val declaringKmClass: KmClass = declaringClass.toKmClass() + + val constructorParameters = constructor.toSignature().desc.let { desc -> + // Compare only desc for performance + declaringKmClass.constructors.first { desc == it.signature?.desc } + }.valueParameters + + valueParameters = constructorParameters.map { ValueParameter(it) } + bucketGenerator = BucketGenerator(constructor.parameterTypes.asList(), constructorParameters.hasVarargParam()) + } + + private val defaultConstructor: Constructor by lazy { + val maskSize = (constructor.parameters.size + Integer.SIZE - 1) / Integer.SIZE + + val defaultTypes = constructor.parameterTypes.let { + val parameterSize = it.size + val temp = it.copyOf(parameterSize + maskSize + 1) + for (i in 0 until maskSize) { + temp[it.size + i] = Int::class.javaPrimitiveType + } + temp[parameterSize + maskSize] = defaultConstructorMarker + + temp + } + + declaringClass.getDeclaredConstructor(*defaultTypes).apply { + if (!this.isAccessible) this.isAccessible = true + } + } + + override fun callBy(args: ArgumentBucket): T = if (args.isFullInitialized) { + constructor.newInstance(*args.getArgs()) + } else { + defaultConstructor.newInstance(*args.getDefaultArgs()) } } diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/deser/value_instantiator/creator/MethodValueCreator.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/deser/value_instantiator/creator/MethodValueCreator.kt index a899a7a8..4402b09e 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/deser/value_instantiator/creator/MethodValueCreator.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/deser/value_instantiator/creator/MethodValueCreator.kt @@ -1,52 +1,85 @@ package com.fasterxml.jackson.module.kotlin.deser.value_instantiator.creator -import com.fasterxml.jackson.module.kotlin.erasedType -import kotlin.reflect.KFunction -import kotlin.reflect.KParameter -import kotlin.reflect.full.extensionReceiverParameter -import kotlin.reflect.full.instanceParameter -import kotlin.reflect.jvm.isAccessible - -internal class MethodValueCreator private constructor( - override val callable: KFunction, - override val accessible: Boolean, - val companionObjectInstance: Any -) : ValueCreator() { - val instanceParameter: KParameter = callable.instanceParameter!! - - companion object { - fun of(callable: KFunction): MethodValueCreator? { - // we shouldn't have an instance or receiver parameter and if we do, just go with default Java-ish behavior - if (callable.extensionReceiverParameter != null) return null - - val possibleCompanion = callable.instanceParameter!!.type.erasedType().kotlin - - // abort, we have some unknown case here - if (!possibleCompanion.isCompanion) return null - - // To prevent the call from failing, save the initial value and then rewrite the flag. - val initialCallableAccessible = callable.isAccessible - if (!initialCallableAccessible) callable.isAccessible = true - - val (companionObjectInstance: Any, accessible: Boolean) = try { - // throws ex - val instance = possibleCompanion.objectInstance!! - - // If an instance of the companion object can be obtained, accessibility depends on the KFunction - instance to initialCallableAccessible - } catch (ex: IllegalAccessException) { - // fallback for when an odd access exception happens through Kotlin reflection - possibleCompanion.java.enclosingClass.declaredFields - .firstOrNull { it.type.kotlin.isCompanion } - ?.let { - it.isAccessible = true - - // If the instance of the companion object cannot be obtained, accessibility will always be false - it.get(null) to false - } ?: throw ex +import com.fasterxml.jackson.module.kotlin.deser.value_instantiator.argument_bucket.ArgumentBucket +import com.fasterxml.jackson.module.kotlin.deser.value_instantiator.argument_bucket.BucketGenerator +import com.fasterxml.jackson.module.kotlin.hasVarargParam +import com.fasterxml.jackson.module.kotlin.toKmClass +import com.fasterxml.jackson.module.kotlin.toSignature +import kotlinx.metadata.KmClass +import kotlinx.metadata.KmFunction +import kotlinx.metadata.jvm.signature +import java.lang.reflect.Field +import java.lang.reflect.Method + +internal class MethodValueCreator(private val method: Method) : ValueCreator() { + // companion object is always present in the case of a factory function + private val companionField: Field + private val companionObjectClass: Class<*> + + override val isAccessible: Boolean + override val callableName: String = method.name + override val valueParameters: List + override val bucketGenerator: BucketGenerator + + init { + val declaringClass = method.declaringClass + companionField = declaringClass.getDeclaredField(declaringClass.toKmClass().companionObject!!) + + // region: about accessibility + isAccessible = method.isAccessible && companionField.isAccessible + + // To prevent the call from failing, save the initial value and then rewrite the flag. + if (!method.isAccessible) method.isAccessible = true + if (!companionField.isAccessible) companionField.isAccessible = true + // endregion + + // region: read kotlin metadata information + companionObjectClass = companionField.type + val companionKmClass: KmClass = companionObjectClass.toKmClass() + val kmFunction: KmFunction = run { + val signature = method.toSignature() + companionKmClass.functions.first { signature == it.signature } + } + + valueParameters = kmFunction.valueParameters.map { ValueParameter(it) } + bucketGenerator = BucketGenerator(method.parameterTypes.asList(), kmFunction.valueParameters.hasVarargParam()) + // endregion + } + + private val defaultCaller: (args: Array) -> T by lazy { + val maskSize = (method.parameters.size + Integer.SIZE - 1) / Integer.SIZE + val defaultTypes = method.parameterTypes.let { parameterTypes -> + val parameterSize = parameterTypes.size + + // companion object instance(1) + parameterSize + maskSize + marker(1) + val temp = arrayOfNulls>(1 + parameterSize + maskSize + 1) + temp[0] = companionObjectClass // companion object + parameterTypes.copyInto(temp, 1) // parameter types + for (i in (parameterSize + 1)..(parameterSize + maskSize)) { // masks + temp[i] = Int::class.javaPrimitiveType } + temp[parameterSize + maskSize + 1] = Object::class.java // maker + temp + } + val defaultMethod = companionObjectClass.getDeclaredMethod("${callableName}\$default", *defaultTypes) - return MethodValueCreator(callable, accessible, companionObjectInstance) + val companionObject = companionField.get(null) + + return@lazy { + val actualArgs = arrayOfNulls(it.size + 1) + actualArgs[0] = companionObject + it.copyInto(actualArgs, 1) + + @Suppress("UNCHECKED_CAST") + defaultMethod.invoke(null, *actualArgs) as T } } + + @Suppress("UNCHECKED_CAST") + override fun callBy(args: ArgumentBucket): T = if (args.isFullInitialized) { + // It calls static method for simplicity, and is a little slower in terms of speed. + method.invoke(null, *args.getArgs()) + } else { + defaultCaller(args.getDefaultArgs()) + } as T } diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/deser/value_instantiator/creator/ValueCreator.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/deser/value_instantiator/creator/ValueCreator.kt index 51635282..93ff8968 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/deser/value_instantiator/creator/ValueCreator.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/deser/value_instantiator/creator/ValueCreator.kt @@ -2,9 +2,8 @@ package com.fasterxml.jackson.module.kotlin.deser.value_instantiator.creator import com.fasterxml.jackson.databind.DeserializationContext import com.fasterxml.jackson.databind.MapperFeature -import kotlin.reflect.KFunction -import kotlin.reflect.KParameter -import kotlin.reflect.full.valueParameters +import com.fasterxml.jackson.module.kotlin.deser.value_instantiator.argument_bucket.ArgumentBucket +import com.fasterxml.jackson.module.kotlin.deser.value_instantiator.argument_bucket.BucketGenerator /** * A class that abstracts the creation of instances by calling KFunction. @@ -12,36 +11,46 @@ import kotlin.reflect.full.valueParameters */ internal sealed class ValueCreator { /** - * Function to be call. + * Initial value for accessibility by reflection. */ - protected abstract val callable: KFunction + protected abstract val isAccessible: Boolean /** - * Initial value for accessibility by reflection. + * Function name for error output */ - protected abstract val accessible: Boolean + protected abstract val callableName: String /** * ValueParameters of the KFunction to be called. */ - val valueParameters: List by lazy { callable.valueParameters } + abstract val valueParameters: List + + protected abstract val bucketGenerator: BucketGenerator + + fun generateBucket(): ArgumentBucket = bucketGenerator.generate() + + private val accessibilityChecker: (DeserializationContext) -> Boolean by lazy { + return@lazy if (isAccessible) { + { ctxt -> ctxt.config.isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS) } + } else { + { ctxt -> ctxt.config.isEnabled(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS) } + } + } /** * Checking process to see if access from context is possible. * @throws IllegalAccessException */ fun checkAccessibility(ctxt: DeserializationContext) { - if ((!accessible && ctxt.config.isEnabled(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS)) || - (accessible && ctxt.config.isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS)) - ) { + if (accessibilityChecker(ctxt)) { return } - throw IllegalAccessException("Cannot access to function or companion object instance, target: $callable") + throw IllegalAccessException("Cannot access to function, target: $callableName") } /** * Function call with default values enabled. */ - fun callBy(args: Map): T = callable.callBy(args) + abstract fun callBy(args: ArgumentBucket): T } diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/deser/value_instantiator/creator/ValueParameter.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/deser/value_instantiator/creator/ValueParameter.kt new file mode 100644 index 00000000..b3d2afac --- /dev/null +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/deser/value_instantiator/creator/ValueParameter.kt @@ -0,0 +1,46 @@ +package com.fasterxml.jackson.module.kotlin.deser.value_instantiator.creator + +import kotlinx.metadata.Flag +import kotlinx.metadata.KmClassifier +import kotlinx.metadata.KmType +import kotlinx.metadata.KmTypeProjection +import kotlinx.metadata.KmValueParameter + +internal class ValueParameter(private val param: KmValueParameter) { + internal sealed interface Argument { + val isNullable: Boolean + val name: String? + + object Star : Argument { + override val isNullable: Boolean = true + override val name: String? = null + } + + class ArgumentImpl(type: KmType) : Argument { + override val isNullable: Boolean = Flag.Type.IS_NULLABLE(type.flags) + + // TODO: Formatting because it is a minimal display about the error content + override val name: String = type.classifier.toString() + } + } + + val name: String = param.name + val isOptional: Boolean = Flag.ValueParameter.DECLARES_DEFAULT_VALUE(param.flags) + val isPrimitive: Boolean = Flag.IS_PRIVATE(param.type.flags) + val isNullable: Boolean = Flag.Type.IS_NULLABLE(param.type.flags) + val isGenericType: Boolean = param.type.classifier is KmClassifier.TypeParameter + + val arguments: List by lazy { + param.type.arguments.map { + if (it === KmTypeProjection.STAR) { + Argument.Star + } else { + // If it is not a StarProjection, type is not null + Argument.ArgumentImpl(it.type!!) + } + } + } + + // TODO: Formatting into a form that is easy to understand as an error message with reference to KParameter + override fun toString() = "parameter name: ${param.name} parameter type: ${param.type.classifier}" +} diff --git a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/_ported/test/github/Github32.kt b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/_ported/test/github/Github32.kt index e85fbd89..475168f9 100644 --- a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/_ported/test/github/Github32.kt +++ b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/_ported/test/github/Github32.kt @@ -5,9 +5,12 @@ import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +// @see com.fasterxml.jackson.module.kotlin.deser.value_instantiator.creator.ValueParameter#toString +@Disabled private class TestGithub32 { @Test fun `valid mandatory data class constructor param`() {