Remove KAPT support (#1848)

* Remove KAPT support

* Stick KSP on the root classpath

* Another

* Another
This commit is contained in:
Zac Sweers
2024-06-03 10:35:36 -04:00
committed by GitHub
parent c8cda23160
commit b648d6ee33
15 changed files with 7 additions and 1764 deletions

View File

@@ -10,7 +10,7 @@ jobs:
strategy:
fail-fast: false
matrix:
kotlin-test-mode: [ 'REFLECT', 'KSP', 'KAPT' ]
kotlin-test-mode: [ 'REFLECT', 'KSP' ]
steps:
- name: Checkout

View File

@@ -1076,7 +1076,7 @@ Note that the reflection adapter transitively depends on the `kotlin-reflect` li
#### Codegen
Moshis Kotlin codegen support can be used as an annotation processor (via [kapt][kapt]) or Kotlin SymbolProcessor ([KSP][ksp]).
Moshis Kotlin codegen support can be used as a Kotlin SymbolProcessor ([KSP][ksp]).
It generates a small and fast adapter for each of your Kotlin classes at compile-time. Enable it by annotating
each class that you want to encode as JSON:
@@ -1109,23 +1109,6 @@ dependencies {
```
</details>
<details>
<summary>Kapt</summary>
```xml
<dependency>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi-kotlin-codegen</artifactId>
<version>1.15.1</version>
<scope>provided</scope>
</dependency>
```
```kotlin
kapt("com.squareup.moshi:moshi-kotlin-codegen:1.15.1")
```
</details>
#### Limitations
If your Kotlin class has a superclass, it must also be a Kotlin class. Neither reflection or codegen
@@ -1190,5 +1173,4 @@ License
[okhttp]: https://github.com/square/okhttp/
[gson]: https://github.com/google/gson/
[javadoc]: https://square.github.io/moshi/1.x/moshi/
[kapt]: https://kotlinlang.org/docs/reference/kapt.html
[ksp]: https://github.com/google/ksp

View File

@@ -27,6 +27,7 @@ plugins {
alias(libs.plugins.dokka) apply false
alias(libs.plugins.spotless)
alias(libs.plugins.japicmp) apply false
alias(libs.plugins.ksp) apply false
}
allprojects {

View File

@@ -2,11 +2,11 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm")
kotlin("kapt")
alias(libs.plugins.ksp)
}
dependencies {
kapt(project(":moshi-kotlin-codegen"))
ksp(project(":moshi-kotlin-codegen"))
compileOnly(libs.jsr305)
implementation(project(":moshi"))
implementation(project(":moshi-adapters"))

View File

@@ -1,4 +1,2 @@
# Memory for Dokka https://github.com/Kotlin/dokka/issues/1405
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
kapt.include.compile.classpath=false

View File

@@ -1,4 +1,3 @@
import com.github.jengelman.gradle.plugins.shadow.transformers.ServiceFileTransformer
import com.vanniktech.maven.publish.JavadocJar.None
import com.vanniktech.maven.publish.KotlinJvm
import com.vanniktech.maven.publish.MavenPublishBaseExtension
@@ -8,7 +7,6 @@ plugins {
kotlin("jvm")
id("com.google.devtools.ksp")
id("com.vanniktech.maven.publish.base")
alias(libs.plugins.mavenShadow)
}
tasks.withType<KotlinCompile>().configureEach {
@@ -26,35 +24,9 @@ tasks.compileTestKotlin {
}
}
// --add-opens for kapt to work. KGP covers this for us but local JVMs in tests do not
tasks.withType<Test>().configureEach {
jvmArgs(
"--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
)
}
val shade: Configuration = configurations.maybeCreate("compileShaded")
configurations.getByName("compileOnly").extendsFrom(shade)
dependencies {
implementation(project(":moshi"))
shade(libs.kotlinxMetadata) {
exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib")
}
api(libs.kotlinpoet)
shade(libs.kotlinpoet.metadata) {
exclude(group = "org.jetbrains.kotlin")
exclude(group = "com.squareup", module = "kotlinpoet")
exclude(group = "com.google.guava")
}
implementation(libs.kotlinpoet.ksp)
implementation(libs.guava)
implementation(libs.asm)
@@ -83,26 +55,6 @@ dependencies {
testImplementation(libs.kotlinCompileTesting)
}
val shadowJar =
tasks.shadowJar.apply {
configure {
archiveClassifier.set("")
configurations = listOf(shade)
relocate("com.squareup.kotlinpoet.metadata", "com.squareup.moshi.kotlinpoet.metadata")
relocate(
"com.squareup.kotlinpoet.classinspector",
"com.squareup.moshi.kotlinpoet.classinspector",
)
relocate("kotlinx.metadata", "com.squareup.moshi.kotlinx.metadata")
transformers.add(ServiceFileTransformer())
}
}
artifacts {
runtimeOnly(shadowJar)
archives(shadowJar)
}
configure<MavenPublishBaseExtension> {
configure(KotlinJvm(javadocJar = None()))
}

View File

@@ -1,65 +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
*
* https://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.apt
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.DelicateKotlinPoetApi
import com.squareup.kotlinpoet.asClassName
import javax.lang.model.element.ElementKind.CLASS
import javax.lang.model.element.TypeElement
import javax.lang.model.type.DeclaredType
import javax.lang.model.util.Types
private val OBJECT_CLASS = ClassName("java.lang", "Object")
/**
* A concrete type like `List<String>` with enough information to know how to resolve its type
* variables.
*/
internal class AppliedType private constructor(
val element: TypeElement,
private val mirror: DeclaredType,
) {
/** Returns all supertypes of this, recursively. Only [CLASS] is used as we can't really use other types. */
@OptIn(DelicateKotlinPoetApi::class)
fun superclasses(
types: Types,
result: LinkedHashSet<AppliedType> = LinkedHashSet(),
): LinkedHashSet<AppliedType> {
result.add(this)
for (supertype in types.directSupertypes(mirror)) {
val supertypeDeclaredType = supertype as DeclaredType
val supertypeElement = supertypeDeclaredType.asElement() as TypeElement
if (supertypeElement.kind != CLASS) {
continue
} else if (supertypeElement.asClassName() == OBJECT_CLASS) {
// Don't load properties for java.lang.Object.
continue
}
val appliedSuperclass = AppliedType(supertypeElement, supertypeDeclaredType)
appliedSuperclass.superclasses(types, result)
}
return result
}
override fun toString() = mirror.toString()
companion object {
operator fun invoke(typeElement: TypeElement): AppliedType {
return AppliedType(typeElement, typeElement.asType() as DeclaredType)
}
}
}

View File

@@ -1,186 +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
*
* https://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.apt
import com.google.auto.service.AutoService
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.metadata.classinspectors.ElementsClassInspector
import com.squareup.moshi.JsonClass
import com.squareup.moshi.kotlin.codegen.api.AdapterGenerator
import com.squareup.moshi.kotlin.codegen.api.Options.OPTION_GENERATED
import com.squareup.moshi.kotlin.codegen.api.Options.OPTION_GENERATE_PROGUARD_RULES
import com.squareup.moshi.kotlin.codegen.api.Options.OPTION_INSTANTIATE_ANNOTATIONS
import com.squareup.moshi.kotlin.codegen.api.Options.POSSIBLE_GENERATED_NAMES
import com.squareup.moshi.kotlin.codegen.api.ProguardConfig
import com.squareup.moshi.kotlin.codegen.api.PropertyGenerator
import javax.annotation.processing.AbstractProcessor
import javax.annotation.processing.Filer
import javax.annotation.processing.Messager
import javax.annotation.processing.ProcessingEnvironment
import javax.annotation.processing.Processor
import javax.annotation.processing.RoundEnvironment
import javax.lang.model.SourceVersion
import javax.lang.model.element.Element
import javax.lang.model.element.TypeElement
import javax.lang.model.util.Elements
import javax.lang.model.util.Types
import javax.tools.Diagnostic
import javax.tools.StandardLocation
/**
* An annotation processor that reads Kotlin data classes and generates Moshi JsonAdapters for them.
* This generates Kotlin code, and understands basic Kotlin language features like default values
* and companion objects.
*
* The generated class will match the visibility of the given data class (i.e. if it's internal, the
* adapter will also be internal).
*/
@AutoService(Processor::class)
public class JsonClassCodegenProcessor : AbstractProcessor() {
private lateinit var types: Types
private lateinit var elements: Elements
private lateinit var filer: Filer
private lateinit var messager: Messager
private lateinit var cachedClassInspector: MoshiCachedClassInspector
private val annotation = JsonClass::class.java
private var generatedType: ClassName? = null
private var generateProguardRules: Boolean = true
private var instantiateAnnotations: Boolean = true
override fun getSupportedAnnotationTypes(): Set<String> = setOf(annotation.canonicalName)
override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latest()
override fun getSupportedOptions(): Set<String> = setOf(OPTION_GENERATED)
override fun init(processingEnv: ProcessingEnvironment) {
super.init(processingEnv)
generatedType = processingEnv.options[OPTION_GENERATED]?.let {
POSSIBLE_GENERATED_NAMES[it] ?: error(
"Invalid option value for $OPTION_GENERATED. Found $it, " +
"allowable values are $POSSIBLE_GENERATED_NAMES.",
)
}
generateProguardRules = processingEnv.options[OPTION_GENERATE_PROGUARD_RULES]?.toBooleanStrictOrNull() ?: true
instantiateAnnotations = processingEnv.options[OPTION_INSTANTIATE_ANNOTATIONS]?.toBooleanStrictOrNull() ?: true
this.types = processingEnv.typeUtils
this.elements = processingEnv.elementUtils
this.filer = processingEnv.filer
this.messager = processingEnv.messager
cachedClassInspector = MoshiCachedClassInspector(ElementsClassInspector.create(elements, types))
}
override fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Boolean {
if (roundEnv.errorRaised()) {
// An error was raised in the previous round. Don't try anything for now to avoid adding
// possible more noise.
return false
}
for (type in roundEnv.getElementsAnnotatedWith(annotation)) {
if (type !is TypeElement) {
messager.printMessage(
Diagnostic.Kind.ERROR,
"@JsonClass can't be applied to $type: must be a Kotlin class",
type,
)
continue
}
val jsonClass = type.getAnnotation(annotation)
if (jsonClass.generateAdapter && jsonClass.generator.isEmpty()) {
val generator = adapterGenerator(type, cachedClassInspector) ?: continue
val preparedAdapter = generator
.prepare(generateProguardRules) { spec ->
spec.toBuilder()
.apply {
@Suppress("DEPRECATION") // This is a Java type
generatedType?.let { generatedClassName ->
addAnnotation(
AnnotationSpec.builder(generatedClassName)
.addMember(
"value = [%S]",
JsonClassCodegenProcessor::class.java.canonicalName,
)
.addMember("comments = %S", "https://github.com/square/moshi")
.build(),
)
}
}
.addOriginatingElement(type)
.build()
}
preparedAdapter.spec.writeTo(filer)
preparedAdapter.proguardConfig?.writeTo(filer, type)
}
}
return false
}
private fun adapterGenerator(
element: TypeElement,
cachedClassInspector: MoshiCachedClassInspector,
): AdapterGenerator? {
val type = targetType(
messager,
elements,
types,
element,
cachedClassInspector,
) ?: return null
val properties = mutableMapOf<String, PropertyGenerator>()
for (property in type.properties.values) {
val generator = property.generator(messager, element, elements)
if (generator != null) {
properties[property.name] = generator
}
}
for ((name, parameter) in type.constructor.parameters) {
if (type.properties[parameter.name] == null && !parameter.hasDefault) {
messager.printMessage(
Diagnostic.Kind.ERROR,
"No property for required constructor parameter $name",
element,
)
return null
}
}
// Sort properties so that those with constructor parameters come first.
val sortedProperties = properties.values.sortedBy {
if (it.hasConstructorParameter) {
it.target.parameterIndex
} else {
Integer.MAX_VALUE
}
}
return AdapterGenerator(type, sortedProperties)
}
}
/** Writes this config to a [filer]. */
private fun ProguardConfig.writeTo(filer: Filer, vararg originatingElements: Element) {
filer.createResource(StandardLocation.CLASS_OUTPUT, "", "${outputFilePathWithoutExtension(targetClass.canonicalName)}.pro", *originatingElements)
.openWriter()
.use(::writeTo)
}

View File

@@ -1,56 +0,0 @@
/*
* Copyright (C) 2020 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
*
* https://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.apt
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.metadata.specs.ClassInspector
import com.squareup.kotlinpoet.metadata.specs.toTypeSpec
import com.squareup.kotlinpoet.metadata.toKmClass
import kotlinx.metadata.KmClass
import java.util.TreeMap
import javax.lang.model.element.TypeElement
/** KmClass doesn't implement equality natively. */
private val KmClassComparator = compareBy<KmClass> { it.name }
/**
* This cached API over [ClassInspector] that caches certain lookups Moshi does potentially multiple
* times. This is useful mostly because it avoids duplicate reloads in cases like common base
* classes, common enclosing types, etc.
*/
internal class MoshiCachedClassInspector(private val classInspector: ClassInspector) {
private val elementToSpecCache = mutableMapOf<TypeElement, TypeSpec>()
private val kmClassToSpecCache = TreeMap<KmClass, TypeSpec>(KmClassComparator)
private val metadataToKmClassCache = mutableMapOf<Metadata, KmClass>()
fun toKmClass(metadata: Metadata): KmClass {
return metadataToKmClassCache.getOrPut(metadata) {
metadata.toKmClass()
}
}
fun toTypeSpec(kmClass: KmClass): TypeSpec {
return kmClassToSpecCache.getOrPut(kmClass) {
kmClass.toTypeSpec(classInspector)
}
}
fun toTypeSpec(element: TypeElement): TypeSpec {
return elementToSpecCache.getOrPut(element) {
toTypeSpec(toKmClass(element.metadata))
}
}
}

View File

@@ -1,539 +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
*
* https://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.apt
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.DelicateKotlinPoetApi
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.asClassName
import com.squareup.kotlinpoet.asTypeName
import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview
import com.squareup.kotlinpoet.metadata.isAbstract
import com.squareup.kotlinpoet.metadata.isClass
import com.squareup.kotlinpoet.metadata.isEnum
import com.squareup.kotlinpoet.metadata.isInner
import com.squareup.kotlinpoet.metadata.isInternal
import com.squareup.kotlinpoet.metadata.isLocal
import com.squareup.kotlinpoet.metadata.isPublic
import com.squareup.kotlinpoet.metadata.isSealed
import com.squareup.kotlinpoet.tag
import com.squareup.moshi.Json
import com.squareup.moshi.JsonQualifier
import com.squareup.moshi.kotlin.codegen.api.DelegateKey
import com.squareup.moshi.kotlin.codegen.api.PropertyGenerator
import com.squareup.moshi.kotlin.codegen.api.TargetConstructor
import com.squareup.moshi.kotlin.codegen.api.TargetParameter
import com.squareup.moshi.kotlin.codegen.api.TargetProperty
import com.squareup.moshi.kotlin.codegen.api.TargetType
import com.squareup.moshi.kotlin.codegen.api.rawType
import com.squareup.moshi.kotlin.codegen.api.unwrapTypeAlias
import kotlinx.metadata.KmConstructor
import kotlinx.metadata.jvm.signature
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import javax.annotation.processing.Messager
import javax.lang.model.element.AnnotationMirror
import javax.lang.model.element.Element
import javax.lang.model.element.TypeElement
import javax.lang.model.type.DeclaredType
import javax.lang.model.util.Elements
import javax.lang.model.util.Types
import javax.tools.Diagnostic.Kind.ERROR
import javax.tools.Diagnostic.Kind.WARNING
private val JSON_QUALIFIER = JsonQualifier::class.java
private val JSON = Json::class.asClassName()
private val TRANSIENT = Transient::class.asClassName()
private val VISIBILITY_MODIFIERS = setOf(
KModifier.INTERNAL,
KModifier.PRIVATE,
KModifier.PROTECTED,
KModifier.PUBLIC,
)
private fun Collection<KModifier>.visibility(): KModifier {
return find { it in VISIBILITY_MODIFIERS } ?: KModifier.PUBLIC
}
@KotlinPoetMetadataPreview
internal fun primaryConstructor(
targetElement: TypeElement,
kotlinApi: TypeSpec,
elements: Elements,
messager: Messager,
): TargetConstructor? {
val primaryConstructor = kotlinApi.primaryConstructor ?: return null
val parameters = LinkedHashMap<String, TargetParameter>()
for ((index, parameter) in primaryConstructor.parameters.withIndex()) {
val name = parameter.name
parameters[name] = TargetParameter(
name = name,
index = index,
type = parameter.type,
hasDefault = parameter.defaultValue != null,
qualifiers = parameter.annotations.qualifiers(messager, elements),
jsonName = parameter.annotations.jsonName(),
jsonIgnore = parameter.annotations.jsonIgnore(),
)
}
val kmConstructorSignature = primaryConstructor.tag<KmConstructor>()?.signature?.toString()
?: run {
messager.printMessage(
ERROR,
"No KmConstructor found for primary constructor.",
targetElement,
)
null
}
return TargetConstructor(
parameters,
primaryConstructor.modifiers.visibility(),
kmConstructorSignature,
)
}
/** Returns a target type for `element`, or null if it cannot be used with code gen. */
@OptIn(DelicateKotlinPoetApi::class)
@KotlinPoetMetadataPreview
internal fun targetType(
messager: Messager,
elements: Elements,
types: Types,
element: TypeElement,
cachedClassInspector: MoshiCachedClassInspector,
): TargetType? {
val typeMetadata = element.getAnnotation(Metadata::class.java)
if (typeMetadata == null) {
messager.printMessage(
ERROR,
"@JsonClass can't be applied to $element: must be a Kotlin class",
element,
)
return null
}
val kmClass = try {
cachedClassInspector.toKmClass(typeMetadata)
} catch (e: UnsupportedOperationException) {
messager.printMessage(
ERROR,
"@JsonClass can't be applied to $element: must be a Class type",
element,
)
return null
}
when {
kmClass.isEnum -> {
messager.printMessage(
ERROR,
"@JsonClass with 'generateAdapter = \"true\"' can't be applied to $element: code gen for enums is not supported or necessary",
element,
)
return null
}
!kmClass.isClass -> {
messager.printMessage(
ERROR,
"@JsonClass can't be applied to $element: must be a Kotlin class",
element,
)
return null
}
kmClass.isInner -> {
messager.printMessage(
ERROR,
"@JsonClass can't be applied to $element: must not be an inner class",
element,
)
return null
}
kmClass.flags.isSealed -> {
messager.printMessage(
ERROR,
"@JsonClass can't be applied to $element: must not be sealed",
element,
)
return null
}
kmClass.flags.isAbstract -> {
messager.printMessage(
ERROR,
"@JsonClass can't be applied to $element: must not be abstract",
element,
)
return null
}
kmClass.flags.isLocal -> {
messager.printMessage(
ERROR,
"@JsonClass can't be applied to $element: must not be local",
element,
)
return null
}
!kmClass.flags.isPublic && !kmClass.flags.isInternal -> {
messager.printMessage(
ERROR,
"@JsonClass can't be applied to $element: must be internal or public",
element,
)
return null
}
}
val kotlinApi = cachedClassInspector.toTypeSpec(kmClass)
val typeVariables = kotlinApi.typeVariables
val appliedType = AppliedType(element)
val constructor = primaryConstructor(element, kotlinApi, elements, messager)
if (constructor == null) {
messager.printMessage(
ERROR,
"No primary constructor found on $element",
element,
)
return null
}
if (constructor.visibility != KModifier.INTERNAL && constructor.visibility != KModifier.PUBLIC) {
messager.printMessage(
ERROR,
"@JsonClass can't be applied to $element: " +
"primary constructor is not internal or public",
element,
)
return null
}
val properties = mutableMapOf<String, TargetProperty>()
val resolvedTypes = mutableListOf<ResolvedTypeMapping>()
val superclass = appliedType.superclasses(types)
.onEach { superclass ->
if (superclass.element.getAnnotation(Metadata::class.java) == null) {
messager.printMessage(
ERROR,
"@JsonClass can't be applied to $element: supertype $superclass is not a Kotlin type",
element,
)
return null
}
}
.associateWithTo(LinkedHashMap()) { superclass ->
// Load the kotlin API cache into memory eagerly so we can reuse the parsed APIs
val api = if (superclass.element == element) {
// We've already parsed this api above, reuse it
kotlinApi
} else {
cachedClassInspector.toTypeSpec(superclass.element)
}
val apiSuperClass = api.superclass
if (apiSuperClass is ParameterizedTypeName) {
//
// This extends a typed generic superclass. We want to construct a mapping of the
// superclass typevar names to their materialized types here.
//
// class Foo extends Bar<String>
// class Bar<T>
//
// We will store {Foo : {T : [String]}}.
//
// Then when we look at Bar<T> later, we'll look up to the descendent Foo and extract its
// materialized type from there.
//
val superSuperClass = superclass.element.superclass as DeclaredType
// Convert to an element and back to wipe the typed generics off of this
val untyped = superSuperClass.asElement().asType().asTypeName() as ParameterizedTypeName
resolvedTypes += ResolvedTypeMapping(
target = untyped.rawType,
args = untyped.typeArguments.asSequence()
.cast<TypeVariableName>()
.map(TypeVariableName::name)
.zip(apiSuperClass.typeArguments.asSequence())
.associate { it },
)
}
return@associateWithTo api
}
for ((localAppliedType, supertypeApi) in superclass.entries) {
val appliedClassName = localAppliedType.element.asClassName()
val supertypeProperties = declaredProperties(
constructor = constructor,
kotlinApi = supertypeApi,
allowedTypeVars = typeVariables.toSet(),
currentClass = appliedClassName,
resolvedTypes = resolvedTypes,
)
for ((name, property) in supertypeProperties) {
properties.putIfAbsent(name, property)
}
}
val visibility = kotlinApi.modifiers.visibility()
// If any class in the enclosing class hierarchy is internal, they must all have internal
// generated adapters.
val resolvedVisibility = if (visibility == KModifier.INTERNAL) {
// Our nested type is already internal, no need to search
visibility
} else {
// Implicitly public, so now look up the hierarchy
val forceInternal = generateSequence<Element>(element) { it.enclosingElement }
.filterIsInstance<TypeElement>()
.map { cachedClassInspector.toKmClass(it.metadata) }
.any { it.flags.isInternal }
if (forceInternal) KModifier.INTERNAL else visibility
}
return TargetType(
typeName = element.asType().asTypeName(),
constructor = constructor,
properties = properties,
typeVariables = typeVariables,
isDataClass = KModifier.DATA in kotlinApi.modifiers,
visibility = resolvedVisibility,
)
}
/**
* Represents a resolved raw class to type arguments where [args] are a map of the parent type var
* name to its resolved [TypeName].
*/
private data class ResolvedTypeMapping(val target: ClassName, val args: Map<String, TypeName>)
private fun resolveTypeArgs(
targetClass: ClassName,
propertyType: TypeName,
resolvedTypes: List<ResolvedTypeMapping>,
allowedTypeVars: Set<TypeVariableName>,
entryStartIndex: Int = resolvedTypes.indexOfLast { it.target == targetClass },
): TypeName {
val unwrappedType = propertyType.unwrapTypeAlias()
if (unwrappedType !is TypeVariableName) {
return unwrappedType
} else if (entryStartIndex == -1) {
return unwrappedType
}
val targetMappingIndex = resolvedTypes[entryStartIndex]
val targetMappings = targetMappingIndex.args
// Try to resolve the real type of this property based on mapped generics in the subclass.
// We need to us a non-nullable version for mapping since we're just mapping based on raw java
// type vars, but then can re-copy nullability back if it is found.
val resolvedType = targetMappings[unwrappedType.name]
?.copy(nullable = unwrappedType.isNullable)
?: unwrappedType
return when {
resolvedType !is TypeVariableName -> resolvedType
entryStartIndex != 0 -> {
// We need to go deeper
resolveTypeArgs(targetClass, resolvedType, resolvedTypes, allowedTypeVars, entryStartIndex - 1)
}
resolvedType.copy(nullable = false) in allowedTypeVars -> {
// This is a generic type in the top-level declared class. This is fine to leave in because
// this will be handled by the `Type` array passed in at runtime.
resolvedType
}
else -> error("Could not find $resolvedType in $resolvedTypes. Also not present in allowable top-level type vars $allowedTypeVars")
}
}
/** Returns the properties declared by `typeElement`. */
@KotlinPoetMetadataPreview
private fun declaredProperties(
constructor: TargetConstructor,
kotlinApi: TypeSpec,
allowedTypeVars: Set<TypeVariableName>,
currentClass: ClassName,
resolvedTypes: List<ResolvedTypeMapping>,
): Map<String, TargetProperty> {
val result = mutableMapOf<String, TargetProperty>()
for (initialProperty in kotlinApi.propertySpecs) {
val resolvedType = resolveTypeArgs(
targetClass = currentClass,
propertyType = initialProperty.type,
resolvedTypes = resolvedTypes,
allowedTypeVars = allowedTypeVars,
)
val property = initialProperty.toBuilder(type = resolvedType).build()
val name = property.name
val parameter = constructor.parameters[name]
val isIgnored = property.annotations.any { it.typeName == TRANSIENT } ||
parameter?.jsonIgnore == true ||
property.annotations.jsonIgnore()
result[name] = TargetProperty(
propertySpec = property,
parameter = parameter,
visibility = property.modifiers.visibility(),
jsonName = parameter?.jsonName ?: property.annotations.jsonName() ?: name,
jsonIgnore = isIgnored,
)
}
return result
}
private val TargetProperty.isSettable get() = propertySpec.mutable || parameter != null
private val TargetProperty.isVisible: Boolean
get() {
return visibility == KModifier.INTERNAL ||
visibility == KModifier.PROTECTED ||
visibility == KModifier.PUBLIC
}
/**
* Returns a generator for this property, or null if either there is an error and this property
* cannot be used with code gen, or if no codegen is necessary for this property.
*/
internal fun TargetProperty.generator(
messager: Messager,
sourceElement: TypeElement,
elements: Elements,
): PropertyGenerator? {
if (jsonIgnore) {
if (!hasDefault) {
messager.printMessage(
ERROR,
"No default value for transient/ignored property $name",
sourceElement,
)
return null
}
return PropertyGenerator(this, DelegateKey(type, emptyList()), true)
}
if (!isVisible) {
messager.printMessage(
ERROR,
"property $name is not visible",
sourceElement,
)
return null
}
if (!isSettable) {
return null // This property is not settable. Ignore it.
}
// Merge parameter and property annotations
val qualifiers = parameter?.qualifiers.orEmpty() + propertySpec.annotations.qualifiers(messager, elements)
for (jsonQualifier in qualifiers) {
val qualifierRawType = jsonQualifier.typeName.rawType()
// Check Java types since that covers both Java and Kotlin annotations.
val annotationElement = elements.getTypeElement(qualifierRawType.canonicalName)
?: continue
annotationElement.getAnnotation(Retention::class.java)?.let {
if (it.value != RetentionPolicy.RUNTIME) {
messager.printMessage(
ERROR,
"JsonQualifier @${qualifierRawType.simpleName} must have RUNTIME retention",
)
}
}
}
val jsonQualifierSpecs = qualifiers.map {
it.toBuilder()
.useSiteTarget(AnnotationSpec.UseSiteTarget.FIELD)
.build()
}
return PropertyGenerator(
this,
DelegateKey(type, jsonQualifierSpecs),
)
}
private fun List<AnnotationSpec>?.qualifiers(
messager: Messager,
elements: Elements,
): Set<AnnotationSpec> {
if (this == null) return setOf()
return filterTo(mutableSetOf()) {
val typeElement: TypeElement? = elements.getTypeElement(it.typeName.rawType().canonicalName)
if (typeElement == null) {
messager.printMessage(WARNING, "Could not get the TypeElement of $it")
}
typeElement?.getAnnotation(JSON_QUALIFIER) != null
}
}
private fun List<AnnotationSpec>?.jsonName(): String? {
if (this == null) return null
return filter { it.typeName == JSON }.firstNotNullOfOrNull { annotation ->
annotation.jsonName()
}
}
private fun List<AnnotationSpec>?.jsonIgnore(): Boolean {
if (this == null) return false
return filter { it.typeName == JSON }.firstNotNullOfOrNull { annotation ->
annotation.jsonIgnore()
} ?: false
}
private fun AnnotationSpec.jsonName(): String? {
return elementValue<String>("name").takeUnless { it == Json.UNSET_NAME }
}
private fun AnnotationSpec.jsonIgnore(): Boolean {
return elementValue<Boolean>("ignore") ?: false
}
private fun <T> AnnotationSpec.elementValue(name: String): T? {
val mirror = requireNotNull(tag<AnnotationMirror>()) {
"Could not get the annotation mirror from the annotation spec"
}
@Suppress("UNCHECKED_CAST")
return mirror.elementValues.entries.firstOrNull {
it.key.simpleName.contentEquals(name)
}?.value?.value as? T
}
internal val TypeElement.metadata: Metadata
get() {
return getAnnotation(Metadata::class.java)
?: throw IllegalStateException("Not a kotlin type! $this")
}
private fun <E> Sequence<*>.cast(): Sequence<E> {
return map {
@Suppress("UNCHECKED_CAST")
it as E
}
}

View File

@@ -1 +0,0 @@
com.squareup.moshi.kotlin.codegen.apt.JsonClassCodegenProcessor,ISOLATING

View File

@@ -15,9 +15,9 @@
*/
package com.squareup.moshi.kotlin.codegen;
import com.squareup.moshi.kotlin.codegen.apt.JsonClassCodegenProcessorTest;
import com.squareup.moshi.kotlin.codegen.ksp.JsonClassSymbolProcessorTest;
/** For {@link JsonClassCodegenProcessorTest#extendJavaType}. */
/** For {@link JsonClassSymbolProcessorTest#extendJavaType}. */
public class JavaSuperclass {
public int a = 1;
}

View File

@@ -1,817 +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
*
* https://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.apt
import com.google.common.truth.Truth.assertThat
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonReader
import com.squareup.moshi.kotlin.codegen.api.Options.OPTION_GENERATED
import com.squareup.moshi.kotlin.codegen.api.Options.OPTION_GENERATE_PROGUARD_RULES
import com.tschuchort.compiletesting.JvmCompilationResult
import com.tschuchort.compiletesting.KotlinCompilation
import com.tschuchort.compiletesting.SourceFile
import com.tschuchort.compiletesting.SourceFile.Companion.kotlin
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import kotlin.reflect.KClass
import kotlin.reflect.KClassifier
import kotlin.reflect.KType
import kotlin.reflect.KTypeProjection
import kotlin.reflect.KVariance
import kotlin.reflect.KVariance.INVARIANT
import kotlin.reflect.full.createType
import kotlin.reflect.full.declaredMemberProperties
/** Execute kotlinc to confirm that either files are generated or errors are printed. */
class JsonClassCodegenProcessorTest {
@Rule @JvmField
val temporaryFolder: TemporaryFolder = TemporaryFolder()
@Test
fun privateConstructor() {
val result = compile(
kotlin(
"source.kt",
"""
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
class PrivateConstructor private constructor(val a: Int, val b: Int) {
fun a() = a
fun b() = b
companion object {
fun newInstance(a: Int, b: Int) = PrivateConstructor(a, b)
}
}
""",
),
)
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains("constructor is not internal or public")
}
@Test
fun privateConstructorParameter() {
val result = compile(
kotlin(
"source.kt",
"""
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
class PrivateConstructorParameter(private var a: Int)
""",
),
)
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains("property a is not visible")
}
@Test
fun privateProperties() {
val result = compile(
kotlin(
"source.kt",
"""
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
class PrivateProperties {
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() {
val result = compile(
kotlin(
"source.kt",
"""
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
interface Interface
""",
),
)
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",
)
}
@Test
fun interfacesDoNotErrorWhenGeneratorNotSet() {
val result = compile(
kotlin(
"source.kt",
"""
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true, generator="customGenerator")
interface Interface
""",
),
)
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",
)
}
@Test
fun sealedClassesNotSupported() {
val result = compile(
kotlin(
"source.kt",
"""
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
sealed class SealedClass(val a: Int)
""",
),
)
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains(
"error: @JsonClass can't be applied to SealedClass: must not be sealed",
)
}
@Test
fun innerClassesNotSupported() {
val result = compile(
kotlin(
"source.kt",
"""
import com.squareup.moshi.JsonClass
class Outer {
@JsonClass(generateAdapter = true)
inner class InnerClass(val a: Int)
}
""",
),
)
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains(
"error: @JsonClass can't be applied to Outer.InnerClass: must not be an inner class",
)
}
@Test
fun enumClassesNotSupported() {
val result = compile(
kotlin(
"source.kt",
"""
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
enum class KotlinEnum {
A, B
}
""",
),
)
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains(
"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
@Ignore
@Test
fun localClassesNotSupported() {
val result = compile(
kotlin(
"source.kt",
"""
import com.squareup.moshi.JsonClass
fun outer() {
@JsonClass(generateAdapter = true)
class LocalClass(val a: Int)
}
""",
),
)
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains(
"error: @JsonClass can't be applied to LocalClass: must not be local",
)
}
@Test
fun privateClassesNotSupported() {
val result = compile(
kotlin(
"source.kt",
"""
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
private class PrivateClass(val a: Int)
""",
),
)
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains(
"error: @JsonClass can't be applied to PrivateClass: must be internal or public",
)
}
@Test
fun objectDeclarationsNotSupported() {
val result = compile(
kotlin(
"source.kt",
"""
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
object ObjectDeclaration {
var a = 5
}
""",
),
)
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains(
"error: @JsonClass can't be applied to ObjectDeclaration: must be a Kotlin class",
)
}
@Test
fun objectExpressionsNotSupported() {
val result = compile(
kotlin(
"source.kt",
"""
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
val expression = object : Any() {
var a = 5
}
""",
),
)
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains(
"error: @JsonClass can't be applied to getExpression\$annotations(): must be a Kotlin class",
)
}
@Test
fun requiredTransientConstructorParameterFails() {
val result = compile(
kotlin(
"source.kt",
"""
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
class RequiredTransientConstructorParameter(@Transient var a: Int)
""",
),
)
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains(
"error: No default value for transient/ignored property a",
)
}
@Test
fun requiredIgnoredConstructorParameterFails() {
val result = compile(
kotlin(
"source.kt",
"""
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
class RequiredIgnoredConstructorParameter(@Json(ignore = true) var a: Int)
""",
),
)
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains(
"error: No default value for transient/ignored property a",
)
}
@Test
fun nonPropertyConstructorParameter() {
val result = compile(
kotlin(
"source.kt",
"""
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
class NonPropertyConstructorParameter(a: Int, val b: Int)
""",
),
)
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains(
"error: No property for required constructor parameter a",
)
}
@Test
fun badGeneratedAnnotation() {
val result = prepareCompilation(
kotlin(
"source.kt",
"""
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class Foo(val a: Int)
""",
),
).apply {
kaptArgs[OPTION_GENERATED] = "javax.annotation.GeneratedBlerg"
}.compile()
assertThat(result.messages).contains(
"Invalid option value for $OPTION_GENERATED",
)
}
@Test
fun disableProguardRulesGenerating() {
val result = prepareCompilation(
kotlin(
"source.kt",
"""
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class Foo(val a: Int)
""",
),
).apply {
kaptArgs[OPTION_GENERATE_PROGUARD_RULES] = "false"
}.compile()
assertThat(result.generatedFiles.filter { it.endsWith(".pro") }).isEmpty()
}
@Test
fun multipleErrors() {
val result = compile(
kotlin(
"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)
""",
),
)
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() {
val result = compile(
kotlin(
"source.kt",
"""
import com.squareup.moshi.JsonClass
import java.util.Date
@JsonClass(generateAdapter = true)
class ExtendsPlatformClass(var a: Int) : Date()
""",
),
)
assertThat(result.messages).contains("supertype java.util.Date is not a Kotlin type")
}
@Test
fun extendJavaType() {
val result = compile(
kotlin(
"source.kt",
"""
import com.squareup.moshi.JsonClass
import com.squareup.moshi.kotlin.codegen.JavaSuperclass
@JsonClass(generateAdapter = true)
class ExtendsJavaType(var b: Int) : JavaSuperclass()
""",
),
)
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages)
.contains("supertype com.squareup.moshi.kotlin.codegen.JavaSuperclass is not a Kotlin type")
}
@Test
fun nonFieldApplicableQualifier() {
val result = compile(
kotlin(
"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)
""",
),
)
// We instantiate directly so doesn't need to be FIELD
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK)
}
@Test
fun nonRuntimeQualifier() {
val result = compile(
kotlin(
"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)
""",
),
)
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains("JsonQualifier @UpperCase must have RUNTIME retention")
}
@Test
fun `TypeAliases with the same backing type should share the same adapter`() {
val result = compile(
kotlin(
"source.kt",
"""
import com.squareup.moshi.JsonClass
typealias FirstName = String
typealias LastName = String
@JsonClass(generateAdapter = true)
data class Person(val firstName: FirstName, val lastName: LastName, val hairColor: String)
""",
),
)
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK)
// We're checking here that we only generate one `stringAdapter` that's used for both the
// regular string properties as well as the the aliased ones.
val adapterClass = result.classLoader.loadClass("PersonJsonAdapter").kotlin
assertThat(adapterClass.declaredMemberProperties.map { it.returnType }).containsExactly(
JsonReader.Options::class.createType(),
JsonAdapter::class.parameterizedBy(String::class),
)
}
@Test
fun `Processor should generate comprehensive proguard rules`() {
val result = compile(
kotlin(
"source.kt",
"""
package testPackage
import com.squareup.moshi.JsonClass
import com.squareup.moshi.JsonQualifier
typealias FirstName = String
typealias LastName = String
@JsonClass(generateAdapter = true)
data class Aliases(val firstName: FirstName, val lastName: LastName, val hairColor: String)
@JsonClass(generateAdapter = true)
data class Simple(val firstName: String)
@JsonClass(generateAdapter = true)
data class Generic<T>(val firstName: T, val lastName: String)
@JsonQualifier
annotation class MyQualifier
@JsonClass(generateAdapter = true)
data class UsingQualifiers(val firstName: String, @MyQualifier val lastName: String)
@JsonClass(generateAdapter = true)
data class MixedTypes(val firstName: String, val otherNames: MutableList<String>)
@JsonClass(generateAdapter = true)
data class DefaultParams(val firstName: String = "")
@JsonClass(generateAdapter = true)
data class Complex<T>(val firstName: FirstName = "", @MyQualifier val names: MutableList<String>, val genericProp: T)
object NestedType {
@JsonQualifier
annotation class NestedQualifier
@JsonClass(generateAdapter = true)
data class NestedSimple(@NestedQualifier val firstName: String)
}
@JsonClass(generateAdapter = true)
class MultipleMasks(
val arg0: Long = 0,
val arg1: Long = 1,
val arg2: Long = 2,
val arg3: Long = 3,
val arg4: Long = 4,
val arg5: Long = 5,
val arg6: Long = 6,
val arg7: Long = 7,
val arg8: Long = 8,
val arg9: Long = 9,
val arg10: Long = 10,
val arg11: Long,
val arg12: Long = 12,
val arg13: Long = 13,
val arg14: Long = 14,
val arg15: Long = 15,
val arg16: Long = 16,
val arg17: Long = 17,
val arg18: Long = 18,
val arg19: Long = 19,
@Suppress("UNUSED_PARAMETER") arg20: Long = 20,
val arg21: Long = 21,
val arg22: Long = 22,
val arg23: Long = 23,
val arg24: Long = 24,
val arg25: Long = 25,
val arg26: Long = 26,
val arg27: Long = 27,
val arg28: Long = 28,
val arg29: Long = 29,
val arg30: Long = 30,
val arg31: Long = 31,
val arg32: Long = 32,
val arg33: Long = 33,
val arg34: Long = 34,
val arg35: Long = 35,
val arg36: Long = 36,
val arg37: Long = 37,
val arg38: Long = 38,
@Transient val arg39: Long = 39,
val arg40: Long = 40,
val arg41: Long = 41,
val arg42: Long = 42,
val arg43: Long = 43,
val arg44: Long = 44,
val arg45: Long = 45,
val arg46: Long = 46,
val arg47: Long = 47,
val arg48: Long = 48,
val arg49: Long = 49,
val arg50: Long = 50,
val arg51: Long = 51,
val arg52: Long = 52,
@Transient val arg53: Long = 53,
val arg54: Long = 54,
val arg55: Long = 55,
val arg56: Long = 56,
val arg57: Long = 57,
val arg58: Long = 58,
val arg59: Long = 59,
val arg60: Long = 60,
val arg61: Long = 61,
val arg62: Long = 62,
val arg63: Long = 63,
val arg64: Long = 64,
val arg65: Long = 65
)
""",
),
)
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK)
result.generatedFiles.filter { it.extension == "pro" }.forEach { generatedFile ->
when (generatedFile.nameWithoutExtension) {
"moshi-testPackage.Aliases" -> assertThat(generatedFile.readText()).contains(
"""
-if class testPackage.Aliases
-keepnames class testPackage.Aliases
-if class testPackage.Aliases
-keep class testPackage.AliasesJsonAdapter {
public <init>(com.squareup.moshi.Moshi);
}
""".trimIndent(),
)
"moshi-testPackage.Simple" -> assertThat(generatedFile.readText()).contains(
"""
-if class testPackage.Simple
-keepnames class testPackage.Simple
-if class testPackage.Simple
-keep class testPackage.SimpleJsonAdapter {
public <init>(com.squareup.moshi.Moshi);
}
""".trimIndent(),
)
"moshi-testPackage.Generic" -> assertThat(generatedFile.readText()).contains(
"""
-if class testPackage.Generic
-keepnames class testPackage.Generic
-if class testPackage.Generic
-keep class testPackage.GenericJsonAdapter {
public <init>(com.squareup.moshi.Moshi,java.lang.reflect.Type[]);
}
""".trimIndent(),
)
"moshi-testPackage.UsingQualifiers" -> {
assertThat(generatedFile.readText()).contains(
"""
-if class testPackage.UsingQualifiers
-keepnames class testPackage.UsingQualifiers
-if class testPackage.UsingQualifiers
-keep class testPackage.UsingQualifiersJsonAdapter {
public <init>(com.squareup.moshi.Moshi);
}
""".trimIndent(),
)
}
"moshi-testPackage.MixedTypes" -> assertThat(generatedFile.readText()).contains(
"""
-if class testPackage.MixedTypes
-keepnames class testPackage.MixedTypes
-if class testPackage.MixedTypes
-keep class testPackage.MixedTypesJsonAdapter {
public <init>(com.squareup.moshi.Moshi);
}
""".trimIndent(),
)
"moshi-testPackage.DefaultParams" -> assertThat(generatedFile.readText()).contains(
"""
-if class testPackage.DefaultParams
-keepnames class testPackage.DefaultParams
-if class testPackage.DefaultParams
-keep class testPackage.DefaultParamsJsonAdapter {
public <init>(com.squareup.moshi.Moshi);
}
-if class testPackage.DefaultParams
-keepnames class kotlin.jvm.internal.DefaultConstructorMarker
-if class testPackage.DefaultParams
-keepclassmembers class testPackage.DefaultParams {
public synthetic <init>(java.lang.String,int,kotlin.jvm.internal.DefaultConstructorMarker);
}
""".trimIndent(),
)
"moshi-testPackage.Complex" -> {
assertThat(generatedFile.readText()).contains(
"""
-if class testPackage.Complex
-keepnames class testPackage.Complex
-if class testPackage.Complex
-keep class testPackage.ComplexJsonAdapter {
public <init>(com.squareup.moshi.Moshi,java.lang.reflect.Type[]);
}
-if class testPackage.Complex
-keepnames class kotlin.jvm.internal.DefaultConstructorMarker
-if class testPackage.Complex
-keepclassmembers class testPackage.Complex {
public synthetic <init>(java.lang.String,java.util.List,java.lang.Object,int,kotlin.jvm.internal.DefaultConstructorMarker);
}
""".trimIndent(),
)
}
"moshi-testPackage.MultipleMasks" -> assertThat(generatedFile.readText()).contains(
"""
-if class testPackage.MultipleMasks
-keepnames class testPackage.MultipleMasks
-if class testPackage.MultipleMasks
-keep class testPackage.MultipleMasksJsonAdapter {
public <init>(com.squareup.moshi.Moshi);
}
-if class testPackage.MultipleMasks
-keepnames class kotlin.jvm.internal.DefaultConstructorMarker
-if class testPackage.MultipleMasks
-keepclassmembers class testPackage.MultipleMasks {
public synthetic <init>(long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,int,int,int,kotlin.jvm.internal.DefaultConstructorMarker);
}
""".trimIndent(),
)
"moshi-testPackage.NestedType.NestedSimple" -> {
assertThat(generatedFile.readText()).contains(
"""
-if class testPackage.NestedType${'$'}NestedSimple
-keepnames class testPackage.NestedType${'$'}NestedSimple
-if class testPackage.NestedType${'$'}NestedSimple
-keep class testPackage.NestedType_NestedSimpleJsonAdapter {
public <init>(com.squareup.moshi.Moshi);
}
""".trimIndent(),
)
}
else -> error("Unexpected proguard file! ${generatedFile.name}")
}
}
}
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): JvmCompilationResult {
return prepareCompilation(*sourceFiles).compile()
}
private fun KClassifier.parameterizedBy(vararg types: KClass<*>): KType {
return parameterizedBy(*types.map { it.createType() }.toTypedArray())
}
private fun KClassifier.parameterizedBy(vararg types: KType): KType {
return createType(
types.map { it.asProjection() },
)
}
private fun KType.asProjection(variance: KVariance? = INVARIANT): KTypeProjection {
return KTypeProjection(variance, this)
}
}

View File

@@ -1,17 +1,14 @@
import Build_gradle.TestMode.KAPT
import Build_gradle.TestMode.KSP
import Build_gradle.TestMode.REFLECT
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm")
kotlin("kapt") apply false
id("com.google.devtools.ksp") apply false
}
enum class TestMode {
REFLECT,
KAPT,
KSP,
}
@@ -24,11 +21,6 @@ when (testMode) {
REFLECT -> {
// Do nothing!
}
KAPT -> {
apply(plugin = "org.jetbrains.kotlin.kapt")
}
KSP -> {
apply(plugin = "com.google.devtools.ksp")
}
@@ -53,11 +45,6 @@ dependencies {
REFLECT -> {
// Do nothing
}
KAPT -> {
"kaptTest"(project(":moshi-kotlin-codegen"))
}
KSP -> {
"kspTest"(project(":moshi-kotlin-codegen"))
}

View File

@@ -1,17 +1,14 @@
import Build_gradle.TestMode.KAPT
import Build_gradle.TestMode.KSP
import Build_gradle.TestMode.REFLECT
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm")
kotlin("kapt") apply false
id("com.google.devtools.ksp") apply false
}
enum class TestMode {
REFLECT,
KAPT,
KSP,
}
@@ -25,11 +22,6 @@ when (testMode) {
// Default to KSP. This is a CI-only thing
apply(plugin = "com.google.devtools.ksp")
}
KAPT -> {
apply(plugin = "org.jetbrains.kotlin.kapt")
}
KSP -> {
apply(plugin = "com.google.devtools.ksp")
}
@@ -55,11 +47,6 @@ dependencies {
// Default to KSP in this case, this is a CI-only thing
"kspTest"(project(":moshi-kotlin-codegen"))
}
KAPT -> {
"kaptTest"(project(":moshi-kotlin-codegen"))
}
KSP -> {
"kspTest"(project(":moshi-kotlin-codegen"))
}