Merge pull request #1080 from square/z/resolveRealTypes

Fix resolution of types in superclass settable properties
diff --git a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/AdapterGenerator.kt b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/AdapterGenerator.kt
index 8f7e406..d6be437 100644
--- a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/AdapterGenerator.kt
+++ b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/AdapterGenerator.kt
@@ -18,7 +18,6 @@
 import com.squareup.kotlinpoet.ARRAY
 import com.squareup.kotlinpoet.AnnotationSpec
 import com.squareup.kotlinpoet.CodeBlock
-import com.squareup.kotlinpoet.CodeBlock.Companion
 import com.squareup.kotlinpoet.FileSpec
 import com.squareup.kotlinpoet.FunSpec
 import com.squareup.kotlinpoet.INT
@@ -74,6 +73,9 @@
         // Because we generate redundant `out` variance for some generics and there's no way
         // for us to know when it's redundant.
         "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
         // like for stylistic reasons.
         "LocalVariableName"
@@ -478,8 +480,10 @@
           localConstructorProperty
       )
     } else {
-      // Standard constructor call. Can omit generics as they're inferred
-      result.addCode("«%L%T(", returnOrResultAssignment, originalTypeName.rawType())
+      // Standard constructor call. Don't omit generics for parameterized types even if they can be
+      // 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>()) {
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..08c630f 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 @@
 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.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 @@
   }
 
   val properties = mutableMapOf<String, TargetProperty>()
+
+  val resolvedTypes = mutableListOf<ResolvedTypeMapping>()
   val superTypes = appliedType.supertypes(types)
       .filterNot { supertype ->
         supertype.element.asClassName() == OBJECT_CLASS || // Don't load properties for java.lang.Object.
@@ -207,15 +212,53 @@
       }
       .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<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) {
       properties.putIfAbsent(name, property)
     }
@@ -243,16 +286,71 @@
       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`. */
 @KotlinPoetMetadataPreview
 private fun declaredProperties(
     constructor: TargetConstructor,
-    kotlinApi: TypeSpec
+    kotlinApi: TypeSpec,
+    allowedTypeVars: Set<TypeVariableName>,
+    currentClass: ClassName,
+    resolvedTypes: List<ResolvedTypeMapping>
 ): Map<String, TargetProperty> {
 
   val result = mutableMapOf<String, TargetProperty>()
   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 parameter = constructor.parameters[name]
     result[name] = TargetProperty(
@@ -380,3 +478,10 @@
     return getAnnotation(Metadata::class.java)
         ?: throw IllegalStateException("Not a kotlin type! $this")
   }
+
+private fun <E> Sequence<*>.cast(): Sequence<E> {
+  return map {
+    @Suppress("UNCHECKED_CAST")
+    it as E
+  }
+}
diff --git a/kotlin/tests/src/test/kotlin/com/squareup/moshi/kotlin/codegen/ComplexGenericsInheritanceTest.kt b/kotlin/tests/src/test/kotlin/com/squareup/moshi/kotlin/codegen/ComplexGenericsInheritanceTest.kt
new file mode 100644
index 0000000..a3a16f8
--- /dev/null
+++ b/kotlin/tests/src/test/kotlin/com/squareup/moshi/kotlin/codegen/ComplexGenericsInheritanceTest.kt
@@ -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>