From 5c45d1e0d9edd97c5f1843579359754e3bd014d0 Mon Sep 17 00:00:00 2001 From: Zac Sweers Date: Tue, 3 Apr 2018 00:27:57 -0700 Subject: [PATCH] Generate `@Generated` annotation onto adapters when possible (#466) * Generate `@Generated` annotation onto adapters when possible Part of #461 This leverages AutoCommon's `GeneratedAnnotations#generatedAnnotation` API to generate a `@Generated` annotation where possible. This keeps with conventions in other code gen tools, and also allows for more fine grained proguard rules for keeping generated adapter names, like so: ```proguard -keepnames @com.squareup.moshi..MoshiSerializable class * # Java < 9 -keepnames @javax.annotation.Generated class **JsonAdapter # Java 9+ -keepnames @javax.annotation.processing.Generated class **JsonAdapter ``` Generated annotation looks like this: ```kotlin @Generated( value = ["com.squareup.moshi.MoshiKotlinCodeGenProcessor"], comments = "https://github.com/square/moshi" ) ``` This doooooes also replace `elements` in `AdapterGenerator` with a `ProcessingEnv`, but I figured that was less evil than polluting it with both `elements` and plumbing down an `env` separately simultaneously. This does also hit a weird ambiguity case due to `KotlinMetadataUtils`' repeat declaration, so a good reason for removing that in the future. Figured it best to punt on a better final place for this to another time. * Remove names and brackets * Add moshi.generated option * Switch back to element property rather than processingEnv * Fold the kotlin-codegen-runtime into Moshi itself. Rename @MoshiSerializable to @JsonClass. Like @Json, I'm anticipating a future where there are other interesting properties on this annotation. Perhaps a future feature where Moshi is strict and only adapts types that have a '@JsonClass' annotation. Also rename MoshiKotlinCodeGenProcessor to JsonClassCodeGenProcessor. We may later support other ways of generating code here; perhaps for regular Java types. * Generate `@Generated` annotation onto adapters when possible Part of #461 This leverages AutoCommon's `GeneratedAnnotations#generatedAnnotation` API to generate a `@Generated` annotation where possible. This keeps with conventions in other code gen tools, and also allows for more fine grained proguard rules for keeping generated adapter names, like so: ```proguard -keepnames @com.squareup.moshi..MoshiSerializable class * # Java < 9 -keepnames @javax.annotation.Generated class **JsonAdapter # Java 9+ -keepnames @javax.annotation.processing.Generated class **JsonAdapter ``` Generated annotation looks like this: ```kotlin @Generated( value = ["com.squareup.moshi.MoshiKotlinCodeGenProcessor"], comments = "https://github.com/square/moshi" ) ``` This doooooes also replace `elements` in `AdapterGenerator` with a `ProcessingEnv`, but I figured that was less evil than polluting it with both `elements` and plumbing down an `env` separately simultaneously. This does also hit a weird ambiguity case due to `KotlinMetadataUtils`' repeat declaration, so a good reason for removing that in the future. Figured it best to punt on a better final place for this to another time. * Fix rebase conflicts and sync with remote --- .../com/squareup/moshi/AdapterGenerator.kt | 18 +++++++-- .../moshi/JsonClassCodeGenProcessor.kt | 38 +++++++++++++++++-- 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/AdapterGenerator.kt b/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/AdapterGenerator.kt index b0e9c66..4f93169 100644 --- a/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/AdapterGenerator.kt +++ b/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/AdapterGenerator.kt @@ -16,6 +16,7 @@ package com.squareup.moshi import com.squareup.kotlinpoet.ARRAY +import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.FileSpec import com.squareup.kotlinpoet.FunSpec @@ -31,6 +32,7 @@ import com.squareup.kotlinpoet.asTypeName import org.jetbrains.kotlin.serialization.ProtoBuf import java.lang.reflect.Type import javax.lang.model.element.Element +import javax.lang.model.element.TypeElement import javax.lang.model.util.Elements /** Generates a JSON adapter for a target type. */ @@ -81,7 +83,7 @@ internal class AdapterGenerator( val delegateAdapters = propertyList.distinctBy { it.delegateKey() } - fun generateFile(): FileSpec { + fun generateFile(generatedOption: TypeElement?): FileSpec { for (property in delegateAdapters) { property.reserveDelegateNames(nameAllocator) } @@ -93,12 +95,20 @@ internal class AdapterGenerator( if (hasCompanionObject) { result.addFunction(generateJsonAdapterFun()) } - result.addType(generateType()) + result.addType(generateType(generatedOption)) return result.build() } - private fun generateType(): TypeSpec { + private fun generateType(generatedOption: TypeElement?): TypeSpec { val result = TypeSpec.classBuilder(adapterName) + + generatedOption?.let { + result.addAnnotation(AnnotationSpec.builder(it.asClassName()) + .addMember("%S", JsonClassCodeGenProcessor::class.java.canonicalName) + .addMember("%S", "https://github.com/square/moshi") + .build()) + } + result.superclass(jsonAdapterTypeName) genericTypeNames?.let { @@ -307,4 +317,4 @@ internal class AdapterGenerator( return result.build() } -} \ No newline at end of file +} diff --git a/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/JsonClassCodeGenProcessor.kt b/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/JsonClassCodeGenProcessor.kt index 91a5c94..82fc8f2 100644 --- a/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/JsonClassCodeGenProcessor.kt +++ b/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/JsonClassCodeGenProcessor.kt @@ -36,6 +36,7 @@ import me.eugeniomarletti.kotlin.processing.KotlinAbstractProcessor import org.jetbrains.kotlin.serialization.ProtoBuf import org.jetbrains.kotlin.serialization.ProtoBuf.ValueParameter import java.io.File +import javax.annotation.processing.ProcessingEnvironment import javax.annotation.processing.Processor import javax.annotation.processing.RoundEnvironment import javax.lang.model.SourceVersion @@ -61,18 +62,49 @@ import javax.tools.Diagnostic.Kind.ERROR @AutoService(Processor::class) class JsonClassCodeGenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils { + companion object { + /** + * This annotation processing argument can be specified to have a `@Generated` annotation + * included in the generated code. It is not encouraged unless you need it for static analysis + * reasons and not enabled by default. + * + * Note that this can only be one of the following values: + * * `"javax.annotation.processing.Generated"` (JRE 9+) + * * `"javax.annotation.Generated"` (JRE <9) + */ + const val OPTION_GENERATED = "moshi.generated" + private val POSSIBLE_GENERATED_NAMES = setOf( + "javax.annotation.processing.Generated", + "javax.annotation.Generated" + ) + } + private val annotation = JsonClass::class.java + private var generatedType: TypeElement? = null override fun getSupportedAnnotationTypes() = setOf(annotation.canonicalName) override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latest() + override fun getSupportedOptions() = setOf(OPTION_GENERATED) + + override fun init(processingEnv: ProcessingEnvironment) { + super.init(processingEnv) + generatedType = processingEnv.options[OPTION_GENERATED]?.let { + if (it !in POSSIBLE_GENERATED_NAMES) { + throw IllegalArgumentException( + "Invalid option value for $OPTION_GENERATED. Found $it, allowable values are $POSSIBLE_GENERATED_NAMES.") + } + processingEnv.elementUtils.getTypeElement(it) + } + } + override fun process(annotations: Set, roundEnv: RoundEnvironment): Boolean { for (type in roundEnv.getElementsAnnotatedWith(annotation)) { val jsonClass = type.getAnnotation(annotation) if (jsonClass.generateAdapter) { val adapterGenerator = processElement(type) ?: continue - adapterGenerator.generateAndWrite() + adapterGenerator.generateAndWrite(generatedType) } } @@ -242,8 +274,8 @@ class JsonClassCodeGenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils element) } - private fun AdapterGenerator.generateAndWrite() { - val fileSpec = generateFile() + private fun AdapterGenerator.generateAndWrite(generatedOption: TypeElement?) { + val fileSpec = generateFile(generatedOption) val adapterName = fileSpec.members.filterIsInstance().first().name!! val outputDir = generatedDir ?: mavenGeneratedDir(adapterName) fileSpec.writeTo(outputDir)