Fix incorrect type variance being applied on generated adapters (#1010)

* Fix: force object type for type arguments

* Add outDeclaration regression test for #1009

* Create mapTypesUtility for reusing recursive type mapping

* Strip typevar variance where appropriate

Resolves #1009
This commit is contained in:
Zac Sweers
2019-11-08 14:35:13 -08:00
committed by GitHub
parent 3c0e3edff3
commit d25abb1ee5
5 changed files with 90 additions and 44 deletions

View File

@@ -69,7 +69,7 @@ internal class AdapterGenerator(
private val nameAllocator = NameAllocator()
private val adapterName = "${className.simpleNames.joinToString(separator = "_")}JsonAdapter"
private val originalTypeName = target.typeName
private val originalTypeName = target.typeName.stripTypeVarVariance()
private val originalRawTypeName = originalTypeName.rawType()
private val moshiParam = ParameterSpec.builder(
@@ -130,7 +130,7 @@ internal class AdapterGenerator(
result.superclass(jsonAdapterTypeName)
if (typeVariables.isNotEmpty()) {
result.addTypeVariables(typeVariables)
result.addTypeVariables(typeVariables.map { it.stripTypeVarVariance() as TypeVariableName })
}
// TODO make this configurable. Right now it just matches the source model

View File

@@ -29,11 +29,15 @@ import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.LONG
import com.squareup.kotlinpoet.NOTHING
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.SHORT
import com.squareup.kotlinpoet.STAR
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.UNIT
import com.squareup.kotlinpoet.WildcardTypeName
import com.squareup.kotlinpoet.asTypeName
import kotlin.reflect.KClass
internal fun TypeName.rawType(): ClassName {
return when (this) {
@@ -95,3 +99,48 @@ internal fun KModifier.checkIsVisibility() {
"Visibility must be one of ${(0..ordinal).joinToString { KModifier.values()[it].name }}. Is $name"
}
}
internal inline fun <reified T: TypeName> TypeName.mapTypes(noinline transform: T.() -> TypeName?): TypeName {
return mapTypes(T::class, transform)
}
@Suppress("UNCHECKED_CAST")
internal fun <T: TypeName> TypeName.mapTypes(target: KClass<T>, transform: T.() -> TypeName?): TypeName {
if (target.java == javaClass) {
return (this as T).transform() ?: return this
}
return when (this) {
is ClassName -> this
is ParameterizedTypeName -> {
(rawType.mapTypes(target, transform) as ClassName).parameterizedBy(typeArguments.map { it.mapTypes(target, transform) })
.copy(nullable = isNullable, annotations = annotations)
}
is TypeVariableName -> {
copy(bounds = bounds.map { it.mapTypes(target, transform) })
}
is WildcardTypeName -> {
// TODO Would be nice if KotlinPoet modeled these easier.
// Producer type - empty inTypes, single element outTypes
// Consumer type - single element inTypes, single ANY element outType.
when {
this == STAR -> this
outTypes.isNotEmpty() && inTypes.isEmpty() -> {
WildcardTypeName.producerOf(outTypes[0].mapTypes(target, transform))
.copy(nullable = isNullable, annotations = annotations)
}
inTypes.isNotEmpty() -> {
WildcardTypeName.consumerOf(inTypes[0].mapTypes(target, transform))
.copy(nullable = isNullable, annotations = annotations)
}
else -> throw UnsupportedOperationException("Not possible.")
}
}
else -> throw UnsupportedOperationException("Type '${javaClass.simpleName}' is illegal. Only classes, parameterized types, wildcard types, or type variables are allowed.")
}
}
internal fun TypeName.stripTypeVarVariance(): TypeName {
return mapTypes<TypeVariableName> {
TypeVariableName(name = name, bounds = bounds.map { it.mapTypes(TypeVariableName::stripTypeVarVariance) }, variance = null)
}
}

View File

@@ -48,6 +48,7 @@ import com.squareup.moshi.kotlin.codegen.api.TargetConstructor
import com.squareup.moshi.kotlin.codegen.api.TargetParameter
import com.squareup.moshi.kotlin.codegen.api.TargetProperty
import com.squareup.moshi.kotlin.codegen.api.TargetType
import com.squareup.moshi.kotlin.codegen.api.mapTypes
import java.lang.annotation.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
@@ -59,6 +60,7 @@ import javax.lang.model.element.TypeElement
import javax.lang.model.util.Elements
import javax.lang.model.util.Types
import javax.tools.Diagnostic
import kotlin.reflect.KClass
private val JSON_QUALIFIER = JsonQualifier::class.java
private val JSON = Json::class.asClassName()
@@ -320,46 +322,19 @@ private fun String.escapeDollarSigns(): String {
return replace("\$", "\${\'\$\'}")
}
private fun TypeName.unwrapTypeAlias(): TypeName {
return when (this) {
is ClassName -> {
tag<TypeNameAliasTag>()?.type?.let { unwrappedType ->
// If any type is nullable, then the whole thing is nullable
var isAnyNullable = isNullable
// Keep track of all annotations across type levels. Sort them too for consistency.
val runningAnnotations = TreeSet<AnnotationSpec>(compareBy { it.toString() }).apply {
addAll(annotations)
}
val nestedUnwrappedType = unwrappedType.unwrapTypeAlias()
runningAnnotations.addAll(nestedUnwrappedType.annotations)
isAnyNullable = isAnyNullable || nestedUnwrappedType.isNullable
nestedUnwrappedType.copy(nullable = isAnyNullable, annotations = runningAnnotations.toList())
} ?: this
}
is ParameterizedTypeName -> {
return (rawType.unwrapTypeAlias() as ClassName).parameterizedBy(typeArguments.map { it.unwrapTypeAlias() })
.copy(nullable = isNullable, annotations = annotations)
}
is TypeVariableName -> {
return copy(bounds = bounds.map { it.unwrapTypeAlias() })
}
is WildcardTypeName -> {
// TODO Would be nice if KotlinPoet modeled these easier.
// Producer type - empty inTypes, single element outTypes
// Consumer type - single element inTypes, single ANY element outType.
return when {
this == STAR -> this
outTypes.isNotEmpty() && inTypes.isEmpty() -> {
WildcardTypeName.producerOf(outTypes[0].unwrapTypeAlias())
.copy(nullable = isNullable, annotations = annotations)
}
inTypes.isNotEmpty() -> {
WildcardTypeName.consumerOf(inTypes[0].unwrapTypeAlias())
.copy(nullable = isNullable, annotations = annotations)
}
else -> throw UnsupportedOperationException("Not possible.")
internal fun TypeName.unwrapTypeAlias(): TypeName {
return mapTypes<ClassName> {
tag<TypeNameAliasTag>()?.type?.let { unwrappedType ->
// If any type is nullable, then the whole thing is nullable
var isAnyNullable = isNullable
// Keep track of all annotations across type levels. Sort them too for consistency.
val runningAnnotations = TreeSet<AnnotationSpec>(compareBy { it.toString() }).apply {
addAll(annotations)
}
val nestedUnwrappedType = unwrappedType.unwrapTypeAlias()
runningAnnotations.addAll(nestedUnwrappedType.annotations)
isAnyNullable = isAnyNullable || nestedUnwrappedType.isNullable
nestedUnwrappedType.copy(nullable = isAnyNullable, annotations = runningAnnotations.toList())
}
else -> throw UnsupportedOperationException("Type '${javaClass.simpleName}' is illegal. Only classes, parameterized types, wildcard types, or type variables are allowed.")
}
}