Change DelegateKey to use AnnotationSpec instead of AnnotationMirror
AnnotationSpec implements equals() in the way we need, but AnnotationMirror doesn't. As a consequence this fixes a problem where we were generating redundant adapters. Closes: https://github.com/square/moshi/issues/563
This commit is contained in:
parent
4f3c418202
commit
7382145318
5 changed files with 70 additions and 41 deletions
|
@ -135,7 +135,7 @@ internal class AdapterGenerator(
|
||||||
result.addProperty(optionsProperty)
|
result.addProperty(optionsProperty)
|
||||||
for (uniqueAdapter in propertyList.distinctBy { it.delegateKey }) {
|
for (uniqueAdapter in propertyList.distinctBy { it.delegateKey }) {
|
||||||
result.addProperty(uniqueAdapter.delegateKey.generateProperty(
|
result.addProperty(uniqueAdapter.delegateKey.generateProperty(
|
||||||
nameAllocator, typeRenderer, moshiParam, messager))
|
nameAllocator, typeRenderer, moshiParam))
|
||||||
}
|
}
|
||||||
|
|
||||||
result.addFunction(generateToStringFun())
|
result.addFunction(generateToStringFun())
|
||||||
|
|
|
@ -15,9 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package com.squareup.moshi.kotlin.codegen
|
package com.squareup.moshi.kotlin.codegen
|
||||||
|
|
||||||
import com.google.auto.common.MoreTypes
|
|
||||||
import com.squareup.kotlinpoet.AnnotationSpec
|
import com.squareup.kotlinpoet.AnnotationSpec
|
||||||
import com.squareup.kotlinpoet.AnnotationSpec.UseSiteTarget.FIELD
|
|
||||||
import com.squareup.kotlinpoet.ClassName
|
import com.squareup.kotlinpoet.ClassName
|
||||||
import com.squareup.kotlinpoet.CodeBlock
|
import com.squareup.kotlinpoet.CodeBlock
|
||||||
import com.squareup.kotlinpoet.KModifier
|
import com.squareup.kotlinpoet.KModifier
|
||||||
|
@ -33,16 +31,11 @@ import com.squareup.kotlinpoet.asClassName
|
||||||
import com.squareup.kotlinpoet.asTypeName
|
import com.squareup.kotlinpoet.asTypeName
|
||||||
import com.squareup.moshi.JsonAdapter
|
import com.squareup.moshi.JsonAdapter
|
||||||
import com.squareup.moshi.Types
|
import com.squareup.moshi.Types
|
||||||
import java.lang.annotation.ElementType
|
|
||||||
import java.lang.annotation.RetentionPolicy
|
|
||||||
import javax.annotation.processing.Messager
|
|
||||||
import javax.lang.model.element.AnnotationMirror
|
|
||||||
import javax.tools.Diagnostic.Kind.ERROR
|
|
||||||
|
|
||||||
/** A JsonAdapter that can be used to encode and decode a particular field. */
|
/** A JsonAdapter that can be used to encode and decode a particular field. */
|
||||||
internal data class DelegateKey(
|
internal data class DelegateKey(
|
||||||
private val type: TypeName,
|
private val type: TypeName,
|
||||||
private val jsonQualifiers: Set<AnnotationMirror>
|
private val jsonQualifiers: List<AnnotationSpec>
|
||||||
) {
|
) {
|
||||||
val nullable get() = type.nullable
|
val nullable get() = type.nullable
|
||||||
|
|
||||||
|
@ -50,36 +43,17 @@ internal data class DelegateKey(
|
||||||
fun generateProperty(
|
fun generateProperty(
|
||||||
nameAllocator: NameAllocator,
|
nameAllocator: NameAllocator,
|
||||||
typeRenderer: TypeRenderer,
|
typeRenderer: TypeRenderer,
|
||||||
moshiParameter: ParameterSpec,
|
moshiParameter: ParameterSpec
|
||||||
messager: Messager): PropertySpec {
|
): PropertySpec {
|
||||||
fun AnnotationMirror.validate(): AnnotationMirror {
|
|
||||||
// Check java types since that covers both java and kotlin annotations
|
|
||||||
val annotationElement = MoreTypes.asTypeElement(annotationType)
|
|
||||||
annotationElement.getAnnotation(java.lang.annotation.Retention::class.java)?.let {
|
|
||||||
if (it.value != RetentionPolicy.RUNTIME) {
|
|
||||||
messager.printMessage(ERROR, "JsonQualifier " +
|
|
||||||
"@${MoreTypes.asTypeElement(annotationType).simpleName} must have RUNTIME retention")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
annotationElement.getAnnotation(java.lang.annotation.Target::class.java)?.let {
|
|
||||||
if (ElementType.FIELD !in it.value) {
|
|
||||||
messager.printMessage(ERROR, "JsonQualifier " +
|
|
||||||
"@${MoreTypes.asTypeElement(annotationType).simpleName} must support FIELD target")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
jsonQualifiers.forEach { it.validate() }
|
|
||||||
val qualifierNames = jsonQualifiers.joinToString("") {
|
val qualifierNames = jsonQualifiers.joinToString("") {
|
||||||
"At${it.annotationType.asElement().simpleName}"
|
"At${(it.type as ClassName).simpleName}"
|
||||||
}
|
}
|
||||||
val adapterName = nameAllocator.newName(
|
val adapterName = nameAllocator.newName(
|
||||||
"${type.toVariableName().decapitalize()}${qualifierNames}Adapter", this)
|
"${type.toVariableName().decapitalize()}${qualifierNames}Adapter", this)
|
||||||
|
|
||||||
val adapterTypeName = JsonAdapter::class.asClassName().parameterizedBy(type)
|
val adapterTypeName = JsonAdapter::class.asClassName().parameterizedBy(type)
|
||||||
val qualifiers = jsonQualifiers
|
|
||||||
val standardArgs = arrayOf(moshiParameter,
|
val standardArgs = arrayOf(moshiParameter,
|
||||||
if (type is ClassName && qualifiers.isEmpty()) {
|
if (type is ClassName && jsonQualifiers.isEmpty()) {
|
||||||
""
|
""
|
||||||
} else {
|
} else {
|
||||||
CodeBlock.of("<%T>", type)
|
CodeBlock.of("<%T>", type)
|
||||||
|
@ -87,7 +61,7 @@ internal data class DelegateKey(
|
||||||
typeRenderer.render(type))
|
typeRenderer.render(type))
|
||||||
val standardArgsSize = standardArgs.size + 1
|
val standardArgsSize = standardArgs.size + 1
|
||||||
val (initializerString, args) = when {
|
val (initializerString, args) = when {
|
||||||
qualifiers.isEmpty() -> "" to emptyArray()
|
jsonQualifiers.isEmpty() -> "" to emptyArray()
|
||||||
else -> {
|
else -> {
|
||||||
", %${standardArgsSize}T.getFieldJsonQualifierAnnotations(javaClass, " +
|
", %${standardArgsSize}T.getFieldJsonQualifierAnnotations(javaClass, " +
|
||||||
"%${standardArgsSize + 1}S)" to arrayOf(Types::class.asTypeName(), adapterName)
|
"%${standardArgsSize + 1}S)" to arrayOf(Types::class.asTypeName(), adapterName)
|
||||||
|
@ -98,9 +72,7 @@ internal data class DelegateKey(
|
||||||
val nullModifier = if (nullable) ".nullSafe()" else ".nonNull()"
|
val nullModifier = if (nullable) ".nullSafe()" else ".nonNull()"
|
||||||
|
|
||||||
return PropertySpec.builder(adapterName, adapterTypeName, KModifier.PRIVATE)
|
return PropertySpec.builder(adapterName, adapterTypeName, KModifier.PRIVATE)
|
||||||
.addAnnotations(qualifiers.map {
|
.addAnnotations(jsonQualifiers)
|
||||||
AnnotationSpec.get(it).toBuilder().useSiteTarget(FIELD).build()
|
|
||||||
})
|
|
||||||
.initializer("%1N.adapter%2L(%3L$initializerString)$nullModifier", *finalArgs)
|
.initializer("%1N.adapter%2L(%3L$initializerString)$nullModifier", *finalArgs)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,8 +20,10 @@ import com.squareup.kotlinpoet.NameAllocator
|
||||||
import com.squareup.kotlinpoet.PropertySpec
|
import com.squareup.kotlinpoet.PropertySpec
|
||||||
|
|
||||||
/** Generates functions to encode and decode a property as JSON. */
|
/** Generates functions to encode and decode a property as JSON. */
|
||||||
internal class PropertyGenerator(val target: TargetProperty) {
|
internal class PropertyGenerator(
|
||||||
val delegateKey = target.delegateKey()
|
val target: TargetProperty,
|
||||||
|
val delegateKey: DelegateKey
|
||||||
|
) {
|
||||||
val name = target.name
|
val name = target.name
|
||||||
val jsonName = target.jsonName()
|
val jsonName = target.jsonName()
|
||||||
val hasDefault = target.hasDefault
|
val hasDefault = target.hasDefault
|
||||||
|
|
|
@ -16,6 +16,8 @@
|
||||||
package com.squareup.moshi.kotlin.codegen
|
package com.squareup.moshi.kotlin.codegen
|
||||||
|
|
||||||
import com.google.auto.common.AnnotationMirrors
|
import com.google.auto.common.AnnotationMirrors
|
||||||
|
import com.google.auto.common.MoreTypes
|
||||||
|
import com.squareup.kotlinpoet.AnnotationSpec
|
||||||
import com.squareup.kotlinpoet.TypeName
|
import com.squareup.kotlinpoet.TypeName
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonQualifier
|
import com.squareup.moshi.JsonQualifier
|
||||||
|
@ -26,11 +28,16 @@ import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility.IN
|
||||||
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility.PROTECTED
|
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility.PROTECTED
|
||||||
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility.PUBLIC
|
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility.PUBLIC
|
||||||
import me.eugeniomarletti.kotlin.metadata.visibility
|
import me.eugeniomarletti.kotlin.metadata.visibility
|
||||||
|
import java.lang.annotation.ElementType
|
||||||
|
import java.lang.annotation.Retention
|
||||||
|
import java.lang.annotation.RetentionPolicy
|
||||||
|
import java.lang.annotation.Target
|
||||||
import javax.annotation.processing.Messager
|
import javax.annotation.processing.Messager
|
||||||
import javax.lang.model.element.AnnotationMirror
|
import javax.lang.model.element.AnnotationMirror
|
||||||
import javax.lang.model.element.Element
|
import javax.lang.model.element.Element
|
||||||
import javax.lang.model.element.ExecutableElement
|
import javax.lang.model.element.ExecutableElement
|
||||||
import javax.lang.model.element.Modifier
|
import javax.lang.model.element.Modifier
|
||||||
|
import javax.lang.model.element.Name
|
||||||
import javax.lang.model.element.VariableElement
|
import javax.lang.model.element.VariableElement
|
||||||
import javax.tools.Diagnostic
|
import javax.tools.Diagnostic
|
||||||
|
|
||||||
|
@ -85,10 +92,32 @@ internal data class TargetProperty(
|
||||||
return null // This property is not settable. Ignore it.
|
return null // This property is not settable. Ignore it.
|
||||||
}
|
}
|
||||||
|
|
||||||
return PropertyGenerator(this)
|
val jsonQualifierMirrors = jsonQualifiers()
|
||||||
}
|
for (jsonQualifier in jsonQualifierMirrors) {
|
||||||
|
// Check Java types since that covers both Java and Kotlin annotations.
|
||||||
|
val annotationElement = MoreTypes.asTypeElement(jsonQualifier.annotationType)
|
||||||
|
annotationElement.getAnnotation(Retention::class.java)?.let {
|
||||||
|
if (it.value != RetentionPolicy.RUNTIME) {
|
||||||
|
messager.printMessage(Diagnostic.Kind.ERROR,
|
||||||
|
"JsonQualifier @${jsonQualifier.simpleName} must have RUNTIME retention")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
annotationElement.getAnnotation(Target::class.java)?.let {
|
||||||
|
if (ElementType.FIELD !in it.value) {
|
||||||
|
messager.printMessage(Diagnostic.Kind.ERROR,
|
||||||
|
"JsonQualifier @${jsonQualifier.simpleName} must support FIELD target")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun delegateKey() = DelegateKey(type, jsonQualifiers())
|
val jsonQualifierSpecs = jsonQualifierMirrors.map {
|
||||||
|
AnnotationSpec.get(it).toBuilder()
|
||||||
|
.useSiteTarget(AnnotationSpec.UseSiteTarget.FIELD)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
return PropertyGenerator(this, DelegateKey(type, jsonQualifierSpecs))
|
||||||
|
}
|
||||||
|
|
||||||
/** Returns the JsonQualifiers on the field and parameter of this property. */
|
/** Returns the JsonQualifiers on the field and parameter of this property. */
|
||||||
private fun jsonQualifiers(): Set<AnnotationMirror> {
|
private fun jsonQualifiers(): Set<AnnotationMirror> {
|
||||||
|
@ -131,5 +160,8 @@ internal data class TargetProperty(
|
||||||
return getAnnotation(Json::class.java)?.name?.replace("$", "\\$")
|
return getAnnotation(Json::class.java)?.name?.replace("$", "\\$")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val AnnotationMirror.simpleName: Name
|
||||||
|
get() = MoreTypes.asTypeElement(annotationType).simpleName!!
|
||||||
|
|
||||||
override fun toString() = name
|
override fun toString() = name
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ import org.junit.Assert.fail
|
||||||
import org.junit.Ignore
|
import org.junit.Ignore
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import kotlin.reflect.full.memberProperties
|
||||||
|
|
||||||
class GeneratedAdaptersTest {
|
class GeneratedAdaptersTest {
|
||||||
|
|
||||||
|
@ -857,6 +858,28 @@ class GeneratedAdaptersTest {
|
||||||
throw AssertionError()
|
throw AssertionError()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** https://github.com/square/moshi/issues/563 */
|
||||||
|
@Test fun qualifiedAdaptersAreShared() {
|
||||||
|
val moshi = Moshi.Builder()
|
||||||
|
.add(UppercaseJsonAdapter())
|
||||||
|
.build()
|
||||||
|
val jsonAdapter = moshi.adapter(MultiplePropertiesShareAdapter::class.java)
|
||||||
|
|
||||||
|
val encoded = MultiplePropertiesShareAdapter("Android", "Banana")
|
||||||
|
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":"ANDROID","b":"BANANA"}""")
|
||||||
|
|
||||||
|
val delegateAdapters = jsonAdapter::class.memberProperties.filter {
|
||||||
|
it.returnType.classifier == JsonAdapter::class
|
||||||
|
}
|
||||||
|
assertThat(delegateAdapters).hasSize(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
class MultiplePropertiesShareAdapter(
|
||||||
|
@Uppercase(true) var a: String,
|
||||||
|
@Uppercase(true) var b: String
|
||||||
|
)
|
||||||
|
|
||||||
@Test fun toJsonOnly() {
|
@Test fun toJsonOnly() {
|
||||||
val moshi = Moshi.Builder()
|
val moshi = Moshi.Builder()
|
||||||
.add(CustomToJsonOnlyAdapter())
|
.add(CustomToJsonOnlyAdapter())
|
||||||
|
|
Loading…
Reference in a new issue