Resolve type arguments correctly for supertype settable properties

Resolves #1046
This commit is contained in:
Zac Sweers
2020-01-18 22:52:18 -05:00
parent a73b932429
commit 4e84451d6e

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,
allowTypeVars = typeVariables.isNotEmpty(),
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,70 @@ 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>,
allowTypeVars: Boolean,
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, allowTypeVars, entryStartIndex - 1)
}
allowTypeVars -> {
// TODO this is ok but it would be nice to verify the typevar here is the one defined on our target class
resolvedType
}
else -> error("Could not find $resolvedType in $resolvedTypes")
}
}
/** 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,
allowTypeVars: Boolean,
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,
allowTypeVars = allowTypeVars
)
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 +477,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
}
}