diff --git a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/metadata.kt b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/metadata.kt index 87a2201..2a1e2b9 100644 --- a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/metadata.kt +++ b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/metadata.kt @@ -18,8 +18,10 @@ package com.squareup.moshi.kotlin.codegen import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.ParameterizedTypeName import com.squareup.kotlinpoet.TypeName import com.squareup.kotlinpoet.TypeSpec +import com.squareup.kotlinpoet.TypeVariableName import com.squareup.kotlinpoet.asClassName import com.squareup.kotlinpoet.asTypeName 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.ElementKind import javax.lang.model.element.TypeElement +import javax.lang.model.type.DeclaredType import javax.lang.model.util.Elements import javax.lang.model.util.Types import javax.tools.Diagnostic.Kind.ERROR @@ -192,6 +195,8 @@ internal fun targetType(messager: Messager, } val properties = mutableMapOf() + + val resolvedTypes = mutableListOf() val superTypes = appliedType.supertypes(types) .filterNot { supertype -> 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 -> // 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 kotlinApi } else { 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 + // class Bar + // + // We will store {Foo : {T : [String]}}. + // + // Then when we look at Bar 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() + .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) { properties.putIfAbsent(name, property) } @@ -243,16 +286,70 @@ internal fun targetType(messager: Messager, 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) + +private fun resolveTypeArgs( + targetClass: ClassName, + propertyType: TypeName, + resolvedTypes: List, + 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`. */ @KotlinPoetMetadataPreview private fun declaredProperties( constructor: TargetConstructor, - kotlinApi: TypeSpec + kotlinApi: TypeSpec, + allowTypeVars: Boolean, + currentClass: ClassName, + resolvedTypes: List ): Map { val result = mutableMapOf() 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 parameter = constructor.parameters[name] result[name] = TargetProperty( @@ -380,3 +477,10 @@ internal val TypeElement.metadata: Metadata return getAnnotation(Metadata::class.java) ?: throw IllegalStateException("Not a kotlin type! $this") } + +private fun Sequence<*>.cast(): Sequence { + return map { + @Suppress("UNCHECKED_CAST") + it as E + } +}