Handle non-property parameters in kotlin code gen (#982)

* Add parameter-only param in multi-masks test

* Add type to target parameters for later reference

* Ensure parameters are ordered

* Reword AdapterGenerator fromJson logic to use sealed FromJsonComponent

This allows us to handle parameter-only types separately (needed for mask index calculation). Resolves #979

* Add more tests
This commit is contained in:
Zac Sweers
2019-10-30 21:06:04 -04:00
committed by Jesse Wilson
parent a384bd0429
commit 9a0294c3a0
6 changed files with 160 additions and 24 deletions

View File

@@ -28,6 +28,7 @@ import com.squareup.kotlinpoet.ParameterSpec
import com.squareup.kotlinpoet.ParameterizedTypeName import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.PropertySpec import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeSpec import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.TypeVariableName import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.asClassName import com.squareup.kotlinpoet.asClassName
@@ -38,6 +39,9 @@ import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter import com.squareup.moshi.JsonWriter
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.squareup.moshi.internal.Util import com.squareup.moshi.internal.Util
import com.squareup.moshi.kotlin.codegen.api.FromJsonComponent.ParameterOnly
import com.squareup.moshi.kotlin.codegen.api.FromJsonComponent.ParameterProperty
import com.squareup.moshi.kotlin.codegen.api.FromJsonComponent.PropertyOnly
import java.lang.reflect.Constructor import java.lang.reflect.Constructor
import java.lang.reflect.Type import java.lang.reflect.Type
@@ -60,6 +64,8 @@ internal class AdapterGenerator(
private val className = target.typeName.rawType() private val className = target.typeName.rawType()
private val visibility = target.visibility private val visibility = target.visibility
private val typeVariables = target.typeVariables private val typeVariables = target.typeVariables
private val targetConstructorParams = target.constructor.parameters
.mapKeys { (_, param) -> param.index }
private val nameAllocator = NameAllocator() private val nameAllocator = NameAllocator()
private val adapterName = "${className.simpleNames.joinToString(separator = "_")}JsonAdapter" private val adapterName = "${className.simpleNames.joinToString(separator = "_")}JsonAdapter"
@@ -196,8 +202,34 @@ internal class AdapterGenerator(
} }
} }
val propertiesByIndex = propertyList.asSequence()
.filter { it.hasConstructorParameter }
.associateBy { it.target.parameterIndex }
val components = mutableListOf<FromJsonComponent>()
// Add parameters (± properties) first, their index matters
for ((index, parameter) in targetConstructorParams) {
val property = propertiesByIndex[index]
if (property == null) {
components += ParameterOnly(parameter)
} else {
components += ParameterProperty(parameter, property)
}
}
// Now add the remaining properties that aren't parameters
for (property in propertyList) {
if (property.target.parameterIndex in targetConstructorParams) {
continue // Already handled
}
if (property.isTransient) {
continue // We don't care about these outside of constructor parameters
}
components += PropertyOnly(property)
}
// Calculate how many masks we'll need. Round up if it's not evenly divisible by 32 // Calculate how many masks we'll need. Round up if it's not evenly divisible by 32
val propertyCount = propertyList.count { it.hasConstructorParameter } val propertyCount = targetConstructorParams.size
val maskCount = if (propertyCount == 0) { val maskCount = if (propertyCount == 0) {
0 0
} else { } else {
@@ -207,8 +239,9 @@ internal class AdapterGenerator(
val maskNames = Array(maskCount) { index -> val maskNames = Array(maskCount) { index ->
nameAllocator.newName("mask$index") nameAllocator.newName("mask$index")
} }
val useConstructorDefaults = nonTransientProperties.any { it.hasConstructorDefault } val useDefaultsConstructor = components.filterIsInstance<ParameterComponent>()
if (useConstructorDefaults) { .any { it.parameter.hasDefault }
if (useDefaultsConstructor) {
// Initialize all our masks, defaulting to fully unset (-1) // Initialize all our masks, defaulting to fully unset (-1)
for (maskName in maskNames) { for (maskName in maskNames) {
result.addStatement("var %L = -1", maskName) result.addStatement("var %L = -1", maskName)
@@ -243,14 +276,21 @@ internal class AdapterGenerator(
maskNameIndex++ maskNameIndex++
} }
} }
for (property in propertyList) {
if (property.isTransient) { for (input in components) {
if (property.hasConstructorParameter) { if (input is ParameterOnly ||
updateMaskIndexes() (input is ParameterProperty && input.property.isTransient)) {
constructorPropertyTypes += property.target.type.asTypeBlock() updateMaskIndexes()
} constructorPropertyTypes += input.type.asTypeBlock()
continue
} else if (input is PropertyOnly && input.property.isTransient) {
continue continue
} }
// We've removed all parameter-only types by this point
val property = (input as PropertyComponent).property
// Proceed as usual
if (property.hasLocalIsPresentName || property.hasConstructorDefault) { if (property.hasLocalIsPresentName || property.hasConstructorDefault) {
result.beginControlFlow("%L ->", propertyIndex) result.beginControlFlow("%L ->", propertyIndex)
if (property.delegateKey.nullable) { if (property.delegateKey.nullable) {
@@ -299,8 +339,6 @@ internal class AdapterGenerator(
result.addStatement("%N.endObject()", readerParam) result.addStatement("%N.endObject()", readerParam)
var separator = "\n" var separator = "\n"
val parameterProperties = propertyList.filter { it.hasConstructorParameter }
val useDefaultsConstructor = parameterProperties.any { it.hasDefault }
val resultName = nameAllocator.newName("result") val resultName = nameAllocator.newName("result")
val hasNonConstructorProperties = nonTransientProperties.any { !it.hasConstructorParameter } val hasNonConstructorProperties = nonTransientProperties.any { !it.hasConstructorParameter }
@@ -352,25 +390,29 @@ internal class AdapterGenerator(
result.addCode("«%L%T(", returnOrResultAssignment, originalTypeName) result.addCode("«%L%T(", returnOrResultAssignment, originalTypeName)
} }
for (property in parameterProperties) { for (input in components.filterIsInstance<ParameterComponent>()) {
result.addCode(separator) result.addCode(separator)
if (useDefaultsConstructor) { if (useDefaultsConstructor) {
if (property.isTransient) { if (input is ParameterOnly || (input is ParameterProperty && input.property.isTransient)) {
// We have to use the default primitive for the available type in order for // We have to use the default primitive for the available type in order for
// invokeDefaultConstructor to properly invoke it. Just using "null" isn't safe because // invokeDefaultConstructor to properly invoke it. Just using "null" isn't safe because
// the transient type may be a primitive type. // the transient type may be a primitive type.
result.addCode(property.target.type.rawType().defaultPrimitiveValue()) result.addCode(input.type.rawType().defaultPrimitiveValue())
} else { } else {
result.addCode("%N", property.localName) result.addCode("%N", (input as ParameterProperty).property.localName)
} }
} else { } else if (input !is ParameterOnly) {
val property = (input as ParameterProperty).property
result.addCode("%N = %N", property.name, property.localName) result.addCode("%N = %N", property.name, property.localName)
} }
if (!property.isTransient && property.isRequired) { if (input is PropertyComponent) {
val missingPropertyBlock = val property = input.property
CodeBlock.of("%T.missingProperty(%S, %S, %N)", if (!property.isTransient && property.isRequired) {
MOSHI_UTIL, property.localName, property.jsonName, readerParam) val missingPropertyBlock =
result.addCode(" ?: throw·%L", missingPropertyBlock) CodeBlock.of("%T.missingProperty(%S, %S, %N)",
MOSHI_UTIL, property.localName, property.jsonName, readerParam)
result.addCode(" ?: throw·%L", missingPropertyBlock)
}
} }
separator = ",\n" separator = ",\n"
} }
@@ -430,3 +472,40 @@ internal class AdapterGenerator(
return result.build() return result.build()
} }
} }
private interface PropertyComponent {
val property: PropertyGenerator
val type: TypeName
}
private interface ParameterComponent {
val parameter: TargetParameter
val type: TypeName
}
/**
* Type hierarchy for describing fromJson() components. Specifically - parameters, properties, and
* parameter properties. All three of these scenarios participate in fromJson() parsing.
*/
private sealed class FromJsonComponent {
abstract val type: TypeName
data class ParameterOnly(
override val parameter: TargetParameter
) : FromJsonComponent(), ParameterComponent {
override val type: TypeName = parameter.type
}
data class PropertyOnly(
override val property: PropertyGenerator
) : FromJsonComponent(), PropertyComponent {
override val type: TypeName = property.target.type
}
data class ParameterProperty(
override val parameter: TargetParameter,
override val property: PropertyGenerator
) : FromJsonComponent(), ParameterComponent, PropertyComponent {
override val type: TypeName = parameter.type
}
}

View File

@@ -19,7 +19,7 @@ import com.squareup.kotlinpoet.KModifier
/** A constructor in user code that should be called by generated code. */ /** A constructor in user code that should be called by generated code. */
internal data class TargetConstructor( internal data class TargetConstructor(
val parameters: Map<String, TargetParameter>, val parameters: LinkedHashMap<String, TargetParameter>,
val visibility: KModifier val visibility: KModifier
) { ) {
init { init {

View File

@@ -16,11 +16,13 @@
package com.squareup.moshi.kotlin.codegen.api package com.squareup.moshi.kotlin.codegen.api
import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.TypeName
/** A parameter in user code that should be populated by generated code. */ /** A parameter in user code that should be populated by generated code. */
internal data class TargetParameter( internal data class TargetParameter(
val name: String, val name: String,
val index: Int, val index: Int,
val type: TypeName,
val hasDefault: Boolean, val hasDefault: Boolean,
val jsonName: String? = null, val jsonName: String? = null,
val qualifiers: Set<AnnotationSpec>? = null val qualifiers: Set<AnnotationSpec>? = null

View File

@@ -72,12 +72,13 @@ private fun Collection<KModifier>.visibility(): KModifier {
internal fun primaryConstructor(kotlinApi: TypeSpec, elements: Elements): TargetConstructor? { internal fun primaryConstructor(kotlinApi: TypeSpec, elements: Elements): TargetConstructor? {
val primaryConstructor = kotlinApi.primaryConstructor ?: return null val primaryConstructor = kotlinApi.primaryConstructor ?: return null
val parameters = mutableMapOf<String, TargetParameter>() val parameters = LinkedHashMap<String, TargetParameter>()
for ((index, parameter) in primaryConstructor.parameters.withIndex()) { for ((index, parameter) in primaryConstructor.parameters.withIndex()) {
val name = parameter.name val name = parameter.name
parameters[name] = TargetParameter( parameters[name] = TargetParameter(
name = name, name = name,
index = index, index = index,
type = parameter.type,
hasDefault = parameter.defaultValue != null, hasDefault = parameter.defaultValue != null,
qualifiers = parameter.annotations.qualifiers(elements), qualifiers = parameter.annotations.qualifiers(elements),
jsonName = parameter.annotations.jsonName() jsonName = parameter.annotations.jsonName()

View File

@@ -327,6 +327,7 @@ class DualKotlinTest(useReflection: Boolean) {
@Test fun multipleConstructors() { @Test fun multipleConstructors() {
val adapter = moshi.adapter<MultipleConstructorsB>() val adapter = moshi.adapter<MultipleConstructorsB>()
//language=JSON
assertThat(adapter.toJson(MultipleConstructorsB(6))).isEqualTo("""{"f":{"f":6},"b":6}""") assertThat(adapter.toJson(MultipleConstructorsB(6))).isEqualTo("""{"f":{"f":6},"b":6}""")
@Language("JSON") @Language("JSON")
@@ -342,6 +343,59 @@ class DualKotlinTest(useReflection: Boolean) {
class MultipleConstructorsB(val f: MultipleConstructorsA = MultipleConstructorsA(5), val b: Int) { class MultipleConstructorsB(val f: MultipleConstructorsA = MultipleConstructorsA(5), val b: Int) {
constructor(f: Int, b: Int = 6): this(MultipleConstructorsA(f), b) constructor(f: Int, b: Int = 6): this(MultipleConstructorsA(f), b)
} }
@Test fun `multiple non-property parameters`() {
val adapter = moshi.adapter<MultipleNonPropertyParameters>()
@Language("JSON")
val testJson = """{"prop":7}"""
assertThat(adapter.toJson(MultipleNonPropertyParameters(7))).isEqualTo(testJson)
val result = adapter.fromJson(testJson)!!
assertThat(result.prop).isEqualTo(7)
}
@JsonClass(generateAdapter = true)
class MultipleNonPropertyParameters(
val prop: Int,
param1: Int = 1,
param2: Int = 2
) {
init {
// Ensure the params always uses their default value
require(param1 == 1)
require(param2 == 2)
}
}
// Tests the case of multiple parameters with no parameter properties.
@Test fun `only multiple non-property parameters`() {
val adapter = moshi.adapter<OnlyMultipleNonPropertyParameters>()
@Language("JSON")
val testJson = """{"prop":7}"""
assertThat(adapter.toJson(OnlyMultipleNonPropertyParameters().apply { prop = 7 }))
.isEqualTo(testJson)
val result = adapter.fromJson(testJson)!!
assertThat(result.prop).isEqualTo(7)
}
@JsonClass(generateAdapter = true)
class OnlyMultipleNonPropertyParameters(
param1: Int = 1,
param2: Int = 2
) {
init {
// Ensure the params always uses their default value
require(param1 == 1)
require(param2 == 2)
}
var prop: Int = 0
}
} }
// Has to be outside since inline classes are only allowed on top level // Has to be outside since inline classes are only allowed on top level

View File

@@ -75,7 +75,7 @@ class MultipleMasks(
val arg17: Long = 17, val arg17: Long = 17,
val arg18: Long = 18, val arg18: Long = 18,
val arg19: Long = 19, val arg19: Long = 19,
val arg20: Long = 20, @Suppress("UNUSED_PARAMETER") arg20: Long = 20,
val arg21: Long = 21, val arg21: Long = 21,
val arg22: Long = 22, val arg22: Long = 22,
val arg23: Long = 23, val arg23: Long = 23,