Support configurable @Generated annotation types (#259)

This commit is contained in:
Ben Bader 2018-11-02 14:13:02 -07:00 коммит произвёл GitHub
Родитель 78f0a79430
Коммит 00bbd2e0a9
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
9 изменённых файлов: 110 добавлений и 74 удалений

Просмотреть файл

@ -1,6 +1,7 @@
language: java language: java
jdk: jdk:
- oraclejdk8 - oraclejdk8
- openjdk10
sudo: false sudo: false
install: echo './gradlew check will install dependencies' install: echo './gradlew check will install dependencies'

Просмотреть файл

@ -481,7 +481,13 @@ The final new flag is `--kt-file-per-type`. Thrifty's convention is to generate
`--omit-file-comments` suppresses the comment that prefixes each autogenerated Java file. `--omit-file-comments` suppresses the comment that prefixes each autogenerated Java file.
Types generated by Thrifty are annotated with `@javax.annotation.Generated` by default; this can be disabled with the `--omit-generated-annotations` flag. Types generated by Thrifty are annotated with a `@Generated` annotation by default; this can be disabled with the `--omit-generated-annotations` flag. The actual type of the annotation may be specified with a `--use-generated-annotation=[jdk8|jdk9|native]` argument. By default, we assume `jdk8`, i.e. `javax.annotation.Generated`, as this is what's supported in the Android toolchain ()and will be for the foreseeable future).
##### About @Generated annotations
In short, from Java 5-8, everyone has used `javax.annotation.Generated`; this class was replaced in Java 9+ with `javax.annotation.processing.Generated`. This means that code generated for JDK8 may not compile on JDK9, and vice-versa. The solution is to either suppress `@Generated` annotations entirely, or to be explicit about which annotation we want to use.
For Android, the compiler's defaults are good enough. If you are going to use generated code with JRE 9+, make sure to use `--use-generated-type=jdk9`. You almost _never_ want `--use-generated-type=native`; it exists to support running our integration tests in a convenient way in multiple environments, and will probably only cause you pain if you try to use it yourself.
### Thanks ### Thanks

Просмотреть файл

@ -28,3 +28,4 @@ Option | Description
`--set-type=[classname]` | A java.util.Set implementation to be used wherever sets are instantiated in generated code. `--set-type=[classname]` | A java.util.Set implementation to be used wherever sets are instantiated in generated code.
`--map-type=[classname]` | A java.util.Map implementation, as above. `--map-type=[classname]` | A java.util.Map implementation, as above.
`--kt-file-per-type` | Specifies that one .kt file should be generated per Kotlin type. The default is for all code to be written to a single file. `--kt-file-per-type` | Specifies that one .kt file should be generated per Kotlin type. The default is for all code to be written to a single file.
`--generated-annotation-type=[jdk8,jdk9,native]` | Optional, defaults to `jdk8`. Specifies which `@Generated` annotation type to use - `javax.annotation.Generated`, `javax.annotation.processing.Generated`, or whichever type is present in the Java runtime at compile-time.

Просмотреть файл

@ -59,6 +59,7 @@ import java.util.ArrayList
* [--use-android-annotations] * [--use-android-annotations]
* [--omit-file-comments] * [--omit-file-comments]
* [--omit-generated-annotations] * [--omit-generated-annotations]
* [--generated-annotation-type=[jdk8|jdk9|native]
* file1.thrift * file1.thrift
* file2.thrift * file2.thrift
* ... * ...
@ -81,6 +82,38 @@ import java.util.ArrayList
* class name when instantiating map-typed values. Defaults to [java.util.HashMap]. * class name when instantiating map-typed values. Defaults to [java.util.HashMap].
* Android users will likely wish to substitute `android.support.v4.util.ArrayMap`. * Android users will likely wish to substitute `android.support.v4.util.ArrayMap`.
* *
* `--lang=[java|kotlin]` is optional, defaulting to Java. When provided, the
* compiler will generate code in the specified language.
*
* `--kt-file-per-type` is optional. When specified, one Kotlin file will be generated
* for each top-level generated Thrift type. When absent (the default), all generated
* types in a single package will go in one file named `ThriftTypes.kt`. Implies
* `--lang=kotlin`.
*
* `--parcelable` is optional. When provided, generated types will contain a
* `Parcelable` implementation. Kotlin types will use the `@Parcelize` extension.
*
* `--use-android-annotations` is optional. When specified, generated Java classes
* will have `@android.support.annotation.Nullable` or `@android.support.annotation.NotNull`
* annotations, as appropriate. Has no effect on Kotlin code.
*
* `--omit-file-comments` is optional. When specified, no file-header comment is generated.
* The default behavior is to prefix generated files with a comment indicating that they
* are generated by Thrifty, and should probably not be modified by hand.
*
* `--omit-generated-annotations` is optional. When specified, generated types will not
* have `@Generated` annotations applied. The default behavior is to annotate all generated
* types with `javax.annotation.Generated`.
*
* `--generated-annotatation-type=[jdk8|jdk9|native]` is optional, defaulting to `jdk8`.
* This option controls the type of `@Generated` annotation added to generated types. The default
* of `jdk8` results in `javax.annotation.Generated`. `jdk9` results in
* `javax.annotation.processing.Generated`. `native` will use whichever version is present
* in the Java runtime used to run the compiler. This option is a sad consequence of the decision
* to repackage the `@Generated` annotation starting in Java 9. Code generated with one annotation
* will not compile on Java versions that do not have this annotation. Because Thrifty is intended
* for use on Android, we default to jdk8 (Android doesn't support JDK9 and probably never* will).
*
* If no .thrift files are given, then all .thrift files located on the search path * If no .thrift files are given, then all .thrift files located on the search path
* will be implicitly included; otherwise only the given files (and those included by them) * will be implicitly included; otherwise only the given files (and those included by them)
* will be compiled. * will be compiled.
@ -92,10 +125,36 @@ class ThriftyCompiler {
KOTLIN KOTLIN
} }
private object GeneratedAnnotationTypes {
const val jdk8 = "javax.annotation.Generated"
const val jdk9 = "javax.annotation.processing.Generated"
}
private val cli = object : CliktCommand( private val cli = object : CliktCommand(
name = "thrifty-compiler", name = "thrifty-compiler",
help = "Generate Java or Kotlin code from .thrift files" help = "Generate Java or Kotlin code from .thrift files"
) { ) {
private val javaVersion: Double by lazy {
val javaVersionText = System.getProperty("java.version")
val versionNumberExpr = Regex("^(\\d+(\\.\\d+)?).*")
val matcher = versionNumberExpr.toPattern().matcher(javaVersionText)
if (!matcher.matches()) {
error("Incomprehensible Java version: $javaVersionText")
}
val versionNumberText = matcher.group(1)
versionNumberText.toDouble()
}
private val nativeGeneratedAnnotation: String by lazy {
when {
javaVersion <= 1.8 -> GeneratedAnnotationTypes.jdk8
else -> GeneratedAnnotationTypes.jdk9
}
}
val outputDirectory: Path by option("-o", "--out", help = "the output directory for generated files") val outputDirectory: Path by option("-o", "--out", help = "the output directory for generated files")
.path(fileOkay = false, folderOkay = true) .path(fileOkay = false, folderOkay = true)
.required() .required()
@ -135,6 +194,15 @@ class ThriftyCompiler {
help = "When set, @Generated annotations will be suppressed") help = "When set, @Generated annotations will be suppressed")
.flag(default = false) .flag(default = false)
val generatedAnnotationType: String by option(
"--generated-annotation-type",
help = "JDK 9 repackaged the traditional @Generated annotation. The current platform's annotation is used by default, unless overridden with this option")
.choice(
"jdk8" to GeneratedAnnotationTypes.jdk8,
"jdk9" to GeneratedAnnotationTypes.jdk9,
"native" to nativeGeneratedAnnotation)
.default("javax.annotation.Generated")
val kotlinFilePerType: Boolean by option( val kotlinFilePerType: Boolean by option(
"--kt-file-per-type", help = "Generate one .kt file per type; default is one per namespace.") "--kt-file-per-type", help = "Generate one .kt file per type; default is one per namespace.")
.flag(default = false) .flag(default = false)
@ -149,7 +217,18 @@ class ThriftyCompiler {
.path(exists = true, fileOkay = true, folderOkay = false, readable = true) .path(exists = true, fileOkay = true, folderOkay = false, readable = true)
.multiple() .multiple()
private val generatedAnnotationClassName: String? by lazy {
when {
omitGeneratedAnnotations -> null
else -> generatedAnnotationType
}
}
override fun run() { override fun run() {
if (javaVersion > 1.8 && generatedAnnotationClassName == GeneratedAnnotationTypes.jdk8) {
TermUi.echo("WARNING: You are using Java $javaVersion, but generating code annotated with $generatedAnnotationClassName")
}
val loader = Loader() val loader = Loader()
for (thriftFile in thriftFiles) { for (thriftFile in thriftFiles) {
loader.addThriftFile(thriftFile) loader.addThriftFile(thriftFile)
@ -207,14 +286,14 @@ class ThriftyCompiler {
gen.emitAndroidAnnotations(emitNullabilityAnnotations) gen.emitAndroidAnnotations(emitNullabilityAnnotations)
gen.emitFileComment(!omitFileComments) gen.emitFileComment(!omitFileComments)
gen.emitParcelable(emitParcelable) gen.emitParcelable(emitParcelable)
gen.emitGeneratedAnnotations(!omitGeneratedAnnotations) gen.emitGeneratedAnnotations(generatedAnnotationClassName)
gen.generate(outputDirectory) gen.generate(outputDirectory)
} }
private fun generateKotlin(schema: Schema) { private fun generateKotlin(schema: Schema) {
val gen = KotlinCodeGenerator(nameStyle) val gen = KotlinCodeGenerator(nameStyle)
.emitGeneratedAnnotations(!omitGeneratedAnnotations) .emitGeneratedAnnotations(generatedAnnotationClassName)
if (emitNullabilityAnnotations) { if (emitNullabilityAnnotations) {
TermUi.echo("Warning: Nullability annotations are unnecessary in Kotlin and will not be generated") TermUi.echo("Warning: Nullability annotations are unnecessary in Kotlin and will not be generated")

Просмотреть файл

@ -62,7 +62,12 @@ task compileTestThrift(type: Exec) {
dependsOn jarTask dependsOn jarTask
executable 'java' executable 'java'
args = ['-jar', jarTask.archivePath.absolutePath, "--out=$projectDir/build/generated-src/thrifty/java", "$projectDir/ClientThriftTest.thrift"] args = [
'-jar',
jarTask.archivePath.absolutePath,
"--out=$projectDir/build/generated-src/thrifty/java",
"--generated-annotation-type=native",
"$projectDir/ClientThriftTest.thrift"]
} }
@ -84,6 +89,7 @@ task kompileTestThrift(type: Exec) {
"--map-type=java.util.LinkedHashMap", "--map-type=java.util.LinkedHashMap",
"--set-type=java.util.LinkedHashSet", "--set-type=java.util.LinkedHashSet",
"--list-type=java.util.ArrayList", "--list-type=java.util.ArrayList",
"--generated-annotation-type=native",
"$projectDir/ClientThriftTest.thrift" "$projectDir/ClientThriftTest.thrift"
] ]
} }
@ -103,6 +109,7 @@ task kompileCoroutineTestThrift(type: Exec) {
jarTask.archivePath.absolutePath, jarTask.archivePath.absolutePath,
"--out=$projectDir/build/generated-src/thrifty/kotlin", "--out=$projectDir/build/generated-src/thrifty/kotlin",
"--lang=kotlin", "--lang=kotlin",
"--generated-annotation-type=native",
"--kt-coroutine-clients", "--kt-coroutine-clients",
"$projectDir/CoroutineClientTest.thrift" "$projectDir/CoroutineClientTest.thrift"
] ]

Просмотреть файл

@ -59,7 +59,6 @@ import java.time.format.DateTimeFormatter
import java.util.ArrayList import java.util.ArrayList
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import javax.annotation.Generated
class ThriftyCodeGenerator { class ThriftyCodeGenerator {
@ -72,7 +71,9 @@ class ThriftyCodeGenerator {
private var emitAndroidAnnotations: Boolean = false private var emitAndroidAnnotations: Boolean = false
private var emitParcelable: Boolean = false private var emitParcelable: Boolean = false
private var emitFileComment = true private var emitFileComment = true
private var emitGeneratedAnnotations = true private var generatedAnnotationType: ClassName? = null
private val emitGeneratedAnnotations: Boolean
get() = generatedAnnotationType != null
constructor(schema: Schema, namingPolicy: FieldNamingPolicy = FieldNamingPolicy.DEFAULT) { constructor(schema: Schema, namingPolicy: FieldNamingPolicy = FieldNamingPolicy.DEFAULT) {
@ -117,8 +118,8 @@ class ThriftyCodeGenerator {
return this return this
} }
fun emitGeneratedAnnotations(emitGeneratedAnnotations: Boolean): ThriftyCodeGenerator = apply { fun emitGeneratedAnnotations(annotationTypeName: String?) = apply {
this.emitGeneratedAnnotations = emitGeneratedAnnotations this.generatedAnnotationType = annotationTypeName?.let { ClassName.bestGuess(it) }
} }
fun usingTypeProcessor(typeProcessor: TypeProcessor): ThriftyCodeGenerator { fun usingTypeProcessor(typeProcessor: TypeProcessor): ThriftyCodeGenerator {
@ -208,7 +209,7 @@ class ThriftyCodeGenerator {
} }
private fun generatedAnnotation(): AnnotationSpec { private fun generatedAnnotation(): AnnotationSpec {
return AnnotationSpec.builder(Generated::class.java) return AnnotationSpec.builder(generatedAnnotationType!!)
.addMember("value", "\$S", ThriftyCodeGenerator::class.java.name) .addMember("value", "\$S", ThriftyCodeGenerator::class.java.name)
.addMember("comments", "\$S", "https://github.com/microsoft/thrifty") .addMember("comments", "\$S", "https://github.com/microsoft/thrifty")
.build() .build()

Просмотреть файл

@ -264,12 +264,6 @@ class ThriftyCodeGeneratorTest {
assertThat(file.toString()).isEqualTo(""" assertThat(file.toString()).isEqualTo("""
package byte_consts; package byte_consts;
import javax.annotation.Generated;
@Generated(
value = "com.microsoft.thrifty.gen.ThriftyCodeGenerator",
comments = "https://github.com/microsoft/thrifty"
)
public final class Constants { public final class Constants {
public static final byte I8 = (byte) 123; public static final byte I8 = (byte) 123;
@ -293,12 +287,6 @@ class ThriftyCodeGeneratorTest {
assertThat(file.toString()).isEqualTo(""" assertThat(file.toString()).isEqualTo("""
package short_consts; package short_consts;
import javax.annotation.Generated;
@Generated(
value = "com.microsoft.thrifty.gen.ThriftyCodeGenerator",
comments = "https://github.com/microsoft/thrifty"
)
public final class Constants { public final class Constants {
public static final short INT = (short) 0xFF; public static final short INT = (short) 0xFF;
@ -322,12 +310,6 @@ class ThriftyCodeGeneratorTest {
assertThat(file.toString()).isEqualTo(""" assertThat(file.toString()).isEqualTo("""
package int_consts; package int_consts;
import javax.annotation.Generated;
@Generated(
value = "com.microsoft.thrifty.gen.ThriftyCodeGenerator",
comments = "https://github.com/microsoft/thrifty"
)
public final class Constants { public final class Constants {
public static final int INT = 12345; public static final int INT = 12345;
@ -351,12 +333,6 @@ class ThriftyCodeGeneratorTest {
assertThat(file.toString()).isEqualTo(""" assertThat(file.toString()).isEqualTo("""
package long_consts; package long_consts;
import javax.annotation.Generated;
@Generated(
value = "com.microsoft.thrifty.gen.ThriftyCodeGenerator",
comments = "https://github.com/microsoft/thrifty"
)
public final class Constants { public final class Constants {
public static final long LONG = 0xFFFFFFFFFFL; public static final long LONG = 0xFFFFFFFFFFL;
@ -407,12 +383,6 @@ class ThriftyCodeGeneratorTest {
val expectedFormat = """ val expectedFormat = """
package sigils.consts; package sigils.consts;
import javax.annotation.Generated;
@Generated(
value = "com.microsoft.thrifty.gen.ThriftyCodeGenerator",
comments = "https://github.com/microsoft/thrifty"
)
public final class Constants { public final class Constants {
/** /**
* This comment has ${"$"}Dollar ${"$"}Signs * This comment has ${"$"}Dollar ${"$"}Signs
@ -453,15 +423,9 @@ class ThriftyCodeGeneratorTest {
val expected = """ val expected = """
package sigils.enums; package sigils.enums;
import javax.annotation.Generated;
/** /**
* ${"$"}Sigil here * ${"$"}Sigil here
*/ */
@Generated(
value = "com.microsoft.thrifty.gen.ThriftyCodeGenerator",
comments = "https://github.com/microsoft/thrifty"
)
public enum TestEnum { public enum TestEnum {
/** /**
* ${"$"}Good, here's another * ${"$"}Good, here's another
@ -541,10 +505,6 @@ class ThriftyCodeGeneratorTest {
/** /**
* ${"$"}A ${"$"}B ${"$"}C ${"$"}D ${"$"}E * ${"$"}A ${"$"}B ${"$"}C ${"$"}D ${"$"}E
*/ */
@Generated(
value = "com.microsoft.thrifty.gen.ThriftyCodeGenerator",
comments = "https://github.com/microsoft/thrifty"
)
public final class Foo implements Struct { public final class Foo implements Struct {
""".trimRawString() """.trimRawString()

Просмотреть файл

@ -86,7 +86,6 @@ import com.squareup.kotlinpoet.jvm.jvmField
import com.squareup.kotlinpoet.jvm.jvmStatic import com.squareup.kotlinpoet.jvm.jvmStatic
import okio.ByteString import okio.ByteString
import java.io.IOException import java.io.IOException
import javax.annotation.Generated
private object Tags { private object Tags {
val ADAPTER = "RESERVED:ADAPTER" val ADAPTER = "RESERVED:ADAPTER"
@ -186,7 +185,7 @@ class KotlinCodeGenerator(
var processor: KotlinTypeProcessor = NoTypeProcessor var processor: KotlinTypeProcessor = NoTypeProcessor
var outputStyle: OutputStyle = OutputStyle.FILE_PER_NAMESPACE var outputStyle: OutputStyle = OutputStyle.FILE_PER_NAMESPACE
var emitGeneratedAnnotations = true var generatedAnnotationType: ClassName? = null
fun filePerNamespace(): KotlinCodeGenerator = apply { outputStyle = OutputStyle.FILE_PER_NAMESPACE } fun filePerNamespace(): KotlinCodeGenerator = apply { outputStyle = OutputStyle.FILE_PER_NAMESPACE }
fun filePerType(): KotlinCodeGenerator = apply { outputStyle = OutputStyle.FILE_PER_TYPE } fun filePerType(): KotlinCodeGenerator = apply { outputStyle = OutputStyle.FILE_PER_TYPE }
@ -212,8 +211,8 @@ class KotlinCodeGenerator(
this.coroutineServiceClients = true this.coroutineServiceClients = true
} }
fun emitGeneratedAnnotations(shouldEmit: Boolean): KotlinCodeGenerator = apply { fun emitGeneratedAnnotations(type: String?): KotlinCodeGenerator = apply {
this.emitGeneratedAnnotations = shouldEmit this.generatedAnnotationType = type?.let { ClassName.bestGuess(type) }
} }
private object NoTypeProcessor : KotlinTypeProcessor { private object NoTypeProcessor : KotlinTypeProcessor {
@ -1878,20 +1877,20 @@ class KotlinCodeGenerator(
} }
private fun generatedAnnotation(): AnnotationSpec { private fun generatedAnnotation(): AnnotationSpec {
return AnnotationSpec.builder(Generated::class.java) return AnnotationSpec.builder(generatedAnnotationType!!)
.addMember("value = [%S]", KotlinCodeGenerator::class.java.name) .addMember("value = [%S]", KotlinCodeGenerator::class.java.name)
.addMember("comments = %S", "https://github.com/microsoft/thrifty") .addMember("comments = %S", "https://github.com/microsoft/thrifty")
.build() .build()
} }
private fun TypeSpec.Builder.addGeneratedAnnotation(): TypeSpec.Builder = apply { private fun TypeSpec.Builder.addGeneratedAnnotation(): TypeSpec.Builder = apply {
if (emitGeneratedAnnotations) { if (generatedAnnotationType != null) {
addAnnotation(generatedAnnotation()) addAnnotation(generatedAnnotation())
} }
} }
private fun PropertySpec.Builder.addGeneratedAnnotation(): PropertySpec.Builder = apply { private fun PropertySpec.Builder.addGeneratedAnnotation(): PropertySpec.Builder = apply {
if (emitGeneratedAnnotations) { if (generatedAnnotationType != null) {
addAnnotation(generatedAnnotation()) addAnnotation(generatedAnnotation())
} }
} }

Просмотреть файл

@ -238,16 +238,11 @@ class KotlinCodeGeneratorTest {
"${generate(thrift).single()}" shouldBe """ "${generate(thrift).single()}" shouldBe """
|package test.typedefs |package test.typedefs
| |
|import javax.annotation.Generated
|import kotlin.Int |import kotlin.Int
|import kotlin.collections.Map |import kotlin.collections.Map
| |
|typealias Weights = Map<Int, Int> |typealias Weights = Map<Int, Int>
| |
|@Generated(
| value = ["com.microsoft.thrifty.kgen.KotlinCodeGenerator"],
| comments = "https://github.com/microsoft/thrifty"
|)
|val WEIGHTS: Weights = mapOf(1 to 2) |val WEIGHTS: Weights = mapOf(1 to 2)
| |
""".trimMargin() """.trimMargin()
@ -293,16 +288,11 @@ class KotlinCodeGeneratorTest {
|package test.map_consts |package test.map_consts
| |
|import android.support.v4.util.ArrayMap |import android.support.v4.util.ArrayMap
|import javax.annotation.Generated
|import kotlin.Int |import kotlin.Int
|import kotlin.String |import kotlin.String
|import kotlin.collections.List |import kotlin.collections.List
|import kotlin.collections.Map |import kotlin.collections.Map
| |
|@Generated(
| value = ["com.microsoft.thrifty.kgen.KotlinCodeGenerator"],
| comments = "https://github.com/microsoft/thrifty"
|)
|val Maps: Map<Int, List<String>> = ArrayMap<Int, List<String>>(2).apply { |val Maps: Map<Int, List<String>> = ArrayMap<Int, List<String>>(2).apply {
| put(1, emptyList()) | put(1, emptyList())
| put(2, listOf("foo")) | put(2, listOf("foo"))
@ -323,18 +313,10 @@ class KotlinCodeGeneratorTest {
val file = generate(thrift) { coroutineServiceClients() } val file = generate(thrift) { coroutineServiceClients() }
file.single().toString() should contain(""" file.single().toString() should contain("""
|@Generated(
| value = ["com.microsoft.thrifty.kgen.KotlinCodeGenerator"],
| comments = "https://github.com/microsoft/thrifty"
|)
|interface Svc { |interface Svc {
| suspend fun doSomething(foo: Int): Int | suspend fun doSomething(foo: Int): Int
|} |}
| |
|@Generated(
| value = ["com.microsoft.thrifty.kgen.KotlinCodeGenerator"],
| comments = "https://github.com/microsoft/thrifty"
|)
|class SvcClient(protocol: Protocol, listener: AsyncClientBase.Listener) : AsyncClientBase(protocol, listener), |class SvcClient(protocol: Protocol, listener: AsyncClientBase.Listener) : AsyncClientBase(protocol, listener),
| Svc { | Svc {
| override suspend fun doSomething(foo: Int): Int = suspendCoroutine { cont -> | override suspend fun doSomething(foo: Int): Int = suspendCoroutine { cont ->