Skip to content

Commit f80a432

Browse files
committed
Refine Kotlin constructor detection.
Attempt two-pass constructor detection in KotlinInstantiationDelegate to detect private constructors that are not synthetic ones. See #3389 Original pull request: #3390
1 parent eeaa995 commit f80a432

File tree

4 files changed

+70
-14
lines changed

4 files changed

+70
-14
lines changed

src/main/java/org/springframework/data/mapping/model/KotlinDefaultMask.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,12 @@ private static KotlinDefaultMask from(KFunction<?> function, Predicate<KParamete
148148
masks.add(mask);
149149
}
150150

151-
return new KotlinDefaultMask(masks.stream().mapToInt(i -> i).toArray());
151+
int[] defaulting = new int[masks.size()];
152+
for (int i = 0; i < masks.size(); i++) {
153+
defaulting[i] = masks.get(i);
154+
}
155+
156+
return new KotlinDefaultMask(defaulting);
152157
}
153158

154159
public int[] getDefaulting() {

src/main/java/org/springframework/data/mapping/model/KotlinInstantiationDelegate.java

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import kotlin.reflect.jvm.ReflectJvmMapping;
2121

2222
import java.lang.reflect.Constructor;
23+
import java.lang.reflect.Modifier;
2324
import java.util.ArrayList;
2425
import java.util.IdentityHashMap;
2526
import java.util.List;
@@ -53,7 +54,6 @@ class KotlinInstantiationDelegate {
5354
private final Map<KParameter, Integer> indexByKParameter;
5455
private final List<Function<@Nullable Object, @Nullable Object>> wrappers = new ArrayList<>();
5556
private final Constructor<?> constructorToInvoke;
56-
private final boolean hasDefaultConstructorMarker;
5757

5858
public KotlinInstantiationDelegate(PreferredConstructor<?, ?> preferredConstructor,
5959
Constructor<?> constructorToInvoke) {
@@ -74,7 +74,6 @@ public KotlinInstantiationDelegate(PreferredConstructor<?, ?> preferredConstruct
7474
}
7575

7676
this.constructorToInvoke = constructorToInvoke;
77-
this.hasDefaultConstructorMarker = hasDefaultConstructorMarker(constructorToInvoke.getParameters());
7877

7978
for (KParameter kParameter : kParameters) {
8079

@@ -93,11 +92,7 @@ static boolean hasDefaultConstructorMarker(java.lang.reflect.Parameter[] paramet
9392
* @return number of constructor arguments.
9493
*/
9594
public int getRequiredParameterCount() {
96-
97-
return hasDefaultConstructorMarker ? constructorToInvoke.getParameterCount()
98-
: (constructorToInvoke.getParameterCount()
99-
+ KotlinDefaultMask.getMaskCount(constructorToInvoke.getParameterCount())
100-
+ /* DefaultConstructorMarker */1);
95+
return constructorToInvoke.getParameterCount();
10196
}
10297

10398
/**
@@ -154,13 +149,14 @@ public <P extends PersistentProperty<P>> void extractInvocationArguments(@Nullab
154149
}
155150

156151
/**
157-
* Resolves a {@link PreferredConstructor} to a synthetic Kotlin constructor accepting the same user-space parameters
158-
* suffixed by Kotlin-specifics required for defaulting and the {@code kotlin.jvm.internal.DefaultConstructorMarker}.
152+
* Resolves a {@link PreferredConstructor} to the constructor to be invoked. This can be a synthetic Kotlin
153+
* constructor accepting the same user-space parameters suffixed by Kotlin-specifics required for defaulting and the
154+
* {@code kotlin.jvm.internal.DefaultConstructorMarker} or an actual non-synthetic constructor (i.e. private
155+
* constructor).
159156
*
160157
* @since 2.0
161158
* @author Mark Paluch
162159
*/
163-
164160
@SuppressWarnings("unchecked")
165161
@Nullable
166162
public static PreferredConstructor<?, ?> resolveKotlinJvmConstructor(
@@ -184,11 +180,18 @@ private static Constructor<?> doResolveKotlinConstructor(Constructor<?> detected
184180

185181
Class<?> entityType = detectedConstructor.getDeclaringClass();
186182
Constructor<?> hit = null;
183+
Constructor<?> privateFallback = null;
187184
KFunction<?> kotlinFunction = ReflectJvmMapping.getKotlinFunction(detectedConstructor);
188185

189186
for (Constructor<?> candidate : entityType.getDeclaredConstructors()) {
190187

191-
// use only synthetic constructors
188+
if (Modifier.isPrivate(candidate.getModifiers())) {
189+
if (detectedConstructor.equals(candidate)) {
190+
privateFallback = candidate;
191+
}
192+
}
193+
194+
// introspect only synthetic constructors
192195
if (!candidate.isSynthetic()) {
193196
continue;
194197
}
@@ -228,6 +231,10 @@ private static Constructor<?> doResolveKotlinConstructor(Constructor<?> detected
228231
}
229232
}
230233

234+
if (hit == null) {
235+
return privateFallback;
236+
}
237+
231238
return hit;
232239
}
233240

src/test/kotlin/org/springframework/data/mapping/model/InlineClasses.kt

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,44 @@ data class WithMyValueClass(val id: MyValueClass) {
4747
// ---------
4848
}
4949

50-
data class WithMyValueClassPrivateConstructor private constructor(val id: MyValueClass)
50+
data class WithMyValueClassPrivateConstructor private constructor(val id: MyValueClass) {
5151

52-
data class WithMyValueClassPrivateConstructorAndDefaultValue private constructor(val id: MyValueClass = MyValueClass("id"))
52+
// ByteCode explanation
53+
54+
// ---------
55+
// default constructor, detected by Discoverers.KOTLIN
56+
// private WithMyValueClassPrivateConstructor(String id) {}
57+
// ---------
58+
}
59+
60+
data class WithNullableMyValueClassPrivateConstructor private constructor(val id: MyNullableValueClass?) {
61+
62+
// ByteCode explanation
63+
64+
// ---------
65+
// default constructor, detected by Discoverers.KOTLIN
66+
// private WithNullableMyValueClassPrivateConstructor(MyNullableValueClass id) {}
67+
// ---------
68+
}
69+
70+
data class WithMyValueClassPrivateConstructorAndDefaultValue private constructor(
71+
val id: MyValueClass = MyValueClass(
72+
"id"
73+
)
74+
) {
75+
76+
// ByteCode explanation
77+
// ---------
78+
// default constructor, detected by Discoverers.KOTLIN
79+
// private WithMyValueClassPrivateConstructorAndDefaultValue(java.lang.String id) {}
80+
// ---------
81+
82+
// ---------
83+
// synthetic constructor that we actually want to use
84+
// synthetic WithMyValueClassPrivateConstructorAndDefaultValue(java.lang.String id, int arg1, kotlin.jvm.internal.DefaultConstructorMarker arg2) {}
85+
// ---------
86+
87+
}
5388

5489
@JvmInline
5590
value class MyNullableValueClass(val id: String? = "id")

src/test/kotlin/org/springframework/data/mapping/model/KotlinClassGeneratingEntityInstantiatorUnitTests.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,15 @@ class KotlinClassGeneratingEntityInstantiatorUnitTests {
199199
assertThat(instance.id.id).isEqualTo("hello")
200200
}
201201

202+
@Test // GH-3389
203+
fun `should use private default constructor for types using nullable value class`() {
204+
205+
every { provider.getParameterValue<String>(any()) } returns "hello"
206+
val instance = construct(WithNullableMyValueClassPrivateConstructor::class)
207+
208+
assertThat(instance.id?.id).isEqualTo("hello")
209+
}
210+
202211
@Test // GH-3389
203212
fun `should use private default constructor for types using value class with default value`() {
204213

0 commit comments

Comments
 (0)