mirror of
https://github.com/fankes/moshi.git
synced 2025-10-20 00:19:21 +08:00
Merge pull request #505 from square/jwilson.0415.honor_kotlin_supertypes
Support generated adapters for Kotlin superclasses
This commit is contained in:
@@ -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 {
|
||||||
|
@@ -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> {
|
||||||
|
@@ -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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
@@ -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();
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user