Full JsonQualifier support in kotlin codegen.

This commit is contained in:
Zac Sweers
2018-04-29 15:54:45 -07:00
committed by Jesse Wilson
parent 10a5dc827b
commit 4b610329bd
7 changed files with 177 additions and 26 deletions

View File

@@ -34,6 +34,7 @@ import me.eugeniomarletti.kotlin.metadata.isDataClass
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility
import me.eugeniomarletti.kotlin.metadata.visibility
import java.lang.reflect.Type
import javax.annotation.processing.Messager
import javax.lang.model.element.TypeElement
/** Generates a JSON adapter for a target type. */
@@ -81,7 +82,7 @@ internal class AdapterGenerator(
.joinToString(", ") { "\"$it\"" }})", JsonReader.Options::class.asTypeName())
.build()
fun generateFile(generatedOption: TypeElement?): FileSpec {
fun generateFile(messager: Messager, generatedOption: TypeElement?): FileSpec {
for (property in propertyList) {
property.allocateNames(nameAllocator)
}
@@ -91,11 +92,11 @@ internal class AdapterGenerator(
if (hasCompanionObject) {
result.addFunction(generateJsonAdapterFun())
}
result.addType(generateType(generatedOption))
result.addType(generateType(messager, generatedOption))
return result.build()
}
private fun generateType(generatedOption: TypeElement?): TypeSpec {
private fun generateType(messager: Messager, generatedOption: TypeElement?): TypeSpec {
val result = TypeSpec.classBuilder(adapterName)
generatedOption?.let {
@@ -129,7 +130,7 @@ internal class AdapterGenerator(
result.addProperty(optionsProperty)
for (uniqueAdapter in propertyList.distinctBy { it.delegateKey }) {
result.addProperty(uniqueAdapter.delegateKey.generateProperty(
nameAllocator, typeRenderer, moshiParam))
nameAllocator, typeRenderer, moshiParam, messager))
}
result.addFunction(generateToStringFun())

View File

@@ -15,6 +15,9 @@
*/
package com.squareup.moshi
import com.google.auto.common.MoreTypes
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.AnnotationSpec.UseSiteTarget.FIELD
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.KModifier
@@ -26,7 +29,11 @@ import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.WildcardTypeName
import com.squareup.kotlinpoet.asTypeName
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. */
internal data class DelegateKey(
@@ -39,7 +46,24 @@ internal data class DelegateKey(
fun generateProperty(
nameAllocator: NameAllocator,
typeRenderer: TypeRenderer,
moshiParameter: ParameterSpec): PropertySpec {
moshiParameter: ParameterSpec,
messager: Messager): 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("") {
"At${it.annotationType.asElement().simpleName}"
}
@@ -59,21 +83,10 @@ internal data class DelegateKey(
val standardArgsSize = standardArgs.size + 1
val (initializerString, args) = when {
qualifiers.isEmpty() -> "" to emptyArray()
qualifiers.size == 1 -> {
", %${standardArgsSize}T::class.java" to arrayOf(
qualifiers.first().annotationType.asTypeName())
}
else -> {
val initString = qualifiers
.mapIndexed { index, _ ->
val annoClassIndex = standardArgsSize + index
return@mapIndexed "%${annoClassIndex}T::class.java"
}
.joinToString()
val initArgs = qualifiers
.map { it.annotationType.asTypeName() }
.toTypedArray()
", $initString" to initArgs
", %${standardArgsSize}T.getFieldJsonQualifierAnnotations(javaClass, %${standardArgsSize + 1}S)" to arrayOf(
Types::class.asTypeName(),
adapterName)
}
}
val finalArgs = arrayOf(*standardArgs, *args)
@@ -81,6 +94,7 @@ internal data class DelegateKey(
val nullModifier = if (nullable) ".nullSafe()" else ".nonNull()"
return PropertySpec.builder(adapterName, adapterTypeName, KModifier.PRIVATE)
.addAnnotations(qualifiers.map { AnnotationSpec.get(it).toBuilder().useSiteTarget(FIELD).build() })
.initializer("%1N.adapter%2L(%3L$initializerString)$nullModifier", *finalArgs)
.build()
}

View File

@@ -124,7 +124,7 @@ class JsonClassCodeGenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils
}
private fun AdapterGenerator.generateAndWrite(generatedOption: TypeElement?) {
val fileSpec = generateFile(generatedOption)
val fileSpec = generateFile(messager, generatedOption)
val adapterName = fileSpec.members.filterIsInstance<TypeSpec>().first().name!!
val outputDir = generatedDir ?: mavenGeneratedDir(adapterName)
fileSpec.writeTo(outputDir)

View File

@@ -244,4 +244,60 @@ class CompilerTest {
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains("supertype java.util.Date is not a Kotlin type")
}
@Test
fun nonFieldApplicableQualifier() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodeGenProcessor::class)
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|import com.squareup.moshi.JsonQualifier
|import kotlin.annotation.AnnotationRetention.RUNTIME
|import kotlin.annotation.AnnotationTarget.PROPERTY
|import kotlin.annotation.Retention
|import kotlin.annotation.Target
|
|@Retention(RUNTIME)
|@Target(PROPERTY)
|@JsonQualifier
|annotation class UpperCase
|
|@JsonClass(generateAdapter = true)
|class ClassWithQualifier(@UpperCase val a: Int)
|""".trimMargin())
val result = call.execute()
println(result.systemErr)
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains("JsonQualifier @UpperCase must support FIELD target")
}
@Test
fun nonRuntimeQualifier() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodeGenProcessor::class)
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|import com.squareup.moshi.JsonQualifier
|import kotlin.annotation.AnnotationRetention.BINARY
|import kotlin.annotation.AnnotationTarget.FIELD
|import kotlin.annotation.AnnotationTarget.PROPERTY
|import kotlin.annotation.Retention
|import kotlin.annotation.Target
|
|@Retention(BINARY)
|@Target(PROPERTY, FIELD)
|@JsonQualifier
|annotation class UpperCase
|
|@JsonClass(generateAdapter = true)
|class ClassWithQualifier(@UpperCase val a: Int)
|""".trimMargin())
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains("JsonQualifier @UpperCase must have RUNTIME retention")
}
}