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

View file

@ -19,6 +19,7 @@ import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.asClassName
import com.squareup.kotlinpoet.asTypeName
import me.eugeniomarletti.kotlin.metadata.KotlinClassMetadata
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 javax.annotation.processing.Messager
import javax.lang.model.element.Element
import javax.lang.model.element.ElementKind
import javax.lang.model.element.ExecutableElement
import javax.lang.model.element.TypeElement
import javax.lang.model.element.VariableElement
import javax.lang.model.type.DeclaredType
import javax.lang.model.util.Elements
import javax.lang.model.util.Types
import javax.tools.Diagnostic.Kind.ERROR
/** A user type that should be decoded and encoded by generated code. */
@ -54,8 +58,10 @@ internal data class TargetType(
val hasCompanionObject = proto.hasCompanionObjectName()
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. */
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
if (element !is TypeElement || typeMetadata !is KotlinClassMetadata) {
messager.printMessage(
@ -87,17 +93,34 @@ internal data class TargetType(
}
}
val constructor = TargetConstructor.primary(typeMetadata, elementUtils)
val properties = properties(element, constructor)
val constructor = TargetConstructor.primary(typeMetadata, elements)
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)
return TargetType(proto, element, constructor, properties, genericTypeNames)
}
private fun properties(
model: TypeElement,
/** Returns the properties declared by `typeElement`. */
private fun declaredProperties(
typeElement: TypeElement,
constructor: TargetConstructor
): Map<String, TargetProperty> {
val typeMetadata: KotlinClassMetadata = model.kotlinMetadata as KotlinClassMetadata
val typeMetadata: KotlinClassMetadata = typeElement.kotlinMetadata as KotlinClassMetadata
val nameResolver = typeMetadata.data.nameResolver
val classProto = typeMetadata.data.classProto
@ -105,7 +128,7 @@ internal data class TargetType(
val fields = mutableMapOf<String, VariableElement>()
val setters = mutableMapOf<String, ExecutableElement>()
val getters = mutableMapOf<String, ExecutableElement>()
for (element in model.enclosedElements) {
for (element in typeElement.enclosedElements) {
if (element is VariableElement) {
fields[element.name] = element
} 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 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 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.Test
import java.util.Locale
import java.util.SimpleTimeZone
class GeneratedAdaptersTest {
@ -628,21 +627,6 @@ class GeneratedAdaptersTest {
var v26: Int, var v27: Int, var v28: Int, var v29: Int, var v30: 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() {
val moshi = Moshi.Builder().build()
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)
@JsonQualifier
annotation class Uppercase

View file

@ -22,11 +22,20 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* Customizes how a type is encoded as JSON.
*
* <p>This annotation is currently only permitted on declarations of classes in Kotlin.
*/
@Retention(RUNTIME)
@Documented
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();
}