Support generated adapters for Kotlin superclasses

This commit is contained in:
Jesse Wilson
2018-04-15 14:37:49 -04:00
parent 8d24d89abf
commit 9401a810f0
5 changed files with 113 additions and 28 deletions

View File

@@ -93,7 +93,7 @@ class JsonClassCodeGenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils
} }
private fun adapterGenerator(element: Element): AdapterGenerator? { private fun adapterGenerator(element: Element): AdapterGenerator? {
val type = TargetType.get(messager, elementUtils, element) ?: return null val type = TargetType.get(messager, elementUtils, typeUtils, element) ?: return null
val properties = mutableMapOf<String, PropertyGenerator>() val properties = mutableMapOf<String, PropertyGenerator>()
for (property in type.properties.values) { for (property in type.properties.values) {
@@ -112,8 +112,7 @@ class JsonClassCodeGenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils
} }
// Sort properties so that those with constructor parameters come first. // Sort properties so that those with constructor parameters come first.
val sortedProperties = properties.values.toMutableList() val sortedProperties = properties.values.sortedBy {
sortedProperties.sortBy {
if (it.hasConstructorParameter) { if (it.hasConstructorParameter) {
it.target.parameterIndex it.target.parameterIndex
} else { } else {

View File

@@ -19,6 +19,7 @@ import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.KModifier import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.ParameterizedTypeName import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.TypeVariableName import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.asClassName
import com.squareup.kotlinpoet.asTypeName import com.squareup.kotlinpoet.asTypeName
import me.eugeniomarletti.kotlin.metadata.KotlinClassMetadata import me.eugeniomarletti.kotlin.metadata.KotlinClassMetadata
import me.eugeniomarletti.kotlin.metadata.KotlinMetadata import me.eugeniomarletti.kotlin.metadata.KotlinMetadata
@@ -36,10 +37,13 @@ import org.jetbrains.kotlin.serialization.deserialization.NameResolver
import org.jetbrains.kotlin.util.capitalizeDecapitalize.decapitalizeAsciiOnly import org.jetbrains.kotlin.util.capitalizeDecapitalize.decapitalizeAsciiOnly
import javax.annotation.processing.Messager import javax.annotation.processing.Messager
import javax.lang.model.element.Element import javax.lang.model.element.Element
import javax.lang.model.element.ElementKind
import javax.lang.model.element.ExecutableElement import javax.lang.model.element.ExecutableElement
import javax.lang.model.element.TypeElement import javax.lang.model.element.TypeElement
import javax.lang.model.element.VariableElement import javax.lang.model.element.VariableElement
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.tools.Diagnostic.Kind.ERROR import javax.tools.Diagnostic.Kind.ERROR
/** A user type that should be decoded and encoded by generated code. */ /** A user type that should be decoded and encoded by generated code. */
@@ -54,8 +58,10 @@ internal data class TargetType(
val hasCompanionObject = proto.hasCompanionObjectName() val hasCompanionObject = proto.hasCompanionObjectName()
companion object { companion object {
private val OBJECT_CLASS = ClassName("java.lang", "Object")
/** Returns a target type for `element`, or null if it cannot be used with code gen. */ /** Returns a target type for `element`, or null if it cannot be used with code gen. */
fun get(messager: Messager, elementUtils: Elements, element: Element): TargetType? { fun get(messager: Messager, elements: Elements, types: Types, element: Element): TargetType? {
val typeMetadata: KotlinMetadata? = element.kotlinMetadata val typeMetadata: KotlinMetadata? = element.kotlinMetadata
if (element !is TypeElement || typeMetadata !is KotlinClassMetadata) { if (element !is TypeElement || typeMetadata !is KotlinClassMetadata) {
messager.printMessage( messager.printMessage(
@@ -87,17 +93,34 @@ internal data class TargetType(
} }
} }
val constructor = TargetConstructor.primary(typeMetadata, elementUtils) val constructor = TargetConstructor.primary(typeMetadata, elements)
val properties = properties(element, constructor) val properties = mutableMapOf<String, TargetProperty>()
for (supertype in element.supertypes(types)) {
if (supertype.asClassName() == OBJECT_CLASS) {
continue // Don't load properties for java.lang.Object.
}
if (supertype.kind != ElementKind.CLASS) {
continue // Don't load properties for interface types.
}
if (supertype.kotlinMetadata == null) {
messager.printMessage(ERROR,
"@JsonClass can't be applied to $element: supertype $supertype is not a Kotlin type",
element)
}
for ((name, property) in declaredProperties(supertype, constructor)) {
properties.putIfAbsent(name, property)
}
}
val genericTypeNames = genericTypeNames(proto, typeMetadata.data.nameResolver) val genericTypeNames = genericTypeNames(proto, typeMetadata.data.nameResolver)
return TargetType(proto, element, constructor, properties, genericTypeNames) return TargetType(proto, element, constructor, properties, genericTypeNames)
} }
private fun properties( /** Returns the properties declared by `typeElement`. */
model: TypeElement, private fun declaredProperties(
typeElement: TypeElement,
constructor: TargetConstructor constructor: TargetConstructor
): Map<String, TargetProperty> { ): Map<String, TargetProperty> {
val typeMetadata: KotlinClassMetadata = model.kotlinMetadata as KotlinClassMetadata val typeMetadata: KotlinClassMetadata = typeElement.kotlinMetadata as KotlinClassMetadata
val nameResolver = typeMetadata.data.nameResolver val nameResolver = typeMetadata.data.nameResolver
val classProto = typeMetadata.data.classProto val classProto = typeMetadata.data.classProto
@@ -105,7 +128,7 @@ internal data class TargetType(
val fields = mutableMapOf<String, VariableElement>() val fields = mutableMapOf<String, VariableElement>()
val setters = mutableMapOf<String, ExecutableElement>() val setters = mutableMapOf<String, ExecutableElement>()
val getters = mutableMapOf<String, ExecutableElement>() val getters = mutableMapOf<String, ExecutableElement>()
for (element in model.enclosedElements) { for (element in typeElement.enclosedElements) {
if (element is VariableElement) { if (element is VariableElement) {
fields[element.name] = element fields[element.name] = element
} else if (element is ExecutableElement) { } else if (element is ExecutableElement) {
@@ -157,6 +180,19 @@ internal data class TargetType(
} }
} }
/** Returns all supertypes of this, recursively. Includes interface and class supertypes. */
private fun TypeElement.supertypes(
types: Types,
result: MutableSet<TypeElement> = mutableSetOf()
): Set<TypeElement> {
result.add(this)
for (supertype in types.directSupertypes(asType())) {
val supertypeElement = (supertype as DeclaredType).asElement() as TypeElement
supertypeElement.supertypes(types, result)
}
return result
}
private val Element.name get() = simpleName.toString() private val Element.name get() = simpleName.toString()
private fun genericTypeNames(proto: Class, nameResolver: NameResolver): List<TypeVariableName> { private fun genericTypeNames(proto: Class, nameResolver: NameResolver): List<TypeVariableName> {

View File

@@ -227,4 +227,21 @@ class CompilerTest {
assertThat(result.systemErr).contains("property b is not visible") assertThat(result.systemErr).contains("property b is not visible")
assertThat(result.systemErr).contains("property c is not visible") assertThat(result.systemErr).contains("property c is not visible")
} }
@Test fun extendPlatformType() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodeGenProcessor::class)
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|import java.util.Date
|
|@JsonClass(generateAdapter = true)
|class ExtendsPlatformClass(var a: Int) : Date()
|""".trimMargin())
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains("supertype java.util.Date is not a Kotlin type")
}
} }

View File

@@ -20,7 +20,6 @@ import org.intellij.lang.annotations.Language
import org.junit.Assert.fail import org.junit.Assert.fail
import org.junit.Test import org.junit.Test
import java.util.Locale import java.util.Locale
import java.util.SimpleTimeZone
class GeneratedAdaptersTest { class GeneratedAdaptersTest {
@@ -628,21 +627,6 @@ class GeneratedAdaptersTest {
var v26: Int, var v27: Int, var v28: Int, var v29: Int, var v30: Int, var v26: Int, var v27: Int, var v28: Int, var v29: Int, var v30: Int,
var v31: Int, var v32: Int, var v33: Int) var v31: Int, var v32: Int, var v33: Int)
@Test fun extendsPlatformClassWithPrivateField() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(ExtendsPlatformClassWithPrivateField::class.java)
val encoded = ExtendsPlatformClassWithPrivateField(3)
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3}""")
val decoded = jsonAdapter.fromJson("""{"a":4,"id":"B"}""")!!
assertThat(decoded.a).isEqualTo(4)
assertThat(decoded.id).isEqualTo("C")
}
@JsonClass(generateAdapter = true)
internal class ExtendsPlatformClassWithPrivateField(var a: Int) : SimpleTimeZone(0, "C")
@Test fun unsettablePropertyIgnored() { @Test fun unsettablePropertyIgnored() {
val moshi = Moshi.Builder().build() val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(UnsettableProperty::class.java) val jsonAdapter = moshi.adapter(UnsettableProperty::class.java)
@@ -710,6 +694,46 @@ class GeneratedAdaptersTest {
} }
} }
@Test fun supertypeConstructorParameters() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(SubtypeConstructorParameters::class.java)
val encoded = SubtypeConstructorParameters(3, 5)
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""")
val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""")!!
assertThat(decoded.a).isEqualTo(4)
assertThat(decoded.b).isEqualTo(6)
}
open class SupertypeConstructorParameters(var a: Int)
@JsonClass(generateAdapter = true)
class SubtypeConstructorParameters(a: Int, var b: Int) : SupertypeConstructorParameters(a)
@Test fun supertypeProperties() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(SubtypeProperties::class.java)
val encoded = SubtypeProperties()
encoded.a = 3
encoded.b = 5
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"b":5,"a":3}""")
val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""")!!
assertThat(decoded.a).isEqualTo(4)
assertThat(decoded.b).isEqualTo(6)
}
open class SupertypeProperties {
var a: Int = -1
}
@JsonClass(generateAdapter = true)
class SubtypeProperties : SupertypeProperties() {
var b: Int = -1
}
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
@JsonQualifier @JsonQualifier
annotation class Uppercase annotation class Uppercase

View File

@@ -22,11 +22,20 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
/** /**
* Customizes how a type is encoded as JSON. * Customizes how a type is encoded as JSON.
*
* <p>This annotation is currently only permitted on declarations of classes in Kotlin.
*/ */
@Retention(RUNTIME) @Retention(RUNTIME)
@Documented @Documented
public @interface JsonClass { public @interface JsonClass {
/**
* True to trigger the annotation processor to generate an adapter for this type.
*
* There are currently some restrictions on which types that can be used with generated adapters:
*
* * The class must be implemented in Kotlin.
* * The class may not be an abstract class, an inner class, or a local class.
* * All superclasses must be implemented in Kotlin.
* * All properties must be public, protected, or internal.
* * All properties must be either non-transient or have a default value.
*/
boolean generateAdapter(); boolean generateAdapter();
} }