Fix nullable properties (#992)

* Fix missing nullability/annotations when unwrapping typealiases

Fixes #990

* Add extra nullable properties test + more nullability in alias test

* Add more complex typeAliasNullability test

* Recursively check nullability and track annotations in typealiases
This commit is contained in:
Zac Sweers
2019-11-02 21:55:20 -04:00
committed by GitHub
parent fe34a577e7
commit 7cf365cdbf
2 changed files with 65 additions and 3 deletions

View File

@@ -52,6 +52,7 @@ import java.lang.annotation.ElementType
import java.lang.annotation.Retention import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target import java.lang.annotation.Target
import java.util.TreeSet
import javax.annotation.processing.Messager import javax.annotation.processing.Messager
import javax.lang.model.element.ElementKind import javax.lang.model.element.ElementKind
import javax.lang.model.element.TypeElement import javax.lang.model.element.TypeElement
@@ -322,10 +323,22 @@ private fun String.escapeDollarSigns(): String {
private fun TypeName.unwrapTypeAlias(): TypeName { private fun TypeName.unwrapTypeAlias(): TypeName {
return when (this) { return when (this) {
is ClassName -> { is ClassName -> {
tag<TypeNameAliasTag>()?.type?.unwrapTypeAlias() ?: this 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 -> { is ParameterizedTypeName -> {
return (rawType.unwrapTypeAlias() as ClassName).parameterizedBy(typeArguments.map { it.unwrapTypeAlias() }) return (rawType.unwrapTypeAlias() as ClassName).parameterizedBy(typeArguments.map { it.unwrapTypeAlias() })
.copy(nullable = isNullable, annotations = annotations)
} }
is TypeVariableName -> { is TypeVariableName -> {
return copy(bounds = bounds.map { it.unwrapTypeAlias() }) return copy(bounds = bounds.map { it.unwrapTypeAlias() })

View File

@@ -433,7 +433,7 @@ class DualKotlinTest(useReflection: Boolean) {
val parameterized: GenericClass<TypeAlias>, val parameterized: GenericClass<TypeAlias>,
val wildcardIn: GenericClass<in TypeAlias>, val wildcardIn: GenericClass<in TypeAlias>,
val wildcardOut: GenericClass<out TypeAlias>, val wildcardOut: GenericClass<out TypeAlias>,
val complex: GenericClass<GenericTypeAlias> val complex: GenericClass<GenericTypeAlias>?
) )
// Regression test for https://github.com/square/moshi/issues/991 // Regression test for https://github.com/square/moshi/issues/991
@@ -481,11 +481,52 @@ class DualKotlinTest(useReflection: Boolean) {
val double: Double, val double: Double,
val nullableDouble: Double? = null val nullableDouble: Double? = null
) )
// Regression test for https://github.com/square/moshi/issues/990
@Test fun nullableProperties() {
val adapter = moshi.adapter<NullableList>()
@Language("JSON")
val testJson = """{"nullableList":null}"""
assertThat(adapter.serializeNulls().toJson(NullableList(null)))
.isEqualTo(testJson)
val result = adapter.fromJson(testJson)!!
assertThat(result.nullableList).isNull()
}
@JsonClass(generateAdapter = true)
data class NullableList(val nullableList: List<Any>?)
@Test fun typeAliasNullability() {
val adapter = moshi.adapter<TypeAliasNullability>()
@Language("JSON")
val testJson = """{"aShouldBeNonNull":3,"nullableAShouldBeNullable":null,"redundantNullableAShouldBeNullable":null,"manuallyNullableAShouldBeNullable":null,"convolutedMultiNullableShouldBeNullable":null,"deepNestedNullableShouldBeNullable":null}"""
val instance = TypeAliasNullability(3, null, null, null, null, null)
assertThat(adapter.serializeNulls().toJson(instance))
.isEqualTo(testJson)
val result = adapter.fromJson(testJson)!!
assertThat(result).isEqualTo(instance)
}
@JsonClass(generateAdapter = true)
data class TypeAliasNullability(
val aShouldBeNonNull: A,
val nullableAShouldBeNullable: NullableA,
val redundantNullableAShouldBeNullable: NullableA?,
val manuallyNullableAShouldBeNullable: A?,
val convolutedMultiNullableShouldBeNullable: NullableB?,
val deepNestedNullableShouldBeNullable: E
)
} }
typealias TypeAlias = Int typealias TypeAlias = Int
@Suppress("REDUNDANT_PROJECTION") @Suppress("REDUNDANT_PROJECTION")
typealias GenericTypeAlias = List<out GenericClass<in TypeAlias>> typealias GenericTypeAlias = List<out GenericClass<in TypeAlias>?>?
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class GenericClass<T>(val value: T) data class GenericClass<T>(val value: T)
@@ -493,3 +534,11 @@ data class GenericClass<T>(val value: T)
// Has to be outside since inline classes are only allowed on top level // Has to be outside since inline classes are only allowed on top level
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
inline class InlineClass(val i: Int) inline class InlineClass(val i: Int)
typealias A = Int
typealias NullableA = A?
typealias B = NullableA
typealias NullableB = B?
typealias C = NullableA
typealias D = C
typealias E = D