Skip to content

Commit c1575b1

Browse files
committed
Extract Invokable helper for backward compat
Port of ZacSweers/MoshiX#847
1 parent 8bdd92c commit c1575b1

File tree

3 files changed

+124
-74
lines changed

3 files changed

+124
-74
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package com.squareup.moshi.kotlin.reflect
2+
3+
import com.squareup.moshi.internal.DEFAULT_CONSTRUCTOR_MARKER
4+
import java.lang.reflect.Constructor
5+
import java.lang.reflect.Method
6+
7+
private val DEFAULT_CONSTRUCTOR_SIGNATURE by
8+
lazy(LazyThreadSafetyMode.NONE) { DEFAULT_CONSTRUCTOR_MARKER!!.descriptor }
9+
10+
/**
11+
* A thin wrapper over [Constructor] and [Method] to avoid using [java.lang.reflect.Executable],
12+
* which is not available on Android until API 26.
13+
*/
14+
internal sealed interface Invokable {
15+
val parameterTypes: Array<Class<*>>
16+
val parameterAnnotations: Array<Array<Annotation>>
17+
val jvmMethodSignature: String
18+
19+
fun setAccessible()
20+
21+
fun defaultsSignature(): String
22+
23+
@JvmInline
24+
value class InvokableConstructor(val constructor: Constructor<*>) : Invokable {
25+
override val parameterTypes: Array<Class<*>>
26+
get() = constructor.parameterTypes
27+
28+
override val parameterAnnotations: Array<Array<Annotation>>
29+
get() = constructor.parameterAnnotations
30+
31+
override val jvmMethodSignature: String
32+
get() = constructor.jvmMethodSignature
33+
34+
override fun setAccessible() {
35+
constructor.isAccessible = true
36+
}
37+
38+
override fun defaultsSignature(): String {
39+
val rawPrefix = jvmMethodSignature.removeSuffix("V").removeSuffix(")")
40+
return buildDefaultsSignature(rawPrefix, parameterTypes.size, "V")
41+
}
42+
}
43+
44+
@JvmInline
45+
value class InvokableMethod(val method: Method) : Invokable {
46+
override val parameterTypes: Array<Class<*>>
47+
get() = method.parameterTypes
48+
49+
override val parameterAnnotations: Array<Array<Annotation>>
50+
get() = method.parameterAnnotations
51+
52+
override val jvmMethodSignature: String
53+
get() = method.jvmMethodSignature
54+
55+
override fun setAccessible() {
56+
method.isAccessible = true
57+
}
58+
59+
override fun defaultsSignature(): String {
60+
val suffixDescriptor = method.returnType.descriptor
61+
val rawPrefix = jvmMethodSignature.removeSuffix(suffixDescriptor).removeSuffix(")")
62+
// Need to add $default to the end of the method name
63+
val (name, rest) = rawPrefix.split("(", limit = 2)
64+
// ktlint doesn't support multi-dollar prefixes
65+
@Suppress("CanConvertToMultiDollarString") val prefix = "$name\$default($rest"
66+
return buildDefaultsSignature(prefix, parameterTypes.size, suffixDescriptor)
67+
}
68+
}
69+
}
70+
71+
private fun buildDefaultsSignature(prefix: String, parameterCount: Int, suffix: String): String {
72+
val maskParamsToAdd = (parameterCount + 31) / 32
73+
return buildString {
74+
append(prefix)
75+
repeat(maskParamsToAdd) { append("I") }
76+
append(DEFAULT_CONSTRUCTOR_SIGNATURE)
77+
append(')')
78+
append(suffix)
79+
}
80+
}

moshi-kotlin/src/main/java/com/squareup/moshi/kotlin/reflect/JvmDescriptors.kt

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
package com.squareup.moshi.kotlin.reflect
1717

1818
import java.lang.reflect.Constructor
19-
import java.lang.reflect.Executable
2019
import java.lang.reflect.Method
2120
import kotlin.metadata.jvm.JvmMethodSignature
2221

@@ -156,25 +155,28 @@ internal fun JvmMethodSignature.decodeParameterTypes(): List<Class<*>> {
156155
}
157156

158157
/**
159-
* Returns the JVM signature in the form "<init>$MethodDescriptor", for example:
160-
* `"<init>(Ljava/lang/Object;)V")`.
158+
* Returns the JVM signature in the form "name$MethodDescriptor", for example:
159+
* `"<init>(Ljava/lang/Object;)V"` or `"foo(Ljava/lang/Object;)V"`.
161160
*
162161
* Useful for comparing with [JvmMethodSignature].
163162
*
164163
* @see <a href="https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3">JVM
165164
* specification, section 4.3</a>
166165
*/
167-
internal val Executable.jvmMethodSignature: String
168-
get() = buildString {
169-
when (this@jvmMethodSignature) {
170-
is Constructor<*> -> append("<init>")
171-
is Method -> append(name)
172-
}
173-
parameterTypes.joinTo(buffer = this, separator = "", prefix = "(", postfix = ")") {
174-
it.descriptor
175-
}
176-
when (this@jvmMethodSignature) {
177-
is Constructor<*> -> append("V")
178-
is Method -> append(returnType.descriptor)
179-
}
166+
private fun jvmMethodSignature(
167+
name: String,
168+
parameterTypes: Array<Class<*>>,
169+
returnDescriptor: String,
170+
): String = buildString {
171+
append(name)
172+
parameterTypes.joinTo(buffer = this, separator = "", prefix = "(", postfix = ")") {
173+
it.descriptor
180174
}
175+
append(returnDescriptor)
176+
}
177+
178+
internal val Constructor<*>.jvmMethodSignature: String
179+
get() = jvmMethodSignature("<init>", parameterTypes, "V")
180+
181+
internal val Method.jvmMethodSignature: String
182+
get() = jvmMethodSignature(name, parameterTypes, returnType.descriptor)

moshi-kotlin/src/main/java/com/squareup/moshi/kotlin/reflect/KmExecutable.kt

Lines changed: 26 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package com.squareup.moshi.kotlin.reflect
22

3-
import com.squareup.moshi.internal.DEFAULT_CONSTRUCTOR_MARKER
43
import java.lang.reflect.Constructor
5-
import java.lang.reflect.Executable
64
import java.lang.reflect.Method
75
import java.lang.reflect.Modifier
86
import kotlin.metadata.KmClass
@@ -12,14 +10,11 @@ import kotlin.metadata.isValue
1210
import kotlin.metadata.jvm.KotlinClassMetadata
1311
import kotlin.metadata.jvm.signature
1412

15-
private val DEFAULT_CONSTRUCTOR_SIGNATURE by
16-
lazy(LazyThreadSafetyMode.NONE) { DEFAULT_CONSTRUCTOR_MARKER!!.descriptor }
17-
1813
/**
1914
* Simple facade over KM constructor-ish types, which could be a constructor or a static creator
2015
* method (value classes).
2116
*/
22-
internal sealed class KmExecutable<T : Executable> {
17+
internal sealed class KmExecutable<T : Any> {
2318
abstract val parameters: List<KtParameter>
2419
abstract val isDefault: Boolean
2520
abstract val needsDefaultMarker: Boolean
@@ -72,56 +67,25 @@ internal sealed class KmExecutable<T : Executable> {
7267
return boxImplMethod to unboxImplMethod
7368
}
7469

75-
private fun Executable.defaultsSignature(): String {
76-
val suffixDescriptor =
77-
when (this) {
78-
is Constructor<*> -> "V"
79-
is Method -> returnType.descriptor
80-
}
81-
val rawPrefix = jvmMethodSignature.removeSuffix(suffixDescriptor).removeSuffix(")")
82-
val prefix =
83-
when (this) {
84-
is Constructor<*> -> {
85-
rawPrefix
86-
}
87-
88-
is Method -> {
89-
// Need to add $default to the end of the method name
90-
val (name, rest) = rawPrefix.split("(", limit = 2)
91-
// ktlint doesn't support multi-dollar prefixes
92-
@Suppress("CanConvertToMultiDollarString") "$name\$default($rest"
93-
}
94-
}
95-
val parameterCount = parameterTypes.size
96-
val maskParamsToAdd = (parameterCount + 31) / 32
97-
val defaultConstructorSignature = buildString {
98-
append(prefix)
99-
repeat(maskParamsToAdd) { append("I") }
100-
append(DEFAULT_CONSTRUCTOR_SIGNATURE)
101-
append(')')
102-
append(suffixDescriptor)
103-
}
104-
return defaultConstructorSignature
105-
}
106-
10770
operator fun invoke(rawType: Class<*>, kmClass: KmClass): KmExecutable<*>? {
10871
// If this is a value class, the "constructor" will actually be a static creator function
109-
val constructorsBySignature =
72+
val constructorsBySignature: Map<String, Invokable> =
11073
if (kmClass.isValue) {
11174
// kmConstructorSignature is something like constructor-impl(I)I
11275
// Need to look up the matching static function
11376
rawType.declaredMethods
11477
.filter { Modifier.isStatic(it.modifiers) }
115-
.associateBy { it.jvmMethodSignature }
78+
.associateBy({ it.jvmMethodSignature }, { Invokable.InvokableMethod(it) })
11679
} else {
117-
rawType.declaredConstructors.associateBy { it.jvmMethodSignature }
80+
rawType.declaredConstructors.associateBy(
81+
{ it.jvmMethodSignature },
82+
{ Invokable.InvokableConstructor(it) },
83+
)
11884
}
11985
val kmConstructor = kmClass.constructors.find { !it.isSecondary } ?: return null
12086
val kmConstructorSignature = kmConstructor.signature?.toString() ?: return null
12187
val jvmConstructor = constructorsBySignature[kmConstructorSignature] ?: return null
12288

123-
val parameterAnnotations = jvmConstructor.parameterAnnotations
124-
val parameterTypes = jvmConstructor.parameterTypes
12589
val parameters =
12690
kmConstructor.valueParameters.withIndex().map { (index, kmParam) ->
12791
// Check if this parameter's type is a value class
@@ -130,41 +94,45 @@ internal sealed class KmExecutable<T : Executable> {
13094
KtParameter(
13195
kmParam,
13296
index,
133-
parameterTypes[index],
134-
parameterAnnotations[index].toList(),
97+
jvmConstructor.parameterTypes[index],
98+
jvmConstructor.parameterAnnotations[index].toList(),
13599
parameterValueClassBoxer,
136100
parameterValueClassUnboxer,
137101
)
138102
}
139103
val anyOptional = parameters.any { it.declaresDefaultValue }
140104
val actualConstructor =
141105
if (anyOptional) {
142-
val defaultsSignature = jvmConstructor.defaultsSignature()
143-
constructorsBySignature[defaultsSignature] ?: return null
106+
constructorsBySignature[jvmConstructor.defaultsSignature()] ?: return null
144107
} else {
145108
jvmConstructor
146109
}
147110

148-
actualConstructor.isAccessible = true
111+
actualConstructor.setAccessible()
149112

150113
return if (kmClass.isValue) {
151114
// Things get quirky here. KM will return the primary constructor for the value class
152115
// as a constructor-impl static function, BUT this function always only returns the
153-
// underlying
154-
// type. What we want is the boxed type, so we need to be able to invoke the constructor and
155-
// then be able to pass it on to the box-impl function to get the full instance.
156-
// We can't just skip ahead and use only the box-impl function because the constructor is
157-
// the only one that handles default values
158-
159-
val boxImpl = constructorsBySignature.entries.first { it.key.startsWith("box-impl") }.value
116+
// underlying type. What we want is the boxed type, so we need to be able to invoke the
117+
// constructor and then be able to pass it on to the box-impl function to get the full
118+
// instance. We can't just skip ahead and use only the box-impl function because the
119+
// constructor is the only one that handles default values
120+
121+
val boxImpl =
122+
constructorsBySignature.entries.first { it.key.startsWith("box-impl") }.value
123+
as Invokable.InvokableMethod
160124
KmExecutableFunction(
161-
actualConstructor as Method,
125+
(actualConstructor as Invokable.InvokableMethod).method,
162126
parameters,
163127
anyOptional,
164-
boxImpl as Method,
128+
boxImpl.method,
165129
)
166130
} else {
167-
KmExecutableConstructor(actualConstructor as Constructor<*>, parameters, anyOptional)
131+
KmExecutableConstructor(
132+
(actualConstructor as Invokable.InvokableConstructor).constructor,
133+
parameters,
134+
anyOptional,
135+
)
168136
}
169137
}
170138
}

0 commit comments

Comments
 (0)