Skip to content

Commit c652531

Browse files
authored
Merge pull request #197 from ProjectMapK/fix/vararg
Fix handling of vararg deserialization.
2 parents 4a8c02c + d246ca5 commit c652531

File tree

10 files changed

+230
-68
lines changed

10 files changed

+230
-68
lines changed

src/main/kotlin/io/github/projectmapk/jackson/module/kogera/InternalCommons.kt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package io.github.projectmapk.jackson.module.kogera
33
import com.fasterxml.jackson.annotation.JsonCreator
44
import kotlinx.metadata.KmClassifier
55
import kotlinx.metadata.KmType
6-
import kotlinx.metadata.KmValueParameter
76
import kotlinx.metadata.isNullable
87
import kotlinx.metadata.jvm.JvmMethodSignature
98
import java.lang.reflect.AnnotatedElement
@@ -57,9 +56,6 @@ internal fun Constructor<*>.toSignature(): JvmMethodSignature =
5756
internal fun Method.toSignature(): JvmMethodSignature =
5857
JvmMethodSignature(this.name, parameterTypes.toDescBuilder().appendDescriptor(this.returnType).toString())
5958

60-
internal fun List<KmValueParameter>.hasVarargParam(): Boolean =
61-
lastOrNull()?.let { it.varargElementType != null } ?: false
62-
6359
internal val defaultConstructorMarker: Class<*> by lazy {
6460
Class.forName("kotlin.jvm.internal.DefaultConstructorMarker")
6561
}

src/main/kotlin/io/github/projectmapk/jackson/module/kogera/annotationIntrospector/KotlinPrimaryAnnotationIntrospector.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ internal class KotlinPrimaryAnnotationIntrospector(
103103
paramDef.type.isNullable -> false
104104
// Default argument are defined
105105
paramDef.declaresDefaultValue -> false
106+
// vararg is treated as an empty array because undefined input is allowed
107+
paramDef.varargElementType != null -> false
106108
// The conversion in case of null is defined.
107109
type.hasDefaultEmptyValue() -> false
108110
else -> true

src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/KotlinValueInstantiator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ internal class KotlinValueInstantiator(
9292
}
9393
} else {
9494
when {
95-
paramDef.isOptional -> return@forEachIndexed
95+
paramDef.isOptional || paramDef.isVararg -> return@forEachIndexed
9696
// do not try to create any object if it is nullable and the value is missing
9797
paramDef.isNullable -> null
9898
// to get suitable "missing" value provided by deserializer

src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/argumentBucket/ArgumentBucket.kt

Lines changed: 57 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.argu
22

33
import io.github.projectmapk.jackson.module.kogera.ValueClassUnboxConverter
44
import io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.calcMaskSize
5+
import io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.creator.ValueParameter
56
import java.lang.reflect.Array as ReflectArray
67

78
private fun defaultPrimitiveValue(type: Class<*>): Any = when (type) {
@@ -20,26 +21,69 @@ private fun defaultPrimitiveValue(type: Class<*>): Any = when (type) {
2021
private fun defaultEmptyArray(arrayType: Class<*>): Any =
2122
ReflectArray.newInstance(arrayType.componentType, 0)
2223

24+
// List of Int with only 1 bit enabled.
25+
private val BIT_FLAGS: List<Int> = IntArray(Int.SIZE_BITS) { (1 shl it).inv() }.asList()
26+
27+
private enum class MaskOperation {
28+
// Set argument.
29+
SET {
30+
override fun invoke(i: Int, j: Int) = i and j
31+
},
32+
33+
// Mark the argument as uninitialized.
34+
// A vararg argument that has no default value need not be given and is therefore marked as initialized.
35+
INIT {
36+
override fun invoke(i: Int, j: Int): Int = i or j.inv()
37+
};
38+
39+
abstract operator fun invoke(i: Int, j: Int): Int
40+
}
41+
42+
private fun IntArray.update(index: Int, operation: MaskOperation) {
43+
val maskIndex = index / Integer.SIZE
44+
this[maskIndex] = operation(this[maskIndex], BIT_FLAGS[index % Integer.SIZE])
45+
}
46+
2347
// @see https://github.com/JetBrains/kotlin/blob/4c925d05883a8073e6732bca95bf575beb031a59/core/reflection.jvm/src/kotlin/reflect/jvm/internal/KCallableImpl.kt#L114
2448
internal class BucketGenerator(
2549
parameterTypes: List<Class<*>>,
26-
hasVarargParam: Boolean,
50+
valueParameters: List<ValueParameter>,
2751
private val converters: List<ValueClassUnboxConverter<Any>?>
2852
) {
2953
private val valueParameterSize: Int = parameterTypes.size
30-
private val originalAbsentArgs: Array<Any?> = Array(valueParameterSize) { i ->
31-
// Set values of primitive arguments to the boxed default values (such as 0, 0.0, false) instead of nulls.
32-
parameterTypes[i].takeIf { it.isPrimitive }?.let { defaultPrimitiveValue(it) }
33-
}
54+
private val originalAbsentArgs: Array<Any?>
3455

35-
// -1 is the filled bit mask.
36-
private val originalMasks: IntArray = IntArray(calcMaskSize(parameterTypes.size)) { -1 }
56+
// Mask initialized to 0 when all arguments are set.
57+
// The contents are initialized in the init block.
58+
private val originalMasks: IntArray = IntArray(calcMaskSize(parameterTypes.size)) { 0 }
3759

3860
init {
39-
if (hasVarargParam) {
40-
// vararg argument is always at the end of the arguments.
41-
val i = valueParameterSize - 1
42-
originalAbsentArgs[i] = defaultEmptyArray(parameterTypes[i])
61+
originalAbsentArgs = Array(valueParameterSize) { i ->
62+
val paramType = parameterTypes[i]
63+
val metaParam = valueParameters[i]
64+
65+
when {
66+
// In Kotlin, it is possible to define non-tail arguments with vararg,
67+
// which cannot be determined by Java reflection, so they are read from Metadata.
68+
metaParam.isVararg -> {
69+
// If no default arguments are set,
70+
// the call may be made with an empty array even if no arguments are passed,
71+
// so it is treated as initialized.
72+
// Conversely, if default arguments are set,
73+
// the initial value is treated as uninitialized to detect that no arguments has passed.
74+
if (metaParam.isOptional) originalMasks.update(i, MaskOperation.INIT)
75+
defaultEmptyArray(paramType)
76+
}
77+
// Set values of primitive arguments to the boxed default values (such as 0, 0.0, false) instead of nulls.
78+
paramType.isPrimitive -> {
79+
originalMasks.update(i, MaskOperation.INIT)
80+
defaultPrimitiveValue(paramType)
81+
}
82+
else -> {
83+
originalMasks.update(i, MaskOperation.INIT)
84+
null
85+
}
86+
}
4387
}
4488
}
4589

@@ -53,13 +97,6 @@ internal class ArgumentBucket(
5397
val masks: IntArray,
5498
private val converters: List<ValueClassUnboxConverter<Any>?>
5599
) {
56-
companion object {
57-
// List of Int with only 1 bit enabled.
58-
private val BIT_FLAGS: List<Int> = IntArray(Int.SIZE_BITS) { (1 shl it).inv() }.asList()
59-
}
60-
61-
private var count = 0
62-
63100
/**
64101
* Sets the argument corresponding to index.
65102
* Note that, arguments defined in the value class must be passed as boxed.
@@ -77,8 +114,8 @@ internal class ArgumentBucket(
77114
val maskIndex = index / Integer.SIZE
78115
masks[maskIndex] = masks[maskIndex] and BIT_FLAGS[index % Integer.SIZE]
79116

80-
count++
117+
masks.update(index, MaskOperation.SET)
81118
}
82119

83-
val isFullInitialized: Boolean get() = count == valueParameterSize
120+
val isFullInitialized: Boolean get() = masks.all { it == 0 }
84121
}

src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/creator/ConstructorValueCreator.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.argum
88
import io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.argumentBucket.BucketGenerator
99
import io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.calcMaskSize
1010
import io.github.projectmapk.jackson.module.kogera.getDeclaredConstructorBy
11-
import io.github.projectmapk.jackson.module.kogera.hasVarargParam
1211
import java.lang.reflect.Constructor
1312

1413
internal class ConstructorValueCreator<T : Any>(
@@ -33,7 +32,7 @@ internal class ConstructorValueCreator<T : Any>(
3332
val rawTypes = constructor.parameterTypes.asList()
3433
bucketGenerator = BucketGenerator(
3534
rawTypes,
36-
constructorParameters.hasVarargParam(),
35+
valueParameters,
3736
constructorParameters.mapToConverters(rawTypes, cache)
3837
)
3938
}

src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/creator/MethodValueCreator.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.argum
77
import io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.argumentBucket.BucketGenerator
88
import io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.calcMaskSize
99
import io.github.projectmapk.jackson.module.kogera.getDeclaredMethodBy
10-
import io.github.projectmapk.jackson.module.kogera.hasVarargParam
1110
import kotlinx.metadata.KmFunction
1211
import java.lang.reflect.Method
1312

@@ -33,7 +32,7 @@ internal class MethodValueCreator<T>(
3332
val rawTypes = method.parameterTypes.asList()
3433
bucketGenerator = BucketGenerator(
3534
rawTypes,
36-
kmParameters.hasVarargParam(),
35+
valueParameters,
3736
kmParameters.mapToConverters(rawTypes, cache)
3837
)
3938
}

src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/creator/ValueParameter.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ internal class ValueParameter(param: KmValueParameter) {
1111
val type: KmType = param.type
1212
val isOptional: Boolean = param.declaresDefaultValue
1313
val isNullable: Boolean = type.isNullable
14+
val isVararg: Boolean = param.varargElementType != null
1415
val isGenericType: Boolean = type.classifier is KmClassifier.TypeParameter
1516

1617
// TODO: Formatting into a form that is easy to understand as an error message with reference to KParameter

src/test/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/argumentBucket/ArgumentBucketTest.kt

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.argumentBucket
22

33
import io.github.projectmapk.jackson.module.kogera.ValueClassUnboxConverter
4+
import io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.creator.ValueParameter
45
import io.mockk.every
56
import io.mockk.mockk
6-
import org.junit.jupiter.api.Assertions.assertArrayEquals
77
import org.junit.jupiter.api.Assertions.assertEquals
88
import org.junit.jupiter.api.Assertions.assertFalse
99
import org.junit.jupiter.api.Assertions.assertTrue
@@ -15,11 +15,20 @@ private class ArgumentBucketTest {
1515
every { this@mockk[any()] } returns null
1616
}
1717

18+
fun mockValueParameter(mockVararg: Boolean = false, mockOptional: Boolean = false) = mockk<ValueParameter> {
19+
every { isVararg } returns mockVararg
20+
every { isOptional } returns mockOptional
21+
}
22+
1823
@Nested
1924
inner class BucketGeneratorTest {
2025
@Test
2126
fun basic() {
22-
val generator = BucketGenerator((0..31).map { String::class.java }, false, mockConverters)
27+
val generator = BucketGenerator(
28+
(0..31).map { String::class.java },
29+
(0..31).map { mockValueParameter() },
30+
mockConverters
31+
)
2332
val result = generator.generate()
2433

2534
assertEquals(32, result.valueParameterSize)
@@ -41,7 +50,7 @@ private class ArgumentBucketTest {
4150
Long::class.java,
4251
Double::class.java
4352
),
44-
false,
53+
(1..8).map { mockValueParameter() },
4554
mockConverters
4655
)
4756
val result = generator.generate()
@@ -58,52 +67,43 @@ private class ArgumentBucketTest {
5867

5968
@Test
6069
fun maskSize() {
61-
val generator = BucketGenerator((0..32).map { String::class.java }, false, mockConverters)
70+
val generator = BucketGenerator(
71+
(0..32).map { String::class.java },
72+
(0..32).map { mockValueParameter() },
73+
mockConverters
74+
)
6275
val result = generator.generate()
6376

6477
assertEquals(33, result.valueParameterSize)
6578
assertTrue(result.arguments.all { it == null })
6679
assertEquals(2, result.masks.size)
6780
assertEquals(-1, result.masks[0])
68-
assertEquals(-1, result.masks[1])
81+
assertEquals(1, result.masks[1])
6982
}
7083

71-
@Test
72-
fun hasVararg() {
73-
val primitiveGenerator = BucketGenerator(listOf(IntArray::class.java), true, mockConverters)
74-
val primitiveResult = primitiveGenerator.generate()
75-
76-
assertEquals(1, primitiveResult.valueParameterSize)
77-
assertArrayEquals(intArrayOf(), primitiveResult.arguments[0] as IntArray)
78-
assertEquals(1, primitiveResult.masks.size)
79-
assertEquals(-1, primitiveResult.masks[0])
80-
81-
val objectGenerator = BucketGenerator(listOf(Array<Int>::class.java), true, mockConverters)
82-
val objectResult = objectGenerator.generate()
83-
assertEquals(1, objectResult.valueParameterSize)
84-
@Suppress("UNCHECKED_CAST")
85-
assertArrayEquals(emptyArray<Int>(), objectResult.arguments[0] as Array<Int>)
86-
assertEquals(1, objectResult.masks.size)
87-
assertEquals(-1, objectResult.masks[0])
88-
}
84+
// vararg is verified with an integration test.
8985
}
9086

9187
@Test
9288
fun test() {
93-
val generator = BucketGenerator((0..32).map { String::class.java }, false, mockConverters)
89+
val generator = BucketGenerator(
90+
(0..32).map { String::class.java },
91+
(0..32).map { mockValueParameter() },
92+
mockConverters
93+
)
9494
val sut = generator.generate()
9595

9696
sut[0] = "0"
9797
sut[32] = "32"
9898

9999
assertEquals(-2, sut.masks[0])
100-
assertEquals(-2, sut.masks[1])
100+
assertEquals(0, sut.masks[1])
101101
assertFalse(sut.isFullInitialized)
102102

103103
(1..31).forEach { sut[it] = it.toString() }
104104

105105
assertEquals(0, sut.masks[0])
106-
assertEquals(-2, sut.masks[1])
106+
assertEquals(0, sut.masks[1])
107107
(0..32).forEach { assertEquals(it.toString(), sut.arguments[it]) }
108108
assertTrue(sut.isFullInitialized)
109109
}
@@ -115,7 +115,11 @@ private class ArgumentBucketTest {
115115
fun unboxTest() {
116116
@Suppress("UNCHECKED_CAST")
117117
val converter = ValueClassUnboxConverter(V::class.java) as ValueClassUnboxConverter<Any>
118-
val generator = BucketGenerator(listOf(Int::class.java, V::class.java), false, listOf(converter, null))
118+
val generator = BucketGenerator(
119+
listOf(Int::class.java, V::class.java),
120+
(1..2).map { mockValueParameter() },
121+
listOf(converter, null)
122+
)
119123
val bucket = generator.generate()
120124

121125
bucket[0] = V(0)

0 commit comments

Comments
 (0)