Switch to kotlin-compile-testing for JsonClassCodegenProcessorTe… (#928)

* Add kotlin-compile-testing dependency

* Add Okio dependency to tests to match kotlin-compile-testing

Without this, Okio.sink() fails to resolve at runtime

* Try converting privateConstructor() to kotlin-compile-testing

* Space

* Update to 1.2.2

* Use new kotlin source file API

* Use temporaryFolder as workingDir

* Extract prepareCompilation helper method

This allows for modifying the compilation with extra bits as needed

* Migrate tests entirely to new API

* Remove incorrect error message

This passed before, but was picked up in https://github.com/square/moshi/pull/903 before and observed to be incorrect.

* Remove custom kotlin compiler implementations

* Add an OK exit code test

This wasn't possible before!

* Remove jitpack dependency now that it's on mavencentral

* Move versions to parent pom

* Fix new test from rebase
This commit is contained in:
Zac Sweers
2019-09-25 16:41:31 -04:00
committed by GitHub
parent c0639316b1
commit 5f98e93698
5 changed files with 296 additions and 540 deletions

View File

@@ -25,7 +25,7 @@
<dependency> <dependency>
<groupId>com.squareup</groupId> <groupId>com.squareup</groupId>
<artifactId>kotlinpoet</artifactId> <artifactId>kotlinpoet</artifactId>
<version>1.3.0</version> <version>${kotlinpoet.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>net.ltgt.gradle.incap</groupId> <groupId>net.ltgt.gradle.incap</groupId>
@@ -37,12 +37,12 @@
<dependency> <dependency>
<groupId>com.google.auto</groupId> <groupId>com.google.auto</groupId>
<artifactId>auto-common</artifactId> <artifactId>auto-common</artifactId>
<version>0.10</version> <version>${auto-common.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.google.auto.service</groupId> <groupId>com.google.auto.service</groupId>
<artifactId>auto-service-annotations</artifactId> <artifactId>auto-service-annotations</artifactId>
<version>${autoservice.version}</version> <version>${auto-service.version}</version>
<scope>provided</scope> <scope>provided</scope>
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
@@ -56,32 +56,31 @@
<artifactId>assertj-core</artifactId> <artifactId>assertj-core</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>com.google.truth</groupId>
<artifactId>truth</artifactId>
<version>${truth.version}</version>
<scope>test</scope>
</dependency>
<!-- <!--
The Kotlin compiler must be near the end of the list because its .jar file includes an The Kotlin compiler usage must be near the end of the list because its .jar file includes an
obsolete version of Guava! obsolete version of Guava!
--> -->
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-compiler-embeddable</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-annotation-processing-embeddable</artifactId>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>me.eugeniomarletti.kotlin.metadata</groupId> <groupId>me.eugeniomarletti.kotlin.metadata</groupId>
<artifactId>kotlin-metadata</artifactId> <artifactId>kotlin-metadata</artifactId>
</dependency> </dependency>
<!--
Though we don't use compile-testing, including it is a convenient way to get tools.jar on the
classpath. This dependency is required by kapt3.
-->
<dependency> <dependency>
<groupId>com.google.testing.compile</groupId> <groupId>com.github.tschuchortdev</groupId>
<artifactId>compile-testing</artifactId> <artifactId>kotlin-compile-testing</artifactId>
<version>${kotlin-compile-testing.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
<version>${okio2.version}</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
</dependencies> </dependencies>
@@ -111,7 +110,7 @@
<annotationProcessorPath> <annotationProcessorPath>
<groupId>com.google.auto.service</groupId> <groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId> <artifactId>auto-service</artifactId>
<version>${autoservice.version}</version> <version>${auto-service.version}</version>
</annotationProcessorPath> </annotationProcessorPath>
<annotationProcessorPath> <annotationProcessorPath>
<groupId>net.ltgt.gradle.incap</groupId> <groupId>net.ltgt.gradle.incap</groupId>

View File

@@ -15,384 +15,352 @@
*/ */
package com.squareup.moshi.kotlin.codegen package com.squareup.moshi.kotlin.codegen
import com.tschuchort.compiletesting.KotlinCompilation
import com.tschuchort.compiletesting.SourceFile
import com.tschuchort.compiletesting.SourceFile.Companion.kotlin
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.jetbrains.kotlin.cli.common.ExitCode
import org.junit.Ignore import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TemporaryFolder import org.junit.rules.TemporaryFolder
import javax.annotation.processing.Processor
/** Execute kotlinc to confirm that either files are generated or errors are printed. */ /** Execute kotlinc to confirm that either files are generated or errors are printed. */
class JsonClassCodegenProcessorTest { class JsonClassCodegenProcessorTest {
@Rule @JvmField var temporaryFolder: TemporaryFolder = TemporaryFolder() @Rule @JvmField var temporaryFolder: TemporaryFolder = TemporaryFolder()
@Test fun privateConstructor() { @Test fun privateConstructor() {
val call = KotlinCompilerCall(temporaryFolder.root) val result = compile(kotlin("source.kt",
call.inheritClasspath = true """
call.addService(Processor::class, JsonClassCodegenProcessor::class) import com.squareup.moshi.JsonClass
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true)
| class PrivateConstructor private constructor(var a: Int, var b: Int) {
|@JsonClass(generateAdapter = true) fun a() = a
|class PrivateConstructor private constructor(var a: Int, var b: Int) { fun b() = b
| fun a() = a companion object {
| fun b() = b fun newInstance(a: Int, b: Int) = PrivateConstructor(a, b)
| companion object { }
| fun newInstance(a: Int, b: Int) = PrivateConstructor(a, b) }
| } """
|} ))
|""".trimMargin()) assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains("constructor is not internal or public")
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains("constructor is not internal or public")
} }
@Test fun privateConstructorParameter() { @Test fun privateConstructorParameter() {
val call = KotlinCompilerCall(temporaryFolder.root) val result = compile(kotlin("source.kt",
call.inheritClasspath = true """
call.addService(Processor::class, JsonClassCodegenProcessor::class) import com.squareup.moshi.JsonClass
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|
|@JsonClass(generateAdapter = true)
|class PrivateConstructorParameter(private var a: Int)
|""".trimMargin())
val result = call.execute() @JsonClass(generateAdapter = true)
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR) class PrivateConstructorParameter(private var a: Int)
assertThat(result.systemErr).contains("property a is not visible") """
))
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains("property a is not visible")
} }
@Test fun privateProperties() { @Test fun privateProperties() {
val call = KotlinCompilerCall(temporaryFolder.root) val result = compile(kotlin("source.kt",
call.inheritClasspath = true """
call.addService(Processor::class, JsonClassCodegenProcessor::class) import com.squareup.moshi.JsonClass
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|
|@JsonClass(generateAdapter = true)
|class PrivateProperties {
| private var a: Int = -1
| private var b: Int = -1
|}
|""".trimMargin())
val result = call.execute() @JsonClass(generateAdapter = true)
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR) class PrivateProperties {
assertThat(result.systemErr).contains("property a is not visible") private var a: Int = -1
private var b: Int = -1
}
"""
))
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains("property a is not visible")
} }
@Test fun interfacesNotSupported() { @Test fun interfacesNotSupported() {
val call = KotlinCompilerCall(temporaryFolder.root) val result = compile(kotlin("source.kt",
call.inheritClasspath = true """
call.addService(Processor::class, JsonClassCodegenProcessor::class) import com.squareup.moshi.JsonClass
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|
|@JsonClass(generateAdapter = true)
|interface Interface
|""".trimMargin())
val result = call.execute() @JsonClass(generateAdapter = true)
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR) interface Interface
assertThat(result.systemErr).contains( """
))
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains(
"error: @JsonClass can't be applied to Interface: must be a Kotlin class") "error: @JsonClass can't be applied to Interface: must be a Kotlin class")
} }
@Test fun abstractClassesNotSupported() { @Test fun interfacesDoNotErrorWhenGeneratorNotSet() {
val call = KotlinCompilerCall(temporaryFolder.root) val result = compile(kotlin("source.kt",
call.inheritClasspath = true """
call.addService(Processor::class, JsonClassCodegenProcessor::class) import com.squareup.moshi.JsonClass
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|
|@JsonClass(generateAdapter = true)
|abstract class AbstractClass(val a: Int)
|""".trimMargin())
val result = call.execute() @JsonClass(generateAdapter = true, generator="customGenerator")
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR) interface Interface
assertThat(result.systemErr).contains( """
))
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK)
}
@Test fun abstractClassesNotSupported() {
val result = compile(kotlin("source.kt",
"""
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
abstract class AbstractClass(val a: Int)
"""
))
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains(
"error: @JsonClass can't be applied to AbstractClass: must not be abstract") "error: @JsonClass can't be applied to AbstractClass: must not be abstract")
} }
@Test fun sealedClassesNotSupported() { @Test fun sealedClassesNotSupported() {
val call = KotlinCompilerCall(temporaryFolder.root) val result = compile(kotlin("source.kt",
call.inheritClasspath = true """
call.addService(Processor::class, JsonClassCodegenProcessor::class) import com.squareup.moshi.JsonClass
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true)
| sealed class SealedClass(val a: Int)
|@JsonClass(generateAdapter = true) """
|sealed class SealedClass(val a: Int) ))
|""".trimMargin()) assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains(
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains(
"error: @JsonClass can't be applied to SealedClass: must not be sealed") "error: @JsonClass can't be applied to SealedClass: must not be sealed")
} }
@Test fun innerClassesNotSupported() { @Test fun innerClassesNotSupported() {
val call = KotlinCompilerCall(temporaryFolder.root) val result = compile(kotlin("source.kt",
call.inheritClasspath = true """
call.addService(Processor::class, JsonClassCodegenProcessor::class) import com.squareup.moshi.JsonClass
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass class Outer {
| @JsonClass(generateAdapter = true)
|class Outer { inner class InnerClass(val a: Int)
| @JsonClass(generateAdapter = true) }
| inner class InnerClass(val a: Int) """
|} ))
|""".trimMargin()) assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains(
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains(
"error: @JsonClass can't be applied to Outer.InnerClass: must not be an inner class") "error: @JsonClass can't be applied to Outer.InnerClass: must not be an inner class")
} }
@Test fun enumClassesNotSupported() { @Test fun enumClassesNotSupported() {
val call = KotlinCompilerCall(temporaryFolder.root) val result = compile(kotlin("source.kt",
call.inheritClasspath = true """
call.addService(Processor::class, JsonClassCodegenProcessor::class) import com.squareup.moshi.JsonClass
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true)
| enum class KotlinEnum {
|@JsonClass(generateAdapter = true) A, B
|enum class KotlinEnum { }
| A, B """
|} ))
|""".trimMargin()) assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains(
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains(
"error: @JsonClass with 'generateAdapter = \"true\"' can't be applied to KotlinEnum: code gen for enums is not supported or necessary") "error: @JsonClass with 'generateAdapter = \"true\"' can't be applied to KotlinEnum: code gen for enums is not supported or necessary")
} }
// Annotation processors don't get called for local classes, so we don't have the opportunity to // Annotation processors don't get called for local classes, so we don't have the opportunity to
// print an error message. Instead local classes will fail at runtime. // print an error message. Instead local classes will fail at runtime.
@Ignore @Ignore @Test fun localClassesNotSupported() {
@Test fun localClassesNotSupported() { val result = compile(kotlin("source.kt",
val call = KotlinCompilerCall(temporaryFolder.root) """
call.inheritClasspath = true import com.squareup.moshi.JsonClass
call.addService(Processor::class, JsonClassCodegenProcessor::class)
call.addKt("source.kt", """ fun outer() {
|import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true)
| class LocalClass(val a: Int)
|fun outer() { }
| @JsonClass(generateAdapter = true) """
| class LocalClass(val a: Int) ))
|} assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
|""".trimMargin()) assertThat(result.messages).contains(
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains(
"error: @JsonClass can't be applied to LocalClass: must not be local") "error: @JsonClass can't be applied to LocalClass: must not be local")
} }
@Test fun objectDeclarationsNotSupported() { @Test fun objectDeclarationsNotSupported() {
val call = KotlinCompilerCall(temporaryFolder.root) val result = compile(kotlin("source.kt",
call.inheritClasspath = true """
call.addService(Processor::class, JsonClassCodegenProcessor::class) import com.squareup.moshi.JsonClass
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true)
| object ObjectDeclaration {
|@JsonClass(generateAdapter = true) var a = 5
|object ObjectDeclaration { }
| var a = 5 """
|} ))
|""".trimMargin()) assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
val result = call.execute() assertThat(result.messages).contains(
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains(
"error: @JsonClass can't be applied to ObjectDeclaration: must be a Kotlin class") "error: @JsonClass can't be applied to ObjectDeclaration: must be a Kotlin class")
} }
@Test fun objectExpressionsNotSupported() { @Test fun objectExpressionsNotSupported() {
val call = KotlinCompilerCall(temporaryFolder.root) val result = compile(kotlin("source.kt",
call.inheritClasspath = true """
call.addService(Processor::class, JsonClassCodegenProcessor::class) import com.squareup.moshi.JsonClass
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true)
| val expression = object : Any() {
|@JsonClass(generateAdapter = true) var a = 5
|val expression = object : Any() { }
| var a = 5 """
|} ))
|""".trimMargin()) assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains(
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains(
"error: @JsonClass can't be applied to expression\$annotations(): must be a Kotlin class") "error: @JsonClass can't be applied to expression\$annotations(): must be a Kotlin class")
} }
@Test fun requiredTransientConstructorParameterFails() { @Test fun requiredTransientConstructorParameterFails() {
val call = KotlinCompilerCall(temporaryFolder.root) val result = compile(kotlin("source.kt",
call.inheritClasspath = true """
call.addService(Processor::class, JsonClassCodegenProcessor::class) import com.squareup.moshi.JsonClass
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true)
| class RequiredTransientConstructorParameter(@Transient var a: Int)
|@JsonClass(generateAdapter = true) """
|class RequiredTransientConstructorParameter(@Transient var a: Int) ))
|""".trimMargin()) assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains(
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains(
"error: No default value for transient property a") "error: No default value for transient property a")
} }
@Test fun nonPropertyConstructorParameter() { @Test fun nonPropertyConstructorParameter() {
val call = KotlinCompilerCall(temporaryFolder.root) val result = compile(kotlin("source.kt",
call.inheritClasspath = true """
call.addService(Processor::class, JsonClassCodegenProcessor::class) import com.squareup.moshi.JsonClass
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
| @JsonClass(generateAdapter = true)
|@JsonClass(generateAdapter = true) class NonPropertyConstructorParameter(a: Int, val b: Int)
|class NonPropertyConstructorParameter(a: Int, val b: Int) """
|""".trimMargin()) ))
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
val result = call.execute() assertThat(result.messages).contains(
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains(
"error: No property for required constructor parameter a") "error: No property for required constructor parameter a")
} }
@Test fun badGeneratedAnnotation() { @Test fun badGeneratedAnnotation() {
val call = KotlinCompilerCall(temporaryFolder.root) val result = prepareCompilation(kotlin("source.kt",
call.inheritClasspath = true """
call.addService(Processor::class, JsonClassCodegenProcessor::class) import com.squareup.moshi.JsonClass
call.kaptArgs[JsonClassCodegenProcessor.OPTION_GENERATED] = "javax.annotation.GeneratedBlerg"
call.addKt("source.kt", """ @JsonClass(generateAdapter = true)
|import com.squareup.moshi.JsonClass data class Foo(val a: Int)
| """
|@JsonClass(generateAdapter = true) )).apply {
|data class Foo(val a: Int) kaptArgs[JsonClassCodegenProcessor.OPTION_GENERATED] = "javax.annotation.GeneratedBlerg"
|""".trimMargin()) }.compile()
assertThat(result.messages).contains(
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains(
"Invalid option value for ${JsonClassCodegenProcessor.OPTION_GENERATED}") "Invalid option value for ${JsonClassCodegenProcessor.OPTION_GENERATED}")
} }
@Test fun multipleErrors() { @Test fun multipleErrors() {
val call = KotlinCompilerCall(temporaryFolder.root) val result = compile(kotlin("source.kt",
call.inheritClasspath = true """
call.addService(Processor::class, JsonClassCodegenProcessor::class) import com.squareup.moshi.JsonClass
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|
|@JsonClass(generateAdapter = true)
|class Class1(private var a: Int, private var b: Int)
|
|@JsonClass(generateAdapter = true)
|class Class2(private var c: Int)
|""".trimMargin())
val result = call.execute() @JsonClass(generateAdapter = true)
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR) class Class1(private var a: Int, private var b: Int)
assertThat(result.systemErr).contains("property a is not visible")
assertThat(result.systemErr).contains("property b is not visible") @JsonClass(generateAdapter = true)
assertThat(result.systemErr).contains("property c is not visible") class Class2(private var c: Int)
"""
))
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains("property a is not visible")
assertThat(result.messages).contains("property c is not visible")
} }
@Test fun extendPlatformType() { @Test fun extendPlatformType() {
val call = KotlinCompilerCall(temporaryFolder.root) val result = compile(kotlin("source.kt",
call.inheritClasspath = true """
call.addService(Processor::class, JsonClassCodegenProcessor::class) import com.squareup.moshi.JsonClass
call.addKt("source.kt", """ import java.util.Date
|import com.squareup.moshi.JsonClass
|import java.util.Date
|
|@JsonClass(generateAdapter = true)
|class ExtendsPlatformClass(var a: Int) : Date()
|""".trimMargin())
val result = call.execute() @JsonClass(generateAdapter = true)
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR) class ExtendsPlatformClass(var a: Int) : Date()
assertThat(result.systemErr).contains("supertype java.util.Date is not a Kotlin type") """
))
assertThat(result.messages).contains("supertype java.util.Date is not a Kotlin type")
} }
@Test fun extendJavaType() { @Test fun extendJavaType() {
val call = KotlinCompilerCall(temporaryFolder.root) val result = compile(kotlin("source.kt",
call.inheritClasspath = true """
call.addService(Processor::class, JsonClassCodegenProcessor::class) import com.squareup.moshi.JsonClass
call.addKt("source.kt", """ import com.squareup.moshi.kotlin.codegen.JavaSuperclass
|import com.squareup.moshi.JsonClass
|import com.squareup.moshi.kotlin.codegen.JavaSuperclass @JsonClass(generateAdapter = true)
| class ExtendsJavaType(var b: Int) : JavaSuperclass()
|@JsonClass(generateAdapter = true) """
|class ExtendsJavaType(var b: Int) : JavaSuperclass() ))
|""".trimMargin()) assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages)
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr)
.contains("supertype com.squareup.moshi.kotlin.codegen.JavaSuperclass is not a Kotlin type") .contains("supertype com.squareup.moshi.kotlin.codegen.JavaSuperclass is not a Kotlin type")
} }
@Test @Test fun nonFieldApplicableQualifier() {
fun nonFieldApplicableQualifier() { val result = compile(kotlin("source.kt",
val call = KotlinCompilerCall(temporaryFolder.root) """
call.inheritClasspath = true import com.squareup.moshi.JsonClass
call.addService(Processor::class, JsonClassCodegenProcessor::class) import com.squareup.moshi.JsonQualifier
call.addKt("source.kt", """ import kotlin.annotation.AnnotationRetention.RUNTIME
|import com.squareup.moshi.JsonClass import kotlin.annotation.AnnotationTarget.PROPERTY
|import com.squareup.moshi.JsonQualifier import kotlin.annotation.Retention
|import kotlin.annotation.AnnotationRetention.RUNTIME import kotlin.annotation.Target
|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() @Retention(RUNTIME)
println(result.systemErr) @Target(PROPERTY)
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR) @JsonQualifier
assertThat(result.systemErr).contains("JsonQualifier @UpperCase must support FIELD target") annotation class UpperCase
@JsonClass(generateAdapter = true)
class ClassWithQualifier(@UpperCase val a: Int)
"""
))
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains("JsonQualifier @UpperCase must support FIELD target")
} }
@Test @Test fun nonRuntimeQualifier() {
fun nonRuntimeQualifier() { val result = compile(kotlin("source.kt",
val call = KotlinCompilerCall(temporaryFolder.root) """
call.inheritClasspath = true import com.squareup.moshi.JsonClass
call.addService(Processor::class, JsonClassCodegenProcessor::class) import com.squareup.moshi.JsonQualifier
call.addKt("source.kt", """ import kotlin.annotation.AnnotationRetention.BINARY
|import com.squareup.moshi.JsonClass import kotlin.annotation.AnnotationTarget.FIELD
|import com.squareup.moshi.JsonQualifier import kotlin.annotation.AnnotationTarget.PROPERTY
|import kotlin.annotation.AnnotationRetention.BINARY import kotlin.annotation.Retention
|import kotlin.annotation.AnnotationTarget.FIELD import kotlin.annotation.Target
|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() @Retention(BINARY)
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR) @Target(PROPERTY, FIELD)
assertThat(result.systemErr).contains("JsonQualifier @UpperCase must have RUNTIME retention") @JsonQualifier
annotation class UpperCase
@JsonClass(generateAdapter = true)
class ClassWithQualifier(@UpperCase val a: Int)
"""
))
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains("JsonQualifier @UpperCase must have RUNTIME retention")
} }
private fun prepareCompilation(vararg sourceFiles: SourceFile): KotlinCompilation {
return KotlinCompilation()
.apply {
workingDir = temporaryFolder.root
annotationProcessors = listOf(JsonClassCodegenProcessor())
inheritClassPath = true
sources = sourceFiles.asList()
verbose = false
}
}
private fun compile(vararg sourceFiles: SourceFile): KotlinCompilation.Result {
return prepareCompilation(*sourceFiles).compile()
}
} }

View File

@@ -1,193 +0,0 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.moshi.kotlin.codegen
import com.google.common.collect.LinkedHashMultimap
import okio.Buffer
import okio.Okio
import org.jetbrains.kotlin.cli.common.CLITool
import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler
import java.io.File
import java.io.FileOutputStream
import java.io.ObjectOutputStream
import java.io.PrintStream
import java.net.URLClassLoader
import java.net.URLDecoder
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
import kotlin.reflect.KClass
/** Prepares an invocation of the Kotlin compiler. */
class KotlinCompilerCall(var scratchDir: File) {
val sourcesDir = File(scratchDir, "sources")
val classesDir = File(scratchDir, "classes")
val servicesJar = File(scratchDir, "services.jar")
var inheritClasspath = false
val args = mutableListOf<String>()
val kaptArgs = mutableMapOf<String, String>()
val classpath = mutableListOf<String>()
val services = LinkedHashMultimap.create<KClass<*>, KClass<*>>()
/** Adds a source file to be compiled. */
fun addKt(path: String, source: String) {
val sourceFile = File(sourcesDir, path)
sourceFile.parentFile.mkdirs()
Okio.buffer(Okio.sink(sourceFile)).use {
it.writeUtf8(source)
}
}
/** Adds a service like an annotation processor to make available to the compiler. */
fun addService(serviceClass: KClass<*>, implementation: KClass<*>) {
services.put(serviceClass, implementation)
}
fun execute(): KotlinCompilerResult {
val fullArgs = mutableListOf<String>()
fullArgs.addAll(args)
fullArgs.add("-d")
fullArgs.add(classesDir.toString())
val fullClasspath = fullClasspath()
if (fullClasspath.isNotEmpty()) {
fullArgs.add("-classpath")
fullArgs.add(fullClasspath.joinToString(separator = ":"))
}
for (source in sourcesDir.listFiles()) {
fullArgs.add(source.toString())
}
fullArgs.addAll(annotationProcessorArgs())
if (kaptArgs.isNotEmpty()) {
fullArgs.apply {
add("-P")
add("plugin:org.jetbrains.kotlin.kapt3:apoptions=${encodeOptions(kaptArgs)}")
}
}
val systemErrBuffer = Buffer()
val oldSystemErr = System.err
System.setErr(PrintStream(systemErrBuffer.outputStream()))
try {
val exitCode = CLITool.doMainNoExit(K2JVMCompiler(), fullArgs.toTypedArray())
val systemErr = systemErrBuffer.readUtf8()
return KotlinCompilerResult(systemErr, exitCode)
} finally {
System.setErr(oldSystemErr)
}
}
/** Returns arguments necessary to enable and configure kapt3. */
private fun annotationProcessorArgs(): List<String> {
val kaptSourceDir = File(scratchDir, "kapt/sources")
val kaptStubsDir = File(scratchDir, "kapt/stubs")
return listOf(
"-Xplugin=${kapt3Jar()}",
"-P", "plugin:org.jetbrains.kotlin.kapt3:sources=$kaptSourceDir",
"-P", "plugin:org.jetbrains.kotlin.kapt3:classes=$classesDir",
"-P", "plugin:org.jetbrains.kotlin.kapt3:stubs=$kaptStubsDir",
"-P", "plugin:org.jetbrains.kotlin.kapt3:apclasspath=$servicesJar",
"-P", "plugin:org.jetbrains.kotlin.kapt3:correctErrorTypes=true"
)
}
/** Returns the classpath to use when compiling code. */
private fun fullClasspath(): List<String> {
val result = mutableListOf<String>()
result.addAll(classpath)
// Copy over the classpath of the running application.
if (inheritClasspath) {
for (classpathFile in classpathFiles()) {
result.add(classpathFile.toString())
}
}
if (!services.isEmpty) {
writeServicesJar()
result.add(servicesJar.toString())
}
return result.toList()
}
/**
* Generate a .jar file that holds ServiceManager registrations. Necessary because AutoService's
* results might not be visible to this test.
*/
private fun writeServicesJar() {
ZipOutputStream(FileOutputStream(servicesJar)).use { zipOutputStream ->
for (entry in services.asMap()) {
zipOutputStream.putNextEntry(
ZipEntry("META-INF/services/${entry.key.qualifiedName}"))
val serviceFile = Okio.buffer(Okio.sink(zipOutputStream))
for (implementation in entry.value) {
serviceFile.writeUtf8(implementation.qualifiedName!!)
serviceFile.writeUtf8("\n")
}
serviceFile.emit() // Don't close the entry; that closes the file.
zipOutputStream.closeEntry()
}
}
}
/** Returns the files on the host process' classpath. */
private fun classpathFiles(): List<File> {
val classLoader = JsonClassCodegenProcessorTest::class.java.classLoader
if (classLoader !is URLClassLoader) {
throw UnsupportedOperationException("unable to extract classpath from $classLoader")
}
val result = mutableListOf<File>()
for (url in classLoader.urLs) {
if (url.protocol != "file") {
throw UnsupportedOperationException("unable to handle classpath element $url")
}
result.add(File(URLDecoder.decode(url.path, "UTF-8")))
}
return result.toList()
}
/** Returns the path to the kotlin-annotation-processing .jar file. */
private fun kapt3Jar(): File {
for (file in classpathFiles()) {
if (file.name.startsWith("kotlin-annotation-processing-embeddable")) return file
}
throw IllegalStateException("no kotlin-annotation-processing-embeddable jar on classpath:\n " +
"${classpathFiles().joinToString(separator = "\n ")}}")
}
/**
* Base64 encodes a mapping of annotation processor args for kapt, as specified by
* https://kotlinlang.org/docs/reference/kapt.html#apjavac-options-encoding
*/
private fun encodeOptions(options: Map<String, String>): String {
val buffer = Buffer()
ObjectOutputStream(buffer.outputStream()).use { oos ->
oos.writeInt(options.size)
for ((key, value) in options.entries) {
oos.writeUTF(key)
oos.writeUTF(value)
}
}
return buffer.readByteString().base64()
}
}

View File

@@ -1,23 +0,0 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.moshi.kotlin.codegen
import org.jetbrains.kotlin.cli.common.ExitCode
class KotlinCompilerResult(
val systemErr: String,
var exitCode: ExitCode
)

19
pom.xml
View File

@@ -29,20 +29,25 @@
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.7</java.version> <java.version>1.7</java.version>
<kotlin.version>1.3.40</kotlin.version>
<kotlin-metadata.version>1.4.0</kotlin-metadata.version>
<dokka.version>0.9.17</dokka.version>
<maven-assembly.version>3.1.0</maven-assembly.version>
<incap.version>0.2</incap.version>
<autoservice.version>1.0-rc5</autoservice.version>
<!-- Dependencies --> <!-- Dependencies -->
<auto-common.version>0.10</auto-common.version>
<auto-service.version>1.0-rc5</auto-service.version>
<dokka.version>0.9.17</dokka.version>
<incap.version>0.2</incap.version>
<okio.version>1.16.0</okio.version> <okio.version>1.16.0</okio.version>
<okio2.version>2.1.0</okio2.version>
<kotlin.version>1.3.40</kotlin.version>
<kotlinpoet.version>1.3.0</kotlinpoet.version>
<kotlin-metadata.version>1.4.0</kotlin-metadata.version>
<maven-assembly.version>3.1.0</maven-assembly.version>
<!-- Test Dependencies --> <!-- Test Dependencies -->
<assertj.version>3.11.1</assertj.version>
<compile-testing.version>0.15</compile-testing.version> <compile-testing.version>0.15</compile-testing.version>
<junit.version>4.12</junit.version> <junit.version>4.12</junit.version>
<assertj.version>3.11.1</assertj.version> <kotlin-compile-testing.version>1.2.2</kotlin-compile-testing.version>
<truth.version>1.0</truth.version>
</properties> </properties>
<scm> <scm>