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:
Jesse Wilson 2018-09-09 13:08:19 -04:00
parent 4f3c418202
commit 7382145318
5 changed files with 70 additions and 41 deletions

View file

@ -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())

View file

@ -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()
} }

View file

@ -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

View file

@ -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
} }

View file

@ -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())