Merge pull request #1080 from square/z/resolveRealTypes

Fix resolution of types in superclass settable properties
This commit is contained in:
Ryan Harter
2020-05-26 17:53:45 -05:00
committed by GitHub
3 changed files with 249 additions and 8 deletions

View File

@@ -18,7 +18,6 @@ package com.squareup.moshi.kotlin.codegen.api
import com.squareup.kotlinpoet.ARRAY import com.squareup.kotlinpoet.ARRAY
import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.CodeBlock import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.CodeBlock.Companion
import com.squareup.kotlinpoet.FileSpec import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.INT import com.squareup.kotlinpoet.INT
@@ -74,6 +73,9 @@ internal class AdapterGenerator(
// Because we generate redundant `out` variance for some generics and there's no way // Because we generate redundant `out` variance for some generics and there's no way
// for us to know when it's redundant. // for us to know when it's redundant.
"REDUNDANT_PROJECTION", "REDUNDANT_PROJECTION",
// Because we may generate redundant explicit types for local vars with default values.
// Example: 'var fooSet: Boolean = false'
"RedundantExplicitType",
// NameAllocator will just add underscores to differentiate names, which Kotlin doesn't // NameAllocator will just add underscores to differentiate names, which Kotlin doesn't
// like for stylistic reasons. // like for stylistic reasons.
"LocalVariableName" "LocalVariableName"
@@ -478,8 +480,10 @@ internal class AdapterGenerator(
localConstructorProperty localConstructorProperty
) )
} else { } else {
// Standard constructor call. Can omit generics as they're inferred // Standard constructor call. Don't omit generics for parameterized types even if they can be
result.addCode("«%L%T(", returnOrResultAssignment, originalTypeName.rawType()) // inferred, as calculating the right condition for inference exceeds the value gained from
// being less pedantic.
result.addCode("«%L%T(", returnOrResultAssignment, originalTypeName)
} }
for (input in components.filterIsInstance<ParameterComponent>()) { for (input in components.filterIsInstance<ParameterComponent>()) {

View File

@@ -18,8 +18,10 @@ package com.squareup.moshi.kotlin.codegen
import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.KModifier import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.TypeName import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeSpec import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.asClassName import com.squareup.kotlinpoet.asClassName
import com.squareup.kotlinpoet.asTypeName import com.squareup.kotlinpoet.asTypeName
import com.squareup.kotlinpoet.metadata.ImmutableKmConstructor import com.squareup.kotlinpoet.metadata.ImmutableKmConstructor
@@ -53,6 +55,7 @@ import javax.lang.model.element.AnnotationMirror
import javax.lang.model.element.Element import javax.lang.model.element.Element
import javax.lang.model.element.ElementKind import javax.lang.model.element.ElementKind
import javax.lang.model.element.TypeElement import javax.lang.model.element.TypeElement
import javax.lang.model.type.DeclaredType
import javax.lang.model.util.Elements import javax.lang.model.util.Elements
import javax.lang.model.util.Types import javax.lang.model.util.Types
import javax.tools.Diagnostic.Kind.ERROR import javax.tools.Diagnostic.Kind.ERROR
@@ -192,6 +195,8 @@ internal fun targetType(messager: Messager,
} }
val properties = mutableMapOf<String, TargetProperty>() val properties = mutableMapOf<String, TargetProperty>()
val resolvedTypes = mutableListOf<ResolvedTypeMapping>()
val superTypes = appliedType.supertypes(types) val superTypes = appliedType.supertypes(types)
.filterNot { supertype -> .filterNot { supertype ->
supertype.element.asClassName() == OBJECT_CLASS || // Don't load properties for java.lang.Object. supertype.element.asClassName() == OBJECT_CLASS || // Don't load properties for java.lang.Object.
@@ -207,15 +212,53 @@ internal fun targetType(messager: Messager,
} }
.associateWithTo(LinkedHashMap()) { supertype -> .associateWithTo(LinkedHashMap()) { supertype ->
// Load the kotlin API cache into memory eagerly so we can reuse the parsed APIs // Load the kotlin API cache into memory eagerly so we can reuse the parsed APIs
if (supertype.element == element) { val api = if (supertype.element == element) {
// We've already parsed this api above, reuse it // We've already parsed this api above, reuse it
kotlinApi kotlinApi
} else { } else {
cachedClassInspector.toTypeSpec(supertype.element) cachedClassInspector.toTypeSpec(supertype.element)
} }
val apiSuperClass = api.superclass
if (apiSuperClass is ParameterizedTypeName) {
//
// This extends a typed generic superclass. We want to construct a mapping of the
// superclass typevar names to their materialized types here.
//
// class Foo extends Bar<String>
// class Bar<T>
//
// We will store {Foo : {T : [String]}}.
//
// Then when we look at Bar<T> later, we'll look up to the descendent Foo and extract its
// materialized type from there.
//
val superSuperClass = supertype.element.superclass as DeclaredType
// Convert to an element and back to wipe the typed generics off of this
val untyped = superSuperClass.asElement().asType().asTypeName() as ParameterizedTypeName
resolvedTypes += ResolvedTypeMapping(
target = untyped.rawType,
args = untyped.typeArguments.asSequence()
.cast<TypeVariableName>()
.map(TypeVariableName::name)
.zip(apiSuperClass.typeArguments.asSequence())
.associate { it }
)
}
return@associateWithTo api
} }
for (supertypeApi in superTypes.values) {
val supertypeProperties = declaredProperties(constructor, supertypeApi) for ((localAppliedType, supertypeApi) in superTypes.entries) {
val appliedClassName = localAppliedType.element.asClassName()
val supertypeProperties = declaredProperties(
constructor = constructor,
kotlinApi = supertypeApi,
allowedTypeVars = typeVariables.toSet(),
currentClass = appliedClassName,
resolvedTypes = resolvedTypes
)
for ((name, property) in supertypeProperties) { for ((name, property) in supertypeProperties) {
properties.putIfAbsent(name, property) properties.putIfAbsent(name, property)
} }
@@ -243,16 +286,71 @@ internal fun targetType(messager: Messager,
visibility = resolvedVisibility) visibility = resolvedVisibility)
} }
/**
* Represents a resolved raw class to type arguments where [args] are a map of the parent type var
* name to its resolved [TypeName].
*/
private data class ResolvedTypeMapping(val target: ClassName, val args: Map<String, TypeName>)
private fun resolveTypeArgs(
targetClass: ClassName,
propertyType: TypeName,
resolvedTypes: List<ResolvedTypeMapping>,
allowedTypeVars: Set<TypeVariableName>,
entryStartIndex: Int = resolvedTypes.indexOfLast { it.target == targetClass }
): TypeName {
val unwrappedType = propertyType.unwrapTypeAlias()
if (unwrappedType !is TypeVariableName) {
return unwrappedType
} else if (entryStartIndex == -1) {
return unwrappedType
}
val targetMappingIndex = resolvedTypes[entryStartIndex]
val targetMappings = targetMappingIndex.args
// Try to resolve the real type of this property based on mapped generics in the subclass.
// We need to us a non-nullable version for mapping since we're just mapping based on raw java
// type vars, but then can re-copy nullability back if it is found.
val resolvedType = targetMappings[unwrappedType.name]
?.copy(nullable = unwrappedType.isNullable)
?: unwrappedType
return when {
resolvedType !is TypeVariableName -> resolvedType
entryStartIndex != 0 -> {
// We need to go deeper
resolveTypeArgs(targetClass, resolvedType, resolvedTypes, allowedTypeVars, entryStartIndex - 1)
}
resolvedType.copy(nullable = false) in allowedTypeVars -> {
// This is a generic type in the top-level declared class. This is fine to leave in because
// this will be handled by the `Type` array passed in at runtime.
resolvedType
}
else -> error("Could not find $resolvedType in $resolvedTypes. Also not present in allowable top-level type vars $allowedTypeVars")
}
}
/** Returns the properties declared by `typeElement`. */ /** Returns the properties declared by `typeElement`. */
@KotlinPoetMetadataPreview @KotlinPoetMetadataPreview
private fun declaredProperties( private fun declaredProperties(
constructor: TargetConstructor, constructor: TargetConstructor,
kotlinApi: TypeSpec kotlinApi: TypeSpec,
allowedTypeVars: Set<TypeVariableName>,
currentClass: ClassName,
resolvedTypes: List<ResolvedTypeMapping>
): Map<String, TargetProperty> { ): Map<String, TargetProperty> {
val result = mutableMapOf<String, TargetProperty>() val result = mutableMapOf<String, TargetProperty>()
for (initialProperty in kotlinApi.propertySpecs) { for (initialProperty in kotlinApi.propertySpecs) {
val property = initialProperty.toBuilder(type = initialProperty.type.unwrapTypeAlias()).build() val resolvedType = resolveTypeArgs(
targetClass = currentClass,
propertyType = initialProperty.type,
resolvedTypes = resolvedTypes,
allowedTypeVars = allowedTypeVars
)
val property = initialProperty.toBuilder(type = resolvedType).build()
val name = property.name val name = property.name
val parameter = constructor.parameters[name] val parameter = constructor.parameters[name]
result[name] = TargetProperty( result[name] = TargetProperty(
@@ -380,3 +478,10 @@ internal val TypeElement.metadata: Metadata
return getAnnotation(Metadata::class.java) return getAnnotation(Metadata::class.java)
?: throw IllegalStateException("Not a kotlin type! $this") ?: throw IllegalStateException("Not a kotlin type! $this")
} }
private fun <E> Sequence<*>.cast(): Sequence<E> {
return map {
@Suppress("UNCHECKED_CAST")
it as E
}
}

View File

@@ -0,0 +1,132 @@
@file:Suppress("UNUSED", "UNUSED_PARAMETER")
package com.squareup.moshi.kotlin.codegen
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.adapter
import org.assertj.core.api.Assertions.assertThat
import org.intellij.lang.annotations.Language
import org.junit.Test
@ExperimentalStdlibApi
class ComplexGenericsInheritanceTest {
private val moshi = Moshi.Builder().build()
@Test
fun simple() {
val adapter = moshi.adapter<PersonResponse>()
@Language("JSON")
val json = """{"data":{"name":"foo"},"data2":"bar","data3":"baz"}"""
val instance = adapter.fromJson(json)!!
val testInstance = PersonResponse().apply {
data = Person("foo")
}
assertThat(instance).isEqualTo(testInstance)
assertThat(adapter.toJson(instance)).isEqualTo(json)
}
@Test
fun nested() {
val adapter = moshi.adapter<NestedPersonResponse>()
@Language("JSON")
val json = """{"data":{"name":"foo"},"data2":"bar","data3":"baz"}"""
val instance = adapter.fromJson(json)!!
val testInstance = NestedPersonResponse().apply {
data = Person("foo")
}
assertThat(instance).isEqualTo(testInstance)
assertThat(adapter.toJson(instance)).isEqualTo(json)
}
@Test
fun untyped() {
val adapter = moshi.adapter<UntypedNestedPersonResponse<Person>>()
@Language("JSON")
val json = """{"data":{"name":"foo"},"data2":"bar","data3":"baz"}"""
val instance = adapter.fromJson(json)!!
val testInstance = UntypedNestedPersonResponse<Person>().apply {
data = Person("foo")
}
assertThat(instance).isEqualTo(testInstance)
assertThat(adapter.toJson(instance)).isEqualTo(json)
}
@Test
fun complex() {
val adapter = moshi.adapter<Layer4<Person, UntypedNestedPersonResponse<Person>>>()
@Language("JSON")
val json = """{"layer4E":{"name":"layer4E"},"layer4F":{"data":{"name":"layer4F"},"data2":"layer4F","data3":"layer4F"},"layer3C":[1,2,3],"layer3D":"layer3D","layer2":"layer2","layer1":"layer1"}"""
val instance = adapter.fromJson(json)!!
val testInstance = Layer4(
layer4E = Person("layer4E"),
layer4F = UntypedNestedPersonResponse<Person>().apply {
data = Person("layer4F")
data2 = "layer4F"
data3 = "layer4F"
}
).apply {
layer3C = listOf(1, 2, 3)
layer3D = "layer3D"
layer2 = "layer2"
layer1 = "layer1"
}
assertThat(instance).isEqualTo(testInstance)
assertThat(adapter.toJson(testInstance)).isEqualTo(json)
}
}
open class ResponseWithSettableProperty<T, R> {
var data: T? = null
var data2: R? = null
var data3: R? = null
}
interface Personable
@JsonClass(generateAdapter = true)
data class Person(val name: String) : Personable
@JsonClass(generateAdapter = true)
data class PersonResponse(
val extra: String? = null) : ResponseWithSettableProperty<Person, String>()
abstract class NestedResponse<T : Personable> : ResponseWithSettableProperty<T, String>()
@JsonClass(generateAdapter = true)
data class NestedPersonResponse(val extra: String? = null) : NestedResponse<Person>()
@JsonClass(generateAdapter = true)
data class UntypedNestedPersonResponse<T : Personable>(
val extra: String? = null
) : NestedResponse<T>()
interface LayerInterface<I>
abstract class Layer1<A> {
var layer1: A? = null
}
abstract class Layer2<B> : Layer1<B>(), LayerInterface<B> {
var layer2: B? = null
}
abstract class Layer3<C, D> : Layer2<D>() {
var layer3C: C? = null
var layer3D: D? = null
}
@JsonClass(generateAdapter = true)
data class Layer4<E : Personable, F>(
val layer4E: E,
val layer4F: F? = null
) : Layer3<List<Int>, String>(), LayerInterface<String>