mirror of
https://github.com/fankes/moshi.git
synced 2025-10-18 23:49:21 +08:00
Change the directory structure to match our modules (#1451)
This commit is contained in:
113
moshi-kotlin-codegen/build.gradle.kts
Normal file
113
moshi-kotlin-codegen/build.gradle.kts
Normal file
@@ -0,0 +1,113 @@
|
||||
import com.github.jengelman.gradle.plugins.shadow.tasks.ConfigureShadowRelocation
|
||||
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
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
id("com.google.devtools.ksp")
|
||||
id("com.vanniktech.maven.publish.base")
|
||||
alias(libs.plugins.mavenShadow)
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile>().configureEach {
|
||||
kotlinOptions {
|
||||
@Suppress("SuspiciousCollectionReassignment")
|
||||
freeCompilerArgs += listOf(
|
||||
"-Xopt-in=kotlin.RequiresOptIn",
|
||||
"-Xopt-in=com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview",
|
||||
"-Xopt-in=com.squareup.kotlinpoet.ksp.KotlinPoetKspPreview",
|
||||
"-Xopt-in=com.squareup.moshi.kotlin.codegen.api.InternalMoshiCodegenApi",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// --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"))
|
||||
implementation(kotlin("reflect"))
|
||||
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")
|
||||
}
|
||||
shade(libs.kotlinpoet.ksp) {
|
||||
exclude(group = "org.jetbrains.kotlin")
|
||||
exclude(group = "com.squareup", module = "kotlinpoet")
|
||||
}
|
||||
implementation(libs.guava)
|
||||
implementation(libs.asm)
|
||||
|
||||
implementation(libs.autoService)
|
||||
ksp(libs.autoService.ksp)
|
||||
|
||||
// KSP deps
|
||||
compileOnly(libs.ksp)
|
||||
compileOnly(libs.ksp.api)
|
||||
compileOnly(libs.kotlin.compilerEmbeddable)
|
||||
// Always force the latest KSP version to match the one we're compiling against
|
||||
testImplementation(libs.ksp)
|
||||
testImplementation(libs.ksp.api)
|
||||
testImplementation(libs.kotlin.compilerEmbeddable)
|
||||
testImplementation(libs.kotlinCompileTesting.ksp)
|
||||
|
||||
// Copy these again as they're not automatically included since they're shaded
|
||||
testImplementation(project(":moshi"))
|
||||
testImplementation(kotlin("reflect"))
|
||||
testImplementation(libs.kotlinpoet.metadata)
|
||||
testImplementation(libs.kotlinpoet.ksp)
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(libs.truth)
|
||||
testImplementation(libs.kotlinCompileTesting)
|
||||
}
|
||||
|
||||
val relocateShadowJar = tasks.register<ConfigureShadowRelocation>("relocateShadowJar") {
|
||||
target = tasks.shadowJar.get()
|
||||
}
|
||||
|
||||
val shadowJar = tasks.shadowJar.apply {
|
||||
configure {
|
||||
dependsOn(relocateShadowJar)
|
||||
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()))
|
||||
}
|
@@ -0,0 +1,770 @@
|
||||
/*
|
||||
* 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.api
|
||||
|
||||
import com.squareup.kotlinpoet.ARRAY
|
||||
import com.squareup.kotlinpoet.AnnotationSpec
|
||||
import com.squareup.kotlinpoet.AnnotationSpec.UseSiteTarget.FILE
|
||||
import com.squareup.kotlinpoet.CodeBlock
|
||||
import com.squareup.kotlinpoet.FileSpec
|
||||
import com.squareup.kotlinpoet.FunSpec
|
||||
import com.squareup.kotlinpoet.INT
|
||||
import com.squareup.kotlinpoet.KModifier
|
||||
import com.squareup.kotlinpoet.MemberName
|
||||
import com.squareup.kotlinpoet.NameAllocator
|
||||
import com.squareup.kotlinpoet.ParameterSpec
|
||||
import com.squareup.kotlinpoet.ParameterizedTypeName
|
||||
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
|
||||
import com.squareup.kotlinpoet.PropertySpec
|
||||
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.joinToCode
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.JsonWriter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.internal.Util
|
||||
import com.squareup.moshi.kotlin.codegen.api.FromJsonComponent.ParameterOnly
|
||||
import com.squareup.moshi.kotlin.codegen.api.FromJsonComponent.ParameterProperty
|
||||
import com.squareup.moshi.kotlin.codegen.api.FromJsonComponent.PropertyOnly
|
||||
import java.lang.reflect.Constructor
|
||||
import java.lang.reflect.Type
|
||||
import org.objectweb.asm.Type as AsmType
|
||||
|
||||
private val MOSHI_UTIL = Util::class.asClassName()
|
||||
private const val TO_STRING_PREFIX = "GeneratedJsonAdapter("
|
||||
private const val TO_STRING_SIZE_BASE = TO_STRING_PREFIX.length + 1 // 1 is the closing paren
|
||||
|
||||
/** Generates a JSON adapter for a target type. */
|
||||
@InternalMoshiCodegenApi
|
||||
public class AdapterGenerator(
|
||||
private val target: TargetType,
|
||||
private val propertyList: List<PropertyGenerator>
|
||||
) {
|
||||
|
||||
private companion object {
|
||||
private val INT_TYPE_BLOCK = CodeBlock.of("%T::class.javaPrimitiveType", INT)
|
||||
private val DEFAULT_CONSTRUCTOR_MARKER_TYPE_BLOCK = CodeBlock.of(
|
||||
"%T.DEFAULT_CONSTRUCTOR_MARKER",
|
||||
Util::class
|
||||
)
|
||||
private val CN_MOSHI = Moshi::class.asClassName()
|
||||
private val CN_TYPE = Type::class.asClassName()
|
||||
|
||||
private val COMMON_SUPPRESS = arrayOf(
|
||||
// https://github.com/square/moshi/issues/1023
|
||||
"DEPRECATION",
|
||||
// Because we look it up reflectively
|
||||
"unused",
|
||||
// Because we include underscores
|
||||
"ClassName",
|
||||
// Because we generate redundant `out` variance for some generics and there's no way
|
||||
// for us to know when it's redundant.
|
||||
"REDUNDANT_PROJECTION",
|
||||
// Because we may generate redundant explicit types for local vars with default values.
|
||||
// Example: 'var fooSet: Boolean = false'
|
||||
"RedundantExplicitType",
|
||||
// NameAllocator will just add underscores to differentiate names, which Kotlin doesn't
|
||||
// like for stylistic reasons.
|
||||
"LocalVariableName",
|
||||
// KotlinPoet always generates explicit public modifiers for public members.
|
||||
"RedundantVisibilityModifier",
|
||||
// For LambdaTypeNames we have to import kotlin.functions.* types
|
||||
"PLATFORM_CLASS_MAPPED_TO_KOTLIN",
|
||||
// Cover for calling fromJson() on a Nothing property type. Theoretically nonsensical but we
|
||||
// support it
|
||||
"IMPLICIT_NOTHING_TYPE_ARGUMENT_IN_RETURN_POSITION"
|
||||
).let { suppressions ->
|
||||
AnnotationSpec.builder(Suppress::class)
|
||||
.useSiteTarget(FILE)
|
||||
.addMember(
|
||||
suppressions.indices.joinToString { "%S" },
|
||||
*suppressions
|
||||
)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
private val nonTransientProperties = propertyList.filterNot { it.isTransient }
|
||||
private val className = target.typeName.rawType()
|
||||
private val visibility = target.visibility
|
||||
private val typeVariables = target.typeVariables
|
||||
private val typeVariableResolver = typeVariables.toTypeVariableResolver()
|
||||
private val targetConstructorParams = target.constructor.parameters
|
||||
.mapKeys { (_, param) -> param.index }
|
||||
|
||||
private val nameAllocator = NameAllocator()
|
||||
private val adapterName = "${className.simpleNames.joinToString(separator = "_")}JsonAdapter"
|
||||
private val originalTypeName = target.typeName.stripTypeVarVariance(typeVariableResolver)
|
||||
private val originalRawTypeName = originalTypeName.rawType()
|
||||
|
||||
private val moshiParam = ParameterSpec.builder(
|
||||
nameAllocator.newName("moshi"),
|
||||
CN_MOSHI
|
||||
).build()
|
||||
private val typesParam = ParameterSpec.builder(
|
||||
nameAllocator.newName("types"),
|
||||
ARRAY.parameterizedBy(CN_TYPE)
|
||||
)
|
||||
.build()
|
||||
private val readerParam = ParameterSpec.builder(
|
||||
nameAllocator.newName("reader"),
|
||||
JsonReader::class
|
||||
)
|
||||
.build()
|
||||
private val writerParam = ParameterSpec.builder(
|
||||
nameAllocator.newName("writer"),
|
||||
JsonWriter::class
|
||||
)
|
||||
.build()
|
||||
private val valueParam = ParameterSpec.builder(
|
||||
nameAllocator.newName("value"),
|
||||
originalTypeName.copy(nullable = true)
|
||||
)
|
||||
.build()
|
||||
private val jsonAdapterTypeName = JsonAdapter::class.asClassName().parameterizedBy(
|
||||
originalTypeName
|
||||
)
|
||||
|
||||
// selectName() API setup
|
||||
private val optionsProperty = PropertySpec.builder(
|
||||
nameAllocator.newName("options"),
|
||||
JsonReader.Options::class.asTypeName(),
|
||||
KModifier.PRIVATE
|
||||
)
|
||||
.initializer(
|
||||
"%T.of(%L)",
|
||||
JsonReader.Options::class.asTypeName(),
|
||||
nonTransientProperties
|
||||
.map { CodeBlock.of("%S", it.jsonName) }
|
||||
.joinToCode(", ")
|
||||
)
|
||||
.build()
|
||||
|
||||
private val constructorProperty = PropertySpec.builder(
|
||||
nameAllocator.newName("constructorRef"),
|
||||
Constructor::class.asClassName().parameterizedBy(originalTypeName).copy(nullable = true),
|
||||
KModifier.PRIVATE
|
||||
)
|
||||
.addAnnotation(Volatile::class)
|
||||
.mutable(true)
|
||||
.initializer("null")
|
||||
.build()
|
||||
|
||||
public fun prepare(generateProguardRules: Boolean, typeHook: (TypeSpec) -> TypeSpec = { it }): PreparedAdapter {
|
||||
val reservedSimpleNames = mutableSetOf<String>()
|
||||
for (property in nonTransientProperties) {
|
||||
// Allocate names for simple property types first to avoid collisions
|
||||
// See https://github.com/square/moshi/issues/1277
|
||||
property.target.type.findRawType()?.simpleName?.let { simpleNameToReserve ->
|
||||
if (reservedSimpleNames.add(simpleNameToReserve)) {
|
||||
nameAllocator.newName(simpleNameToReserve)
|
||||
}
|
||||
}
|
||||
property.allocateNames(nameAllocator)
|
||||
}
|
||||
|
||||
val generatedAdapter = generateType().let(typeHook)
|
||||
val result = FileSpec.builder(className.packageName, adapterName)
|
||||
result.addComment("Code generated by moshi-kotlin-codegen. Do not edit.")
|
||||
result.addAnnotation(COMMON_SUPPRESS)
|
||||
result.addType(generatedAdapter)
|
||||
val proguardConfig = if (generateProguardRules) {
|
||||
generatedAdapter.createProguardRule()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
return PreparedAdapter(result.build(), proguardConfig)
|
||||
}
|
||||
|
||||
private fun TypeSpec.createProguardRule(): ProguardConfig {
|
||||
val adapterConstructorParams = when (requireNotNull(primaryConstructor).parameters.size) {
|
||||
1 -> listOf(CN_MOSHI.reflectionName())
|
||||
2 -> listOf(CN_MOSHI.reflectionName(), "${CN_TYPE.reflectionName()}[]")
|
||||
// Should never happen
|
||||
else -> error("Unexpected number of arguments on primary constructor: $primaryConstructor")
|
||||
}
|
||||
|
||||
var hasDefaultProperties = false
|
||||
var parameterTypes = emptyList<String>()
|
||||
target.constructor.signature?.let { constructorSignature ->
|
||||
if (constructorSignature.startsWith("constructor-impl")) {
|
||||
// Inline class, we don't support this yet.
|
||||
// This is a static method with signature like 'constructor-impl(I)I'
|
||||
return@let
|
||||
}
|
||||
hasDefaultProperties = propertyList.any { it.hasDefault }
|
||||
parameterTypes = AsmType.getArgumentTypes(constructorSignature.removePrefix("<init>"))
|
||||
.map { it.toReflectionString() }
|
||||
}
|
||||
return ProguardConfig(
|
||||
targetClass = className,
|
||||
adapterName = adapterName,
|
||||
adapterConstructorParams = adapterConstructorParams,
|
||||
targetConstructorHasDefaults = hasDefaultProperties,
|
||||
targetConstructorParams = parameterTypes,
|
||||
)
|
||||
}
|
||||
|
||||
private fun generateType(): TypeSpec {
|
||||
val result = TypeSpec.classBuilder(adapterName)
|
||||
|
||||
result.superclass(jsonAdapterTypeName)
|
||||
|
||||
if (typeVariables.isNotEmpty()) {
|
||||
result.addTypeVariables(typeVariables.map { it.stripTypeVarVariance(typeVariableResolver) as TypeVariableName })
|
||||
// require(types.size == 1) {
|
||||
// "TypeVariable mismatch: Expecting 1 type(s) for generic type variables [T], but received ${types.size} with values $types"
|
||||
// }
|
||||
result.addInitializerBlock(
|
||||
CodeBlock.builder()
|
||||
.beginControlFlow("require(types.size == %L)", typeVariables.size)
|
||||
.addStatement(
|
||||
"buildString·{·append(%S).append(%L).append(%S).append(%S).append(%S).append(%L)·}",
|
||||
"TypeVariable mismatch: Expecting ",
|
||||
typeVariables.size,
|
||||
" ${if (typeVariables.size == 1) "type" else "types"} for generic type variables [",
|
||||
typeVariables.joinToString(", ") { it.name },
|
||||
"], but received ",
|
||||
"${typesParam.name}.size"
|
||||
)
|
||||
.endControlFlow()
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
// TODO make this configurable. Right now it just matches the source model
|
||||
if (visibility == KModifier.INTERNAL) {
|
||||
result.addModifiers(KModifier.INTERNAL)
|
||||
}
|
||||
|
||||
result.primaryConstructor(generateConstructor())
|
||||
|
||||
val typeRenderer: TypeRenderer = object : TypeRenderer() {
|
||||
override fun renderTypeVariable(typeVariable: TypeVariableName): CodeBlock {
|
||||
val index = typeVariables.indexOfFirst { it == typeVariable }
|
||||
check(index != -1) { "Unexpected type variable $typeVariable" }
|
||||
return CodeBlock.of("%N[%L]", typesParam, index)
|
||||
}
|
||||
}
|
||||
|
||||
result.addProperty(optionsProperty)
|
||||
for (uniqueAdapter in nonTransientProperties.distinctBy { it.delegateKey }) {
|
||||
result.addProperty(
|
||||
uniqueAdapter.delegateKey.generateProperty(
|
||||
nameAllocator,
|
||||
typeRenderer,
|
||||
moshiParam,
|
||||
uniqueAdapter.name
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
result.addFunction(generateToStringFun())
|
||||
result.addFunction(generateFromJsonFun(result))
|
||||
result.addFunction(generateToJsonFun())
|
||||
|
||||
return result.build()
|
||||
}
|
||||
|
||||
private fun generateConstructor(): FunSpec {
|
||||
val result = FunSpec.constructorBuilder()
|
||||
result.addParameter(moshiParam)
|
||||
|
||||
if (typeVariables.isNotEmpty()) {
|
||||
result.addParameter(typesParam)
|
||||
}
|
||||
|
||||
return result.build()
|
||||
}
|
||||
|
||||
private fun generateToStringFun(): FunSpec {
|
||||
val name = originalRawTypeName.simpleNames.joinToString(".")
|
||||
val size = TO_STRING_SIZE_BASE + name.length
|
||||
return FunSpec.builder("toString")
|
||||
.addModifiers(KModifier.OVERRIDE)
|
||||
.returns(String::class)
|
||||
.addStatement(
|
||||
"return %M(%L)·{ append(%S).append(%S).append('%L') }",
|
||||
MemberName("kotlin.text", "buildString"),
|
||||
size,
|
||||
TO_STRING_PREFIX,
|
||||
name,
|
||||
")"
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun generateFromJsonFun(classBuilder: TypeSpec.Builder): FunSpec {
|
||||
val result = FunSpec.builder("fromJson")
|
||||
.addModifiers(KModifier.OVERRIDE)
|
||||
.addParameter(readerParam)
|
||||
.returns(originalTypeName)
|
||||
|
||||
for (property in nonTransientProperties) {
|
||||
result.addCode("%L", property.generateLocalProperty())
|
||||
if (property.hasLocalIsPresentName) {
|
||||
result.addCode("%L", property.generateLocalIsPresentProperty())
|
||||
}
|
||||
}
|
||||
|
||||
val propertiesByIndex = propertyList.asSequence()
|
||||
.filter { it.hasConstructorParameter }
|
||||
.associateBy { it.target.parameterIndex }
|
||||
val components = mutableListOf<FromJsonComponent>()
|
||||
|
||||
// Add parameters (± properties) first, their index matters
|
||||
for ((index, parameter) in targetConstructorParams) {
|
||||
val property = propertiesByIndex[index]
|
||||
if (property == null) {
|
||||
components += ParameterOnly(parameter)
|
||||
} else {
|
||||
components += ParameterProperty(parameter, property)
|
||||
}
|
||||
}
|
||||
|
||||
// Now add the remaining properties that aren't parameters
|
||||
for (property in propertyList) {
|
||||
if (property.target.parameterIndex in targetConstructorParams) {
|
||||
continue // Already handled
|
||||
}
|
||||
if (property.isTransient) {
|
||||
continue // We don't care about these outside of constructor parameters
|
||||
}
|
||||
components += PropertyOnly(property)
|
||||
}
|
||||
|
||||
// Calculate how many masks we'll need. Round up if it's not evenly divisible by 32
|
||||
val propertyCount = targetConstructorParams.size
|
||||
val maskCount = if (propertyCount == 0) {
|
||||
0
|
||||
} else {
|
||||
(propertyCount + 31) / 32
|
||||
}
|
||||
// Allocate mask names
|
||||
val maskNames = Array(maskCount) { index ->
|
||||
nameAllocator.newName("mask$index")
|
||||
}
|
||||
val maskAllSetValues = Array(maskCount) { -1 }
|
||||
val useDefaultsConstructor = components.filterIsInstance<ParameterComponent>()
|
||||
.any { it.parameter.hasDefault }
|
||||
if (useDefaultsConstructor) {
|
||||
// Initialize all our masks, defaulting to fully unset (-1)
|
||||
for (maskName in maskNames) {
|
||||
result.addStatement("var %L = -1", maskName)
|
||||
}
|
||||
}
|
||||
result.addStatement("%N.beginObject()", readerParam)
|
||||
result.beginControlFlow("while (%N.hasNext())", readerParam)
|
||||
result.beginControlFlow("when (%N.selectName(%N))", readerParam, optionsProperty)
|
||||
|
||||
// We track property index and mask index separately, because mask index is based on _all_
|
||||
// constructor arguments, while property index is only based on the index passed into
|
||||
// JsonReader.Options.
|
||||
var propertyIndex = 0
|
||||
val constructorPropertyTypes = mutableListOf<CodeBlock>()
|
||||
|
||||
//
|
||||
// Track important indices for masks. Masks generally increment with each parameter (including
|
||||
// transient).
|
||||
//
|
||||
// Mask name index is an index into the maskNames array we initialized above.
|
||||
//
|
||||
// Once the maskIndex reaches 32, we've filled up that mask and have to move to the next mask
|
||||
// name. Reset the maskIndex relative to here and continue incrementing.
|
||||
//
|
||||
var maskIndex = 0
|
||||
var maskNameIndex = 0
|
||||
val updateMaskIndexes = {
|
||||
maskIndex++
|
||||
if (maskIndex == 32) {
|
||||
// Move to the next mask
|
||||
maskIndex = 0
|
||||
maskNameIndex++
|
||||
}
|
||||
}
|
||||
|
||||
for (input in components) {
|
||||
if (input is ParameterOnly ||
|
||||
(input is ParameterProperty && input.property.isTransient)
|
||||
) {
|
||||
updateMaskIndexes()
|
||||
constructorPropertyTypes += input.type.asTypeBlock()
|
||||
continue
|
||||
} else if (input is PropertyOnly && input.property.isTransient) {
|
||||
continue
|
||||
}
|
||||
|
||||
// We've removed all parameter-only types by this point
|
||||
val property = (input as PropertyComponent).property
|
||||
|
||||
// Proceed as usual
|
||||
if (property.hasLocalIsPresentName || property.hasConstructorDefault) {
|
||||
result.beginControlFlow("%L ->", propertyIndex)
|
||||
if (property.delegateKey.nullable) {
|
||||
result.addStatement(
|
||||
"%N = %N.fromJson(%N)",
|
||||
property.localName,
|
||||
nameAllocator[property.delegateKey],
|
||||
readerParam
|
||||
)
|
||||
} else {
|
||||
val exception = unexpectedNull(property, readerParam)
|
||||
result.addStatement(
|
||||
"%N = %N.fromJson(%N) ?: throw·%L",
|
||||
property.localName,
|
||||
nameAllocator[property.delegateKey],
|
||||
readerParam,
|
||||
exception
|
||||
)
|
||||
}
|
||||
if (property.hasConstructorDefault) {
|
||||
val inverted = (1 shl maskIndex).inv()
|
||||
if (input is ParameterComponent && input.parameter.hasDefault) {
|
||||
maskAllSetValues[maskNameIndex] = maskAllSetValues[maskNameIndex] and inverted
|
||||
}
|
||||
result.addComment("\$mask = \$mask and (1 shl %L).inv()", maskIndex)
|
||||
result.addStatement(
|
||||
"%1L = %1L and 0x%2L.toInt()",
|
||||
maskNames[maskNameIndex],
|
||||
Integer.toHexString(inverted)
|
||||
)
|
||||
} else {
|
||||
// Presence tracker for a mutable property
|
||||
result.addStatement("%N = true", property.localIsPresentName)
|
||||
}
|
||||
result.endControlFlow()
|
||||
} else {
|
||||
if (property.delegateKey.nullable) {
|
||||
result.addStatement(
|
||||
"%L -> %N = %N.fromJson(%N)",
|
||||
propertyIndex,
|
||||
property.localName,
|
||||
nameAllocator[property.delegateKey],
|
||||
readerParam
|
||||
)
|
||||
} else {
|
||||
val exception = unexpectedNull(property, readerParam)
|
||||
result.addStatement(
|
||||
"%L -> %N = %N.fromJson(%N) ?: throw·%L",
|
||||
propertyIndex,
|
||||
property.localName,
|
||||
nameAllocator[property.delegateKey],
|
||||
readerParam,
|
||||
exception
|
||||
)
|
||||
}
|
||||
}
|
||||
if (property.hasConstructorParameter) {
|
||||
constructorPropertyTypes += property.target.type.asTypeBlock()
|
||||
}
|
||||
propertyIndex++
|
||||
updateMaskIndexes()
|
||||
}
|
||||
|
||||
result.beginControlFlow("-1 ->")
|
||||
result.addComment("Unknown name, skip it.")
|
||||
result.addStatement("%N.skipName()", readerParam)
|
||||
result.addStatement("%N.skipValue()", readerParam)
|
||||
result.endControlFlow()
|
||||
|
||||
result.endControlFlow() // when
|
||||
result.endControlFlow() // while
|
||||
result.addStatement("%N.endObject()", readerParam)
|
||||
|
||||
var separator = "\n"
|
||||
|
||||
val resultName = nameAllocator.newName("result")
|
||||
val hasNonConstructorProperties = nonTransientProperties.any { !it.hasConstructorParameter }
|
||||
val returnOrResultAssignment = if (hasNonConstructorProperties) {
|
||||
// Save the result var for reuse
|
||||
result.addStatement("val %N: %T", resultName, originalTypeName)
|
||||
CodeBlock.of("%N = ", resultName)
|
||||
} else {
|
||||
CodeBlock.of("return·")
|
||||
}
|
||||
|
||||
// Used to indicate we're in an if-block that's assigning our result value and
|
||||
// needs to be closed with endControlFlow
|
||||
var closeNextControlFlowInAssignment = false
|
||||
|
||||
if (useDefaultsConstructor) {
|
||||
// Happy path - all parameters with defaults are set
|
||||
val allMasksAreSetBlock = maskNames.withIndex()
|
||||
.map { (index, maskName) ->
|
||||
CodeBlock.of("$maskName·== 0x${Integer.toHexString(maskAllSetValues[index])}.toInt()")
|
||||
}
|
||||
.joinToCode("·&& ")
|
||||
result.beginControlFlow("if (%L)", allMasksAreSetBlock)
|
||||
result.addComment("All parameters with defaults are set, invoke the constructor directly")
|
||||
result.addCode("«%L·%T(", returnOrResultAssignment, originalTypeName)
|
||||
var localSeparator = "\n"
|
||||
val paramsToSet = components.filterIsInstance<ParameterProperty>()
|
||||
.filterNot { it.property.isTransient }
|
||||
|
||||
// Set all non-transient property parameters
|
||||
for (input in paramsToSet) {
|
||||
result.addCode(localSeparator)
|
||||
val property = input.property
|
||||
result.addCode("%N = %N", property.name, property.localName)
|
||||
if (property.isRequired) {
|
||||
result.addMissingPropertyCheck(property, readerParam)
|
||||
} else if (!input.type.isNullable) {
|
||||
// Unfortunately incurs an intrinsic null-check even though we know it's set, but
|
||||
// maybe in the future we can use contracts to omit them.
|
||||
result.addCode("·as·%T", input.type)
|
||||
}
|
||||
localSeparator = ",\n"
|
||||
}
|
||||
result.addCode("\n»)\n")
|
||||
result.nextControlFlow("else")
|
||||
closeNextControlFlowInAssignment = true
|
||||
|
||||
classBuilder.addProperty(constructorProperty)
|
||||
result.addComment("Reflectively invoke the synthetic defaults constructor")
|
||||
// Dynamic default constructor call
|
||||
val nonNullConstructorType = constructorProperty.type.copy(nullable = false)
|
||||
val args = constructorPropertyTypes
|
||||
.plus(0.until(maskCount).map { INT_TYPE_BLOCK }) // Masks, one every 32 params
|
||||
.plus(DEFAULT_CONSTRUCTOR_MARKER_TYPE_BLOCK) // Default constructor marker is always last
|
||||
.joinToCode(", ")
|
||||
val coreLookupBlock = CodeBlock.of(
|
||||
"%T::class.java.getDeclaredConstructor(%L)",
|
||||
originalRawTypeName,
|
||||
args
|
||||
)
|
||||
val lookupBlock = if (originalTypeName is ParameterizedTypeName) {
|
||||
CodeBlock.of("(%L·as·%T)", coreLookupBlock, nonNullConstructorType)
|
||||
} else {
|
||||
coreLookupBlock
|
||||
}
|
||||
val initializerBlock = CodeBlock.of(
|
||||
"this.%1N·?: %2L.also·{ this.%1N·= it }",
|
||||
constructorProperty,
|
||||
lookupBlock
|
||||
)
|
||||
val localConstructorProperty = PropertySpec.builder(
|
||||
nameAllocator.newName("localConstructor"),
|
||||
nonNullConstructorType
|
||||
)
|
||||
.addAnnotation(
|
||||
AnnotationSpec.builder(Suppress::class)
|
||||
.addMember("%S", "UNCHECKED_CAST")
|
||||
.build()
|
||||
)
|
||||
.initializer(initializerBlock)
|
||||
.build()
|
||||
result.addCode("%L", localConstructorProperty)
|
||||
result.addCode(
|
||||
"«%L%N.newInstance(",
|
||||
returnOrResultAssignment,
|
||||
localConstructorProperty
|
||||
)
|
||||
} else {
|
||||
// Standard constructor call. Don't omit generics for parameterized types even if they can be
|
||||
// inferred, as calculating the right condition for inference exceeds the value gained from
|
||||
// being less pedantic.
|
||||
result.addCode("«%L%T(", returnOrResultAssignment, originalTypeName)
|
||||
}
|
||||
|
||||
for (input in components.filterIsInstance<ParameterComponent>()) {
|
||||
result.addCode(separator)
|
||||
if (useDefaultsConstructor) {
|
||||
if (input is ParameterOnly || (input is ParameterProperty && input.property.isTransient)) {
|
||||
// We have to use the default primitive for the available type in order for
|
||||
// invokeDefaultConstructor to properly invoke it. Just using "null" isn't safe because
|
||||
// the transient type may be a primitive type.
|
||||
// Inline a little comment for readability indicating which parameter is it's referring to
|
||||
result.addCode(
|
||||
"/*·%L·*/·%L",
|
||||
input.parameter.name,
|
||||
input.type.rawType().defaultPrimitiveValue()
|
||||
)
|
||||
} else {
|
||||
result.addCode("%N", (input as ParameterProperty).property.localName)
|
||||
}
|
||||
} else if (input !is ParameterOnly) {
|
||||
val property = (input as ParameterProperty).property
|
||||
result.addCode("%N = %N", property.name, property.localName)
|
||||
}
|
||||
if (input is PropertyComponent) {
|
||||
val property = input.property
|
||||
if (!property.isTransient && property.isRequired) {
|
||||
result.addMissingPropertyCheck(property, readerParam)
|
||||
}
|
||||
}
|
||||
separator = ",\n"
|
||||
}
|
||||
|
||||
if (useDefaultsConstructor) {
|
||||
// Add the masks and a null instance for the trailing default marker instance
|
||||
result.addCode(",\n%L,\n/*·DefaultConstructorMarker·*/·null", maskNames.map { CodeBlock.of("%L", it) }.joinToCode(", "))
|
||||
}
|
||||
|
||||
result.addCode("\n»)\n")
|
||||
|
||||
// Close the result assignment control flow, if any
|
||||
if (closeNextControlFlowInAssignment) {
|
||||
result.endControlFlow()
|
||||
}
|
||||
|
||||
// Assign properties not present in the constructor.
|
||||
for (property in nonTransientProperties) {
|
||||
if (property.hasConstructorParameter) {
|
||||
continue // Property already handled.
|
||||
}
|
||||
if (property.hasLocalIsPresentName) {
|
||||
result.beginControlFlow("if (%N)", property.localIsPresentName)
|
||||
result.addStatement(
|
||||
"%N.%N = %N",
|
||||
resultName,
|
||||
property.name,
|
||||
property.localName
|
||||
)
|
||||
result.endControlFlow()
|
||||
} else {
|
||||
result.addStatement(
|
||||
"%1N.%2N = %3N ?: %1N.%2N",
|
||||
resultName,
|
||||
property.name,
|
||||
property.localName
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (hasNonConstructorProperties) {
|
||||
result.addStatement("return·%1N", resultName)
|
||||
}
|
||||
return result.build()
|
||||
}
|
||||
|
||||
private fun unexpectedNull(property: PropertyGenerator, reader: ParameterSpec): CodeBlock {
|
||||
return CodeBlock.of(
|
||||
"%T.unexpectedNull(%S, %S, %N)",
|
||||
MOSHI_UTIL,
|
||||
property.localName,
|
||||
property.jsonName,
|
||||
reader
|
||||
)
|
||||
}
|
||||
|
||||
private fun generateToJsonFun(): FunSpec {
|
||||
val result = FunSpec.builder("toJson")
|
||||
.addModifiers(KModifier.OVERRIDE)
|
||||
.addParameter(writerParam)
|
||||
.addParameter(valueParam)
|
||||
|
||||
result.beginControlFlow("if (%N == null)", valueParam)
|
||||
result.addStatement(
|
||||
"throw·%T(%S)",
|
||||
NullPointerException::class,
|
||||
"${valueParam.name} was null! Wrap in .nullSafe() to write nullable values."
|
||||
)
|
||||
result.endControlFlow()
|
||||
|
||||
result.addStatement("%N.beginObject()", writerParam)
|
||||
nonTransientProperties.forEach { property ->
|
||||
// We manually put in quotes because we know the jsonName is already escaped
|
||||
result.addStatement("%N.name(%S)", writerParam, property.jsonName)
|
||||
result.addStatement(
|
||||
"%N.toJson(%N, %N.%N)",
|
||||
nameAllocator[property.delegateKey],
|
||||
writerParam,
|
||||
valueParam,
|
||||
property.name
|
||||
)
|
||||
}
|
||||
result.addStatement("%N.endObject()", writerParam)
|
||||
|
||||
return result.build()
|
||||
}
|
||||
}
|
||||
|
||||
private fun FunSpec.Builder.addMissingPropertyCheck(property: PropertyGenerator, readerParam: ParameterSpec) {
|
||||
val missingPropertyBlock =
|
||||
CodeBlock.of(
|
||||
"%T.missingProperty(%S, %S, %N)",
|
||||
MOSHI_UTIL,
|
||||
property.localName,
|
||||
property.jsonName,
|
||||
readerParam
|
||||
)
|
||||
addCode(" ?: throw·%L", missingPropertyBlock)
|
||||
}
|
||||
|
||||
/** Represents a prepared adapter with its [spec] and optional associated [proguardConfig]. */
|
||||
@InternalMoshiCodegenApi
|
||||
public data class PreparedAdapter(val spec: FileSpec, val proguardConfig: ProguardConfig?)
|
||||
|
||||
private fun AsmType.toReflectionString(): String {
|
||||
return when (this) {
|
||||
AsmType.VOID_TYPE -> "void"
|
||||
AsmType.BOOLEAN_TYPE -> "boolean"
|
||||
AsmType.CHAR_TYPE -> "char"
|
||||
AsmType.BYTE_TYPE -> "byte"
|
||||
AsmType.SHORT_TYPE -> "short"
|
||||
AsmType.INT_TYPE -> "int"
|
||||
AsmType.FLOAT_TYPE -> "float"
|
||||
AsmType.LONG_TYPE -> "long"
|
||||
AsmType.DOUBLE_TYPE -> "double"
|
||||
else -> when (sort) {
|
||||
AsmType.ARRAY -> "${elementType.toReflectionString()}[]"
|
||||
// Object type
|
||||
else -> className
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private interface PropertyComponent {
|
||||
val property: PropertyGenerator
|
||||
val type: TypeName
|
||||
}
|
||||
|
||||
private interface ParameterComponent {
|
||||
val parameter: TargetParameter
|
||||
val type: TypeName
|
||||
}
|
||||
|
||||
/**
|
||||
* Type hierarchy for describing fromJson() components. Specifically - parameters, properties, and
|
||||
* parameter properties. All three of these scenarios participate in fromJson() parsing.
|
||||
*/
|
||||
private sealed class FromJsonComponent {
|
||||
|
||||
abstract val type: TypeName
|
||||
|
||||
data class ParameterOnly(
|
||||
override val parameter: TargetParameter
|
||||
) : FromJsonComponent(), ParameterComponent {
|
||||
override val type: TypeName = parameter.type
|
||||
}
|
||||
|
||||
data class PropertyOnly(
|
||||
override val property: PropertyGenerator
|
||||
) : FromJsonComponent(), PropertyComponent {
|
||||
override val type: TypeName = property.target.type
|
||||
}
|
||||
|
||||
data class ParameterProperty(
|
||||
override val parameter: TargetParameter,
|
||||
override val property: PropertyGenerator
|
||||
) : FromJsonComponent(), ParameterComponent, PropertyComponent {
|
||||
override val type: TypeName = parameter.type
|
||||
}
|
||||
}
|
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* 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.api
|
||||
|
||||
import com.squareup.kotlinpoet.AnnotationSpec
|
||||
import com.squareup.kotlinpoet.ClassName
|
||||
import com.squareup.kotlinpoet.CodeBlock
|
||||
import com.squareup.kotlinpoet.KModifier
|
||||
import com.squareup.kotlinpoet.MemberName
|
||||
import com.squareup.kotlinpoet.NameAllocator
|
||||
import com.squareup.kotlinpoet.ParameterSpec
|
||||
import com.squareup.kotlinpoet.ParameterizedTypeName
|
||||
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
|
||||
import com.squareup.kotlinpoet.PropertySpec
|
||||
import com.squareup.kotlinpoet.TypeName
|
||||
import com.squareup.kotlinpoet.TypeVariableName
|
||||
import com.squareup.kotlinpoet.WildcardTypeName
|
||||
import com.squareup.kotlinpoet.asClassName
|
||||
import com.squareup.kotlinpoet.joinToCode
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import java.util.Locale
|
||||
|
||||
/** A JsonAdapter that can be used to encode and decode a particular field. */
|
||||
@InternalMoshiCodegenApi
|
||||
public data class DelegateKey(
|
||||
private val type: TypeName,
|
||||
private val jsonQualifiers: List<AnnotationSpec>,
|
||||
) {
|
||||
public val nullable: Boolean get() = type.isNullable
|
||||
|
||||
/** Returns an adapter to use when encoding and decoding this property. */
|
||||
internal fun generateProperty(
|
||||
nameAllocator: NameAllocator,
|
||||
typeRenderer: TypeRenderer,
|
||||
moshiParameter: ParameterSpec,
|
||||
propertyName: String
|
||||
): PropertySpec {
|
||||
val qualifierNames = jsonQualifiers.joinToString("") {
|
||||
"At${it.typeName.rawType().simpleName}"
|
||||
}
|
||||
val adapterName = nameAllocator.newName(
|
||||
"${type.toVariableName().replaceFirstChar { it.lowercase(Locale.US) }}${qualifierNames}Adapter",
|
||||
this
|
||||
)
|
||||
|
||||
val adapterTypeName = JsonAdapter::class.asClassName().parameterizedBy(type)
|
||||
val standardArgs = arrayOf(
|
||||
moshiParameter,
|
||||
typeRenderer.render(type)
|
||||
)
|
||||
|
||||
val (initializerString, args) = when {
|
||||
jsonQualifiers.isEmpty() -> ", %M()" to arrayOf(MemberName("kotlin.collections", "emptySet"))
|
||||
else -> {
|
||||
", setOf(%L)" to arrayOf(jsonQualifiers.map { it.asInstantiationExpression() }.joinToCode())
|
||||
}
|
||||
}
|
||||
val finalArgs = arrayOf(*standardArgs, *args, propertyName)
|
||||
|
||||
return PropertySpec.builder(adapterName, adapterTypeName, KModifier.PRIVATE)
|
||||
.initializer("%N.adapter(%L$initializerString, %S)", *finalArgs)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
private fun AnnotationSpec.asInstantiationExpression(): CodeBlock {
|
||||
// <Type>(args)
|
||||
return CodeBlock.of(
|
||||
"%T(%L)",
|
||||
typeName,
|
||||
members.joinToCode()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a suggested variable name derived from a list of type names. This just concatenates,
|
||||
* yielding types like MapOfStringLong.
|
||||
*/
|
||||
private fun List<TypeName>.toVariableNames() = joinToString("") { it.toVariableName() }
|
||||
|
||||
/** Returns a suggested variable name derived from a type name, like nullableListOfString. */
|
||||
private fun TypeName.toVariableName(): String {
|
||||
val base = when (this) {
|
||||
is ClassName -> simpleName
|
||||
is ParameterizedTypeName -> rawType.simpleName + "Of" + typeArguments.toVariableNames()
|
||||
is WildcardTypeName -> (inTypes + outTypes).toVariableNames()
|
||||
is TypeVariableName -> name + bounds.toVariableNames()
|
||||
else -> throw IllegalArgumentException("Unrecognized type! $this")
|
||||
}
|
||||
|
||||
return if (isNullable) {
|
||||
"Nullable$base"
|
||||
} else {
|
||||
base
|
||||
}
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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.api
|
||||
|
||||
/** Internal Moshi code gen APIs. */
|
||||
@MustBeDocumented
|
||||
@Retention(value = AnnotationRetention.BINARY)
|
||||
@RequiresOptIn(
|
||||
level = RequiresOptIn.Level.WARNING,
|
||||
message = "This is an internal API and may change at any time."
|
||||
)
|
||||
public annotation class InternalMoshiCodegenApi
|
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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.api
|
||||
|
||||
import com.squareup.kotlinpoet.ClassName
|
||||
|
||||
@InternalMoshiCodegenApi
|
||||
public object Options {
|
||||
/**
|
||||
* This processing option can be specified to have a `@Generated` annotation
|
||||
* included in the generated code. It is not encouraged unless you need it for static analysis
|
||||
* reasons and not enabled by default.
|
||||
*
|
||||
* Note that this can only be one of the following values:
|
||||
* * `"javax.annotation.processing.Generated"` (JRE 9+)
|
||||
* * `"javax.annotation.Generated"` (JRE <9)
|
||||
*/
|
||||
public const val OPTION_GENERATED: String = "moshi.generated"
|
||||
|
||||
/**
|
||||
* This boolean processing option can disable proguard rule generation.
|
||||
* Normally, this is not recommended unless end-users build their own JsonAdapter look-up tool.
|
||||
* This is enabled by default.
|
||||
*/
|
||||
public const val OPTION_GENERATE_PROGUARD_RULES: String = "moshi.generateProguardRules"
|
||||
|
||||
/**
|
||||
* This boolean processing option controls whether or not Moshi will directly instantiate
|
||||
* JsonQualifier annotations in Kotlin 1.6+. Note that this is enabled by default in Kotlin 1.6
|
||||
* but can be disabled to restore the legacy behavior of storing annotations on generated adapter
|
||||
* fields and looking them up reflectively.
|
||||
*/
|
||||
public const val OPTION_INSTANTIATE_ANNOTATIONS: String = "moshi.instantiateAnnotations"
|
||||
|
||||
public val POSSIBLE_GENERATED_NAMES: Map<String, ClassName> = arrayOf(
|
||||
ClassName("javax.annotation.processing", "Generated"),
|
||||
ClassName("javax.annotation", "Generated")
|
||||
).associateBy { it.canonicalName }
|
||||
}
|
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* 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.api
|
||||
|
||||
import com.squareup.kotlinpoet.ClassName
|
||||
|
||||
/**
|
||||
* Represents a proguard configuration for a given spec. This covers three main areas:
|
||||
* - Keeping the target class name to Moshi's reflective lookup of the adapter.
|
||||
* - Keeping the generated adapter class name + public constructor for reflective lookup.
|
||||
* - Keeping any used JsonQualifier annotations and the properties they are attached to.
|
||||
* - If the target class has default parameter values, also keeping the associated synthetic
|
||||
* constructor as well as the DefaultConstructorMarker type Kotlin adds to it.
|
||||
*
|
||||
* Each rule is intended to be as specific and targeted as possible to reduce footprint, and each is
|
||||
* conditioned on usage of the original target type.
|
||||
*
|
||||
* To keep this processor as an ISOLATING incremental processor, we generate one file per target
|
||||
* class with a deterministic name (see [outputFilePathWithoutExtension]) with an appropriate
|
||||
* originating element.
|
||||
*/
|
||||
@InternalMoshiCodegenApi
|
||||
public data class ProguardConfig(
|
||||
val targetClass: ClassName,
|
||||
val adapterName: String,
|
||||
val adapterConstructorParams: List<String>,
|
||||
val targetConstructorHasDefaults: Boolean,
|
||||
val targetConstructorParams: List<String>,
|
||||
) {
|
||||
public fun outputFilePathWithoutExtension(canonicalName: String): String {
|
||||
return "META-INF/proguard/moshi-$canonicalName"
|
||||
}
|
||||
|
||||
public fun writeTo(out: Appendable): Unit = out.run {
|
||||
//
|
||||
// -if class {the target class}
|
||||
// -keepnames class {the target class}
|
||||
// -if class {the target class}
|
||||
// -keep class {the generated adapter} {
|
||||
// <init>(...);
|
||||
// private final {adapter fields}
|
||||
// }
|
||||
//
|
||||
val targetName = targetClass.reflectionName()
|
||||
val adapterCanonicalName = ClassName(targetClass.packageName, adapterName).canonicalName
|
||||
// Keep the class name for Moshi's reflective lookup based on it
|
||||
appendLine("-if class $targetName")
|
||||
appendLine("-keepnames class $targetName")
|
||||
|
||||
appendLine("-if class $targetName")
|
||||
appendLine("-keep class $adapterCanonicalName {")
|
||||
// Keep the constructor for Moshi's reflective lookup
|
||||
val constructorArgs = adapterConstructorParams.joinToString(",")
|
||||
appendLine(" public <init>($constructorArgs);")
|
||||
appendLine("}")
|
||||
|
||||
if (targetConstructorHasDefaults) {
|
||||
// If the target class has default parameter values, keep its synthetic constructor
|
||||
//
|
||||
// -keepnames class kotlin.jvm.internal.DefaultConstructorMarker
|
||||
// -keepclassmembers @com.squareup.moshi.JsonClass @kotlin.Metadata class * {
|
||||
// synthetic <init>(...);
|
||||
// }
|
||||
//
|
||||
appendLine("-if class $targetName")
|
||||
appendLine("-keepnames class kotlin.jvm.internal.DefaultConstructorMarker")
|
||||
appendLine("-if class $targetName")
|
||||
appendLine("-keepclassmembers class $targetName {")
|
||||
val allParams = targetConstructorParams.toMutableList()
|
||||
val maskCount = if (targetConstructorParams.isEmpty()) {
|
||||
0
|
||||
} else {
|
||||
(targetConstructorParams.size + 31) / 32
|
||||
}
|
||||
repeat(maskCount) {
|
||||
allParams += "int"
|
||||
}
|
||||
allParams += "kotlin.jvm.internal.DefaultConstructorMarker"
|
||||
val params = allParams.joinToString(",")
|
||||
appendLine(" public synthetic <init>($params);")
|
||||
appendLine("}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a qualified property with its [name] in the adapter fields and list of [qualifiers]
|
||||
* associated with it.
|
||||
*/
|
||||
@InternalMoshiCodegenApi
|
||||
public data class QualifierAdapterProperty(val name: String, val qualifiers: Set<ClassName>)
|
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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.api
|
||||
|
||||
import com.squareup.kotlinpoet.BOOLEAN
|
||||
import com.squareup.kotlinpoet.NameAllocator
|
||||
import com.squareup.kotlinpoet.PropertySpec
|
||||
|
||||
/** Generates functions to encode and decode a property as JSON. */
|
||||
@InternalMoshiCodegenApi
|
||||
public class PropertyGenerator(
|
||||
public val target: TargetProperty,
|
||||
public val delegateKey: DelegateKey,
|
||||
public val isTransient: Boolean = false
|
||||
) {
|
||||
public val name: String = target.name
|
||||
public val jsonName: String = target.jsonName ?: target.name
|
||||
public val hasDefault: Boolean = target.hasDefault
|
||||
|
||||
public lateinit var localName: String
|
||||
public lateinit var localIsPresentName: String
|
||||
|
||||
public val isRequired: Boolean get() = !delegateKey.nullable && !hasDefault
|
||||
|
||||
public val hasConstructorParameter: Boolean get() = target.parameterIndex != -1
|
||||
|
||||
/**
|
||||
* IsPresent is required if the following conditions are met:
|
||||
* - Is not transient
|
||||
* - Has a default
|
||||
* - Is not a constructor parameter (for constructors we use a defaults mask)
|
||||
* - Is nullable (because we differentiate absent from null)
|
||||
*
|
||||
* This is used to indicate that presence should be checked first before possible assigning null
|
||||
* to an absent value
|
||||
*/
|
||||
public val hasLocalIsPresentName: Boolean = !isTransient && hasDefault && !hasConstructorParameter && delegateKey.nullable
|
||||
public val hasConstructorDefault: Boolean = hasDefault && hasConstructorParameter
|
||||
|
||||
internal fun allocateNames(nameAllocator: NameAllocator) {
|
||||
localName = nameAllocator.newName(name)
|
||||
localIsPresentName = nameAllocator.newName("${name}Set")
|
||||
}
|
||||
|
||||
internal fun generateLocalProperty(): PropertySpec {
|
||||
return PropertySpec.builder(localName, target.type.copy(nullable = true))
|
||||
.mutable(true)
|
||||
.apply {
|
||||
if (hasConstructorDefault) {
|
||||
// We default to the primitive default type, as reflectively invoking the constructor
|
||||
// without this (even though it's a throwaway) will fail argument type resolution in
|
||||
// the reflective invocation.
|
||||
initializer(target.type.defaultPrimitiveValue())
|
||||
} else {
|
||||
initializer("null")
|
||||
}
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
internal fun generateLocalIsPresentProperty(): PropertySpec {
|
||||
return PropertySpec.builder(localIsPresentName, BOOLEAN)
|
||||
.mutable(true)
|
||||
.initializer("false")
|
||||
.build()
|
||||
}
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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.api
|
||||
|
||||
import com.squareup.kotlinpoet.KModifier
|
||||
|
||||
/** A constructor in user code that should be called by generated code. */
|
||||
@InternalMoshiCodegenApi
|
||||
public data class TargetConstructor(
|
||||
val parameters: LinkedHashMap<String, TargetParameter>,
|
||||
val visibility: KModifier,
|
||||
val signature: String?
|
||||
) {
|
||||
init {
|
||||
visibility.checkIsVisibility()
|
||||
}
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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.api
|
||||
|
||||
import com.squareup.kotlinpoet.AnnotationSpec
|
||||
import com.squareup.kotlinpoet.TypeName
|
||||
|
||||
/** A parameter in user code that should be populated by generated code. */
|
||||
@InternalMoshiCodegenApi
|
||||
public data class TargetParameter(
|
||||
val name: String,
|
||||
val index: Int,
|
||||
val type: TypeName,
|
||||
val hasDefault: Boolean,
|
||||
val jsonName: String? = null,
|
||||
val jsonIgnore: Boolean = false,
|
||||
val qualifiers: Set<AnnotationSpec>? = null
|
||||
)
|
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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.api
|
||||
|
||||
import com.squareup.kotlinpoet.KModifier
|
||||
import com.squareup.kotlinpoet.PropertySpec
|
||||
import com.squareup.kotlinpoet.TypeName
|
||||
|
||||
/** A property in user code that maps to JSON. */
|
||||
@InternalMoshiCodegenApi
|
||||
public data class TargetProperty(
|
||||
val propertySpec: PropertySpec,
|
||||
val parameter: TargetParameter?,
|
||||
val visibility: KModifier,
|
||||
val jsonName: String?,
|
||||
val jsonIgnore: Boolean
|
||||
) {
|
||||
val name: String get() = propertySpec.name
|
||||
val type: TypeName get() = propertySpec.type
|
||||
val parameterIndex: Int get() = parameter?.index ?: -1
|
||||
val hasDefault: Boolean get() = parameter?.hasDefault ?: true
|
||||
|
||||
override fun toString(): String = name
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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.api
|
||||
|
||||
import com.squareup.kotlinpoet.KModifier
|
||||
import com.squareup.kotlinpoet.TypeName
|
||||
import com.squareup.kotlinpoet.TypeVariableName
|
||||
|
||||
/** A user type that should be decoded and encoded by generated code. */
|
||||
@InternalMoshiCodegenApi
|
||||
public data class TargetType(
|
||||
val typeName: TypeName,
|
||||
val constructor: TargetConstructor,
|
||||
val properties: Map<String, TargetProperty>,
|
||||
val typeVariables: List<TypeVariableName>,
|
||||
val isDataClass: Boolean,
|
||||
val visibility: KModifier,
|
||||
) {
|
||||
|
||||
init {
|
||||
visibility.checkIsVisibility()
|
||||
}
|
||||
}
|
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* 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.api
|
||||
|
||||
import com.squareup.kotlinpoet.ARRAY
|
||||
import com.squareup.kotlinpoet.BOOLEAN
|
||||
import com.squareup.kotlinpoet.BYTE
|
||||
import com.squareup.kotlinpoet.CHAR
|
||||
import com.squareup.kotlinpoet.ClassName
|
||||
import com.squareup.kotlinpoet.CodeBlock
|
||||
import com.squareup.kotlinpoet.DOUBLE
|
||||
import com.squareup.kotlinpoet.FLOAT
|
||||
import com.squareup.kotlinpoet.INT
|
||||
import com.squareup.kotlinpoet.LONG
|
||||
import com.squareup.kotlinpoet.ParameterizedTypeName
|
||||
import com.squareup.kotlinpoet.SHORT
|
||||
import com.squareup.kotlinpoet.TypeName
|
||||
import com.squareup.kotlinpoet.TypeVariableName
|
||||
import com.squareup.kotlinpoet.WildcardTypeName
|
||||
import com.squareup.moshi.Types
|
||||
|
||||
/**
|
||||
* Renders literals like `Types.newParameterizedType(List::class.java, String::class.java)`.
|
||||
* Rendering is pluggable so that type variables can either be resolved or emitted as other code
|
||||
* blocks.
|
||||
*/
|
||||
internal abstract class TypeRenderer {
|
||||
abstract fun renderTypeVariable(typeVariable: TypeVariableName): CodeBlock
|
||||
|
||||
fun render(typeName: TypeName, forceBox: Boolean = false): CodeBlock {
|
||||
if (typeName.annotations.isNotEmpty()) {
|
||||
return render(typeName.copy(annotations = emptyList()), forceBox)
|
||||
}
|
||||
if (typeName.isNullable) {
|
||||
return renderObjectType(typeName.copy(nullable = false))
|
||||
}
|
||||
|
||||
return when (typeName) {
|
||||
is ClassName -> {
|
||||
if (forceBox) {
|
||||
renderObjectType(typeName)
|
||||
} else {
|
||||
CodeBlock.of("%T::class.java", typeName)
|
||||
}
|
||||
}
|
||||
|
||||
is ParameterizedTypeName -> {
|
||||
// If it's an Array type, we shortcut this to return Types.arrayOf()
|
||||
if (typeName.rawType == ARRAY) {
|
||||
CodeBlock.of(
|
||||
"%T.arrayOf(%L)",
|
||||
Types::class,
|
||||
renderObjectType(typeName.typeArguments[0])
|
||||
)
|
||||
} else {
|
||||
val builder = CodeBlock.builder().apply {
|
||||
add("%T.", Types::class)
|
||||
val enclosingClassName = typeName.rawType.enclosingClassName()
|
||||
if (enclosingClassName != null) {
|
||||
add("newParameterizedTypeWithOwner(%L, ", render(enclosingClassName))
|
||||
} else {
|
||||
add("newParameterizedType(")
|
||||
}
|
||||
add("%T::class.java", typeName.rawType)
|
||||
for (typeArgument in typeName.typeArguments) {
|
||||
add(", %L", renderObjectType(typeArgument))
|
||||
}
|
||||
add(")")
|
||||
}
|
||||
builder.build()
|
||||
}
|
||||
}
|
||||
|
||||
is WildcardTypeName -> {
|
||||
val target: TypeName
|
||||
val method: String
|
||||
when {
|
||||
typeName.inTypes.size == 1 -> {
|
||||
target = typeName.inTypes[0]
|
||||
method = "supertypeOf"
|
||||
}
|
||||
typeName.outTypes.size == 1 -> {
|
||||
target = typeName.outTypes[0]
|
||||
method = "subtypeOf"
|
||||
}
|
||||
else -> throw IllegalArgumentException(
|
||||
"Unrepresentable wildcard type. Cannot have more than one bound: $typeName"
|
||||
)
|
||||
}
|
||||
CodeBlock.of("%T.%L(%L)", Types::class, method, render(target, forceBox = true))
|
||||
}
|
||||
|
||||
is TypeVariableName -> renderTypeVariable(typeName)
|
||||
|
||||
else -> throw IllegalArgumentException("Unrepresentable type: $typeName")
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderObjectType(typeName: TypeName): CodeBlock {
|
||||
return if (typeName.isPrimitive()) {
|
||||
CodeBlock.of("%T::class.javaObjectType", typeName)
|
||||
} else {
|
||||
render(typeName)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TypeName.isPrimitive(): Boolean {
|
||||
return when (this) {
|
||||
BOOLEAN, BYTE, SHORT, INT, LONG, CHAR, FLOAT, DOUBLE -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,229 @@
|
||||
/*
|
||||
* 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.api
|
||||
|
||||
import com.squareup.kotlinpoet.ANY
|
||||
import com.squareup.kotlinpoet.ARRAY
|
||||
import com.squareup.kotlinpoet.BOOLEAN
|
||||
import com.squareup.kotlinpoet.BYTE
|
||||
import com.squareup.kotlinpoet.CHAR
|
||||
import com.squareup.kotlinpoet.ClassName
|
||||
import com.squareup.kotlinpoet.CodeBlock
|
||||
import com.squareup.kotlinpoet.DOUBLE
|
||||
import com.squareup.kotlinpoet.DelicateKotlinPoetApi
|
||||
import com.squareup.kotlinpoet.FLOAT
|
||||
import com.squareup.kotlinpoet.INT
|
||||
import com.squareup.kotlinpoet.KModifier
|
||||
import com.squareup.kotlinpoet.LONG
|
||||
import com.squareup.kotlinpoet.LambdaTypeName
|
||||
import com.squareup.kotlinpoet.NOTHING
|
||||
import com.squareup.kotlinpoet.ParameterizedTypeName
|
||||
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
|
||||
import com.squareup.kotlinpoet.SHORT
|
||||
import com.squareup.kotlinpoet.STAR
|
||||
import com.squareup.kotlinpoet.TypeName
|
||||
import com.squareup.kotlinpoet.TypeVariableName
|
||||
import com.squareup.kotlinpoet.UNIT
|
||||
import com.squareup.kotlinpoet.WildcardTypeName
|
||||
import com.squareup.kotlinpoet.asClassName
|
||||
import com.squareup.kotlinpoet.asTypeName
|
||||
import java.lang.reflect.Array
|
||||
|
||||
internal fun TypeName.rawType(): ClassName {
|
||||
return findRawType() ?: throw IllegalArgumentException("Cannot get raw type from $this")
|
||||
}
|
||||
|
||||
internal fun TypeName.findRawType(): ClassName? {
|
||||
return when (this) {
|
||||
is ClassName -> this
|
||||
is ParameterizedTypeName -> rawType
|
||||
is LambdaTypeName -> {
|
||||
var count = parameters.size
|
||||
if (receiver != null) {
|
||||
count++
|
||||
}
|
||||
val functionSimpleName = if (count >= 23) {
|
||||
"FunctionN"
|
||||
} else {
|
||||
"Function$count"
|
||||
}
|
||||
ClassName("kotlin.jvm.functions", functionSimpleName)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
internal fun TypeName.defaultPrimitiveValue(): CodeBlock =
|
||||
when (this) {
|
||||
BOOLEAN -> CodeBlock.of("false")
|
||||
CHAR -> CodeBlock.of("0.toChar()")
|
||||
BYTE -> CodeBlock.of("0.toByte()")
|
||||
SHORT -> CodeBlock.of("0.toShort()")
|
||||
INT -> CodeBlock.of("0")
|
||||
FLOAT -> CodeBlock.of("0f")
|
||||
LONG -> CodeBlock.of("0L")
|
||||
DOUBLE -> CodeBlock.of("0.0")
|
||||
UNIT, Void::class.asTypeName(), NOTHING -> throw IllegalStateException("Parameter with void, Unit, or Nothing type is illegal")
|
||||
else -> CodeBlock.of("null")
|
||||
}
|
||||
|
||||
@OptIn(DelicateKotlinPoetApi::class)
|
||||
internal fun TypeName.asTypeBlock(): CodeBlock {
|
||||
if (annotations.isNotEmpty()) {
|
||||
return copy(annotations = emptyList()).asTypeBlock()
|
||||
}
|
||||
when (this) {
|
||||
is ParameterizedTypeName -> {
|
||||
return if (rawType == ARRAY) {
|
||||
val componentType = typeArguments[0]
|
||||
if (componentType is ParameterizedTypeName) {
|
||||
// "generic" array just uses the component's raw type
|
||||
// java.lang.reflect.Array.newInstance(<raw-type>, 0).javaClass
|
||||
CodeBlock.of(
|
||||
"%T.newInstance(%L, 0).javaClass",
|
||||
Array::class.java.asClassName(),
|
||||
componentType.rawType.asTypeBlock()
|
||||
)
|
||||
} else {
|
||||
CodeBlock.of("%T::class.java", copy(nullable = false))
|
||||
}
|
||||
} else {
|
||||
rawType.asTypeBlock()
|
||||
}
|
||||
}
|
||||
is TypeVariableName -> {
|
||||
val bound = bounds.firstOrNull() ?: ANY
|
||||
return bound.asTypeBlock()
|
||||
}
|
||||
is LambdaTypeName -> return rawType().asTypeBlock()
|
||||
is ClassName -> {
|
||||
// Check against the non-nullable version for equality, but we'll keep the nullability in
|
||||
// consideration when creating the CodeBlock if needed.
|
||||
return when (copy(nullable = false)) {
|
||||
BOOLEAN, CHAR, BYTE, SHORT, INT, FLOAT, LONG, DOUBLE -> {
|
||||
if (isNullable) {
|
||||
// Remove nullable but keep the java object type
|
||||
CodeBlock.of("%T::class.javaObjectType", copy(nullable = false))
|
||||
} else {
|
||||
CodeBlock.of("%T::class.javaPrimitiveType", this)
|
||||
}
|
||||
}
|
||||
UNIT, Void::class.asTypeName(), NOTHING -> throw IllegalStateException("Parameter with void, Unit, or Nothing type is illegal")
|
||||
else -> CodeBlock.of("%T::class.java", copy(nullable = false))
|
||||
}
|
||||
}
|
||||
else -> throw UnsupportedOperationException("Parameter with type '${javaClass.simpleName}' is illegal. Only classes, parameterized types, or type variables are allowed.")
|
||||
}
|
||||
}
|
||||
|
||||
internal fun KModifier.checkIsVisibility() {
|
||||
require(ordinal <= ordinal) {
|
||||
"Visibility must be one of ${(0..ordinal).joinToString { KModifier.values()[it].name }}. Is $name"
|
||||
}
|
||||
}
|
||||
|
||||
internal fun TypeName.stripTypeVarVariance(resolver: TypeVariableResolver): TypeName {
|
||||
return when (this) {
|
||||
is ClassName -> this
|
||||
is ParameterizedTypeName -> {
|
||||
deepCopy { it.stripTypeVarVariance(resolver) }
|
||||
}
|
||||
is TypeVariableName -> resolver[name]
|
||||
is WildcardTypeName -> deepCopy { it.stripTypeVarVariance(resolver) }
|
||||
else -> throw UnsupportedOperationException("Type '${javaClass.simpleName}' is illegal. Only classes, parameterized types, wildcard types, or type variables are allowed.")
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ParameterizedTypeName.deepCopy(
|
||||
transform: (TypeName) -> TypeName
|
||||
): ParameterizedTypeName {
|
||||
return rawType.parameterizedBy(typeArguments.map { transform(it) })
|
||||
.copy(nullable = isNullable, annotations = annotations, tags = tags)
|
||||
}
|
||||
|
||||
internal fun TypeVariableName.deepCopy(
|
||||
variance: KModifier? = this.variance,
|
||||
transform: (TypeName) -> TypeName
|
||||
): TypeVariableName {
|
||||
return TypeVariableName(name = name, bounds = bounds.map { transform(it) }, variance = variance)
|
||||
.copy(nullable = isNullable, annotations = annotations, tags = tags)
|
||||
}
|
||||
|
||||
internal fun WildcardTypeName.deepCopy(transform: (TypeName) -> TypeName): TypeName {
|
||||
// TODO Would be nice if KotlinPoet modeled these easier.
|
||||
// Producer type - empty inTypes, single element outTypes
|
||||
// Consumer type - single element inTypes, single ANY element outType.
|
||||
return when {
|
||||
this == STAR -> this
|
||||
outTypes.isNotEmpty() && inTypes.isEmpty() -> {
|
||||
WildcardTypeName.producerOf(transform(outTypes[0]))
|
||||
.copy(nullable = isNullable, annotations = annotations)
|
||||
}
|
||||
inTypes.isNotEmpty() -> {
|
||||
WildcardTypeName.consumerOf(transform(inTypes[0]))
|
||||
.copy(nullable = isNullable, annotations = annotations)
|
||||
}
|
||||
else -> throw UnsupportedOperationException("Not possible.")
|
||||
}
|
||||
}
|
||||
|
||||
internal fun LambdaTypeName.deepCopy(transform: (TypeName) -> TypeName): TypeName {
|
||||
return LambdaTypeName.get(
|
||||
receiver?.let(transform),
|
||||
parameters.map { it.toBuilder(type = transform(it.type)).build() },
|
||||
transform(returnType)
|
||||
).copy(nullable = isNullable, annotations = annotations, suspending = isSuspending)
|
||||
}
|
||||
|
||||
internal interface TypeVariableResolver {
|
||||
val parametersMap: Map<String, TypeVariableName>
|
||||
operator fun get(index: String): TypeVariableName
|
||||
}
|
||||
|
||||
internal fun List<TypeName>.toTypeVariableResolver(
|
||||
fallback: TypeVariableResolver? = null,
|
||||
sourceType: String? = null,
|
||||
): TypeVariableResolver {
|
||||
val parametersMap = LinkedHashMap<String, TypeVariableName>()
|
||||
val typeParamResolver = { id: String ->
|
||||
parametersMap[id]
|
||||
?: fallback?.get(id)
|
||||
?: throw IllegalStateException("No type argument found for $id! Anaylzing $sourceType")
|
||||
}
|
||||
|
||||
val resolver = object : TypeVariableResolver {
|
||||
override val parametersMap: Map<String, TypeVariableName> = parametersMap
|
||||
|
||||
override operator fun get(index: String): TypeVariableName = typeParamResolver(index)
|
||||
}
|
||||
|
||||
// Fill the parametersMap. Need to do sequentially and allow for referencing previously defined params
|
||||
for (typeVar in this) {
|
||||
check(typeVar is TypeVariableName)
|
||||
// Put the simple typevar in first, then it can be referenced in the full toTypeVariable()
|
||||
// replacement later that may add bounds referencing this.
|
||||
val id = typeVar.name
|
||||
parametersMap[id] = TypeVariableName(id)
|
||||
}
|
||||
|
||||
for (typeVar in this) {
|
||||
check(typeVar is TypeVariableName)
|
||||
// Now replace it with the full version.
|
||||
parametersMap[typeVar.name] = typeVar.deepCopy(null) { it.stripTypeVarVariance(resolver) }
|
||||
}
|
||||
|
||||
return resolver
|
||||
}
|
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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.api
|
||||
|
||||
import com.squareup.kotlinpoet.AnnotationSpec
|
||||
import com.squareup.kotlinpoet.ClassName
|
||||
import com.squareup.kotlinpoet.Dynamic
|
||||
import com.squareup.kotlinpoet.LambdaTypeName
|
||||
import com.squareup.kotlinpoet.ParameterizedTypeName
|
||||
import com.squareup.kotlinpoet.TypeName
|
||||
import com.squareup.kotlinpoet.TypeVariableName
|
||||
import com.squareup.kotlinpoet.WildcardTypeName
|
||||
import com.squareup.kotlinpoet.tag
|
||||
import com.squareup.kotlinpoet.tags.TypeAliasTag
|
||||
import java.util.TreeSet
|
||||
|
||||
private fun TypeName.unwrapTypeAliasInternal(): TypeName? {
|
||||
return tag<TypeAliasTag>()?.abbreviatedType?.let { unwrappedType ->
|
||||
// Keep track of all annotations across type levels. Sort them too for consistency.
|
||||
val runningAnnotations = TreeSet<AnnotationSpec>(compareBy { it.toString() }).apply {
|
||||
addAll(annotations)
|
||||
}
|
||||
val nestedUnwrappedType = unwrappedType.unwrapTypeAlias()
|
||||
runningAnnotations.addAll(nestedUnwrappedType.annotations)
|
||||
// If any type is nullable, then the whole thing is nullable
|
||||
val isAnyNullable = isNullable || nestedUnwrappedType.isNullable
|
||||
nestedUnwrappedType.copy(nullable = isAnyNullable, annotations = runningAnnotations.toList())
|
||||
}
|
||||
}
|
||||
|
||||
internal fun TypeName.unwrapTypeAlias(): TypeName {
|
||||
return when (this) {
|
||||
is ClassName -> unwrapTypeAliasInternal() ?: this
|
||||
is ParameterizedTypeName -> {
|
||||
unwrapTypeAliasInternal() ?: deepCopy(TypeName::unwrapTypeAlias)
|
||||
}
|
||||
is TypeVariableName -> {
|
||||
unwrapTypeAliasInternal() ?: deepCopy(transform = TypeName::unwrapTypeAlias)
|
||||
}
|
||||
is WildcardTypeName -> {
|
||||
unwrapTypeAliasInternal() ?: deepCopy(TypeName::unwrapTypeAlias)
|
||||
}
|
||||
is LambdaTypeName -> {
|
||||
unwrapTypeAliasInternal() ?: deepCopy(TypeName::unwrapTypeAlias)
|
||||
}
|
||||
Dynamic -> throw UnsupportedOperationException("Type '${javaClass.simpleName}' is illegal. Only classes, parameterized types, wildcard types, or type variables are allowed.")
|
||||
}
|
||||
}
|
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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))
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,530 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
}
|
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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.ksp
|
||||
|
||||
import com.google.devtools.ksp.getAllSuperTypes
|
||||
import com.google.devtools.ksp.processing.Resolver
|
||||
import com.google.devtools.ksp.symbol.ClassKind.CLASS
|
||||
import com.google.devtools.ksp.symbol.KSClassDeclaration
|
||||
import com.squareup.kotlinpoet.ANY
|
||||
import com.squareup.kotlinpoet.TypeName
|
||||
import com.squareup.kotlinpoet.asClassName
|
||||
import com.squareup.kotlinpoet.ksp.toClassName
|
||||
|
||||
private val OBJECT_CLASS = java.lang.Object::class.asClassName()
|
||||
|
||||
/**
|
||||
* A concrete type like `List<String>` with enough information to know how to resolve its type
|
||||
* variables.
|
||||
*/
|
||||
internal class AppliedType private constructor(
|
||||
val type: KSClassDeclaration,
|
||||
val typeName: TypeName = type.toClassName()
|
||||
) {
|
||||
|
||||
/** Returns all super classes of this, recursively. Only [CLASS] is used as we can't really use other types. */
|
||||
fun superclasses(
|
||||
resolver: Resolver,
|
||||
): LinkedHashSet<AppliedType> {
|
||||
val result: LinkedHashSet<AppliedType> = LinkedHashSet()
|
||||
result.add(this)
|
||||
for (supertype in type.getAllSuperTypes()) {
|
||||
val decl = supertype.declaration
|
||||
check(decl is KSClassDeclaration)
|
||||
if (decl.classKind != CLASS) {
|
||||
// Don't load properties for interface types.
|
||||
continue
|
||||
}
|
||||
val qualifiedName = decl.qualifiedName
|
||||
val superTypeKsClass = resolver.getClassDeclarationByName(qualifiedName!!)!!
|
||||
val typeName = decl.toClassName()
|
||||
if (typeName == ANY || typeName == OBJECT_CLASS) {
|
||||
// Don't load properties for kotlin.Any/java.lang.Object.
|
||||
continue
|
||||
}
|
||||
result.add(AppliedType(superTypeKsClass, typeName))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString() = type.qualifiedName!!.asString()
|
||||
|
||||
companion object {
|
||||
operator fun invoke(type: KSClassDeclaration): AppliedType {
|
||||
return AppliedType(type)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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.ksp
|
||||
|
||||
import com.google.auto.service.AutoService
|
||||
import com.google.devtools.ksp.processing.CodeGenerator
|
||||
import com.google.devtools.ksp.processing.Dependencies
|
||||
import com.google.devtools.ksp.processing.KSPLogger
|
||||
import com.google.devtools.ksp.processing.Resolver
|
||||
import com.google.devtools.ksp.processing.SymbolProcessor
|
||||
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
|
||||
import com.google.devtools.ksp.processing.SymbolProcessorProvider
|
||||
import com.google.devtools.ksp.symbol.KSAnnotated
|
||||
import com.google.devtools.ksp.symbol.KSDeclaration
|
||||
import com.google.devtools.ksp.symbol.KSFile
|
||||
import com.squareup.kotlinpoet.AnnotationSpec
|
||||
import com.squareup.kotlinpoet.ClassName
|
||||
import com.squareup.kotlinpoet.ksp.addOriginatingKSFile
|
||||
import com.squareup.kotlinpoet.ksp.writeTo
|
||||
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.POSSIBLE_GENERATED_NAMES
|
||||
import com.squareup.moshi.kotlin.codegen.api.ProguardConfig
|
||||
import com.squareup.moshi.kotlin.codegen.api.PropertyGenerator
|
||||
import java.io.OutputStreamWriter
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
@AutoService(SymbolProcessorProvider::class)
|
||||
public class JsonClassSymbolProcessorProvider : SymbolProcessorProvider {
|
||||
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
|
||||
return JsonClassSymbolProcessor(environment)
|
||||
}
|
||||
}
|
||||
|
||||
private class JsonClassSymbolProcessor(
|
||||
environment: SymbolProcessorEnvironment
|
||||
) : SymbolProcessor {
|
||||
|
||||
private companion object {
|
||||
val JSON_CLASS_NAME = JsonClass::class.qualifiedName!!
|
||||
}
|
||||
|
||||
private val codeGenerator = environment.codeGenerator
|
||||
private val logger = environment.logger
|
||||
private val generatedOption = environment.options[OPTION_GENERATED]?.also {
|
||||
logger.check(it in POSSIBLE_GENERATED_NAMES) {
|
||||
"Invalid option value for $OPTION_GENERATED. Found $it, allowable values are ${POSSIBLE_GENERATED_NAMES.keys}."
|
||||
}
|
||||
}
|
||||
private val generateProguardRules = environment.options[OPTION_GENERATE_PROGUARD_RULES]?.toBooleanStrictOrNull() ?: true
|
||||
|
||||
override fun process(resolver: Resolver): List<KSAnnotated> {
|
||||
val generatedAnnotation = generatedOption?.let {
|
||||
AnnotationSpec.builder(ClassName.bestGuess(it))
|
||||
.addMember("value = [%S]", JsonClassSymbolProcessor::class.java.canonicalName)
|
||||
.addMember("comments = %S", "https://github.com/square/moshi")
|
||||
.build()
|
||||
}
|
||||
|
||||
for (type in resolver.getSymbolsWithAnnotation(JSON_CLASS_NAME)) {
|
||||
// For the smart cast
|
||||
if (type !is KSDeclaration) {
|
||||
logger.error("@JsonClass can't be applied to $type: must be a Kotlin class", type)
|
||||
continue
|
||||
}
|
||||
|
||||
val jsonClassAnnotation = type.findAnnotationWithType<JsonClass>() ?: continue
|
||||
|
||||
val generator = jsonClassAnnotation.generator
|
||||
|
||||
if (generator.isNotEmpty()) continue
|
||||
|
||||
if (!jsonClassAnnotation.generateAdapter) continue
|
||||
|
||||
val originatingFile = type.containingFile!!
|
||||
val adapterGenerator = adapterGenerator(logger, resolver, type) ?: return emptyList()
|
||||
try {
|
||||
val preparedAdapter = adapterGenerator
|
||||
.prepare(generateProguardRules) { spec ->
|
||||
spec.toBuilder()
|
||||
.apply {
|
||||
generatedAnnotation?.let(::addAnnotation)
|
||||
}
|
||||
.addOriginatingKSFile(originatingFile)
|
||||
.build()
|
||||
}
|
||||
preparedAdapter.spec.writeTo(codeGenerator, aggregating = false)
|
||||
preparedAdapter.proguardConfig?.writeTo(codeGenerator, originatingFile)
|
||||
} catch (e: Exception) {
|
||||
logger.error(
|
||||
"Error preparing ${type.simpleName.asString()}: ${e.stackTrace.joinToString("\n")}"
|
||||
)
|
||||
}
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
private fun adapterGenerator(
|
||||
logger: KSPLogger,
|
||||
resolver: Resolver,
|
||||
originalType: KSDeclaration,
|
||||
): AdapterGenerator? {
|
||||
val type = targetType(originalType, resolver, logger) ?: return null
|
||||
|
||||
val properties = mutableMapOf<String, PropertyGenerator>()
|
||||
for (property in type.properties.values) {
|
||||
val generator = property.generator(logger, resolver, originalType)
|
||||
if (generator != null) {
|
||||
properties[property.name] = generator
|
||||
}
|
||||
}
|
||||
|
||||
for ((name, parameter) in type.constructor.parameters) {
|
||||
if (type.properties[parameter.name] == null && !parameter.hasDefault) {
|
||||
// TODO would be nice if we could pass the parameter node directly?
|
||||
logger.error("No property for required constructor parameter $name", originalType)
|
||||
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 [codeGenerator]. */
|
||||
private fun ProguardConfig.writeTo(codeGenerator: CodeGenerator, originatingKSFile: KSFile) {
|
||||
val file = codeGenerator.createNewFile(
|
||||
dependencies = Dependencies(aggregating = false, originatingKSFile),
|
||||
packageName = "",
|
||||
fileName = outputFilePathWithoutExtension(targetClass.canonicalName),
|
||||
extensionName = "pro"
|
||||
)
|
||||
// Don't use writeTo(file) because that tries to handle directories under the hood
|
||||
OutputStreamWriter(file, StandardCharsets.UTF_8)
|
||||
.use(::writeTo)
|
||||
}
|
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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.ksp
|
||||
|
||||
import com.google.devtools.ksp.processing.KSPLogger
|
||||
import com.google.devtools.ksp.processing.Resolver
|
||||
import com.google.devtools.ksp.symbol.ClassKind
|
||||
import com.google.devtools.ksp.symbol.KSAnnotated
|
||||
import com.google.devtools.ksp.symbol.KSAnnotation
|
||||
import com.google.devtools.ksp.symbol.KSClassDeclaration
|
||||
import com.google.devtools.ksp.symbol.KSName
|
||||
import com.google.devtools.ksp.symbol.KSNode
|
||||
import com.google.devtools.ksp.symbol.KSType
|
||||
import com.google.devtools.ksp.symbol.KSTypeAlias
|
||||
import com.google.devtools.ksp.symbol.Origin.KOTLIN
|
||||
import com.google.devtools.ksp.symbol.Origin.KOTLIN_LIB
|
||||
import com.squareup.kotlinpoet.AnnotationSpec
|
||||
import com.squareup.kotlinpoet.ClassName
|
||||
import com.squareup.kotlinpoet.CodeBlock
|
||||
import com.squareup.kotlinpoet.ksp.toClassName
|
||||
|
||||
internal fun KSClassDeclaration.asType() = asType(emptyList())
|
||||
|
||||
internal fun KSClassDeclaration.isKotlinClass(): Boolean {
|
||||
return origin == KOTLIN ||
|
||||
origin == KOTLIN_LIB ||
|
||||
isAnnotationPresent(Metadata::class)
|
||||
}
|
||||
|
||||
internal inline fun <reified T : Annotation> KSAnnotated.findAnnotationWithType(): T? {
|
||||
return getAnnotationsByType(T::class).firstOrNull()
|
||||
}
|
||||
|
||||
internal fun KSType.unwrapTypeAlias(): KSType {
|
||||
return if (this.declaration is KSTypeAlias) {
|
||||
(this.declaration as KSTypeAlias).type.resolve()
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
internal fun KSAnnotation.toAnnotationSpec(resolver: Resolver): AnnotationSpec {
|
||||
val element = annotationType.resolve().unwrapTypeAlias().declaration as KSClassDeclaration
|
||||
val builder = AnnotationSpec.builder(element.toClassName())
|
||||
for (argument in arguments) {
|
||||
val member = CodeBlock.builder()
|
||||
val name = argument.name!!.getShortName()
|
||||
member.add("%L = ", name)
|
||||
addValueToBlock(argument.value!!, resolver, member)
|
||||
builder.addMember(member.build())
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
private fun addValueToBlock(value: Any, resolver: Resolver, member: CodeBlock.Builder) {
|
||||
when (value) {
|
||||
is List<*> -> {
|
||||
// Array type
|
||||
member.add("arrayOf(⇥⇥")
|
||||
value.forEachIndexed { index, innerValue ->
|
||||
if (index > 0) member.add(", ")
|
||||
addValueToBlock(innerValue!!, resolver, member)
|
||||
}
|
||||
member.add("⇤⇤)")
|
||||
}
|
||||
is KSType -> {
|
||||
val unwrapped = value.unwrapTypeAlias()
|
||||
val isEnum = (unwrapped.declaration as KSClassDeclaration).classKind == ClassKind.ENUM_ENTRY
|
||||
if (isEnum) {
|
||||
val parent = unwrapped.declaration.parentDeclaration as KSClassDeclaration
|
||||
val entry = unwrapped.declaration.simpleName.getShortName()
|
||||
member.add("%T.%L", parent.toClassName(), entry)
|
||||
} else {
|
||||
member.add("%T::class", unwrapped.toClassName())
|
||||
}
|
||||
}
|
||||
is KSName ->
|
||||
member.add(
|
||||
"%T.%L", ClassName.bestGuess(value.getQualifier()),
|
||||
value.getShortName()
|
||||
)
|
||||
is KSAnnotation -> member.add("%L", value.toAnnotationSpec(resolver))
|
||||
else -> member.add(memberForValue(value))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a [CodeBlock] with parameter `format` depending on the given `value` object.
|
||||
* Handles a number of special cases, such as appending "f" to `Float` values, and uses
|
||||
* `%L` for other types.
|
||||
*/
|
||||
internal fun memberForValue(value: Any) = when (value) {
|
||||
is Class<*> -> CodeBlock.of("%T::class", value)
|
||||
is Enum<*> -> CodeBlock.of("%T.%L", value.javaClass, value.name)
|
||||
is String -> CodeBlock.of("%S", value)
|
||||
is Float -> CodeBlock.of("%Lf", value)
|
||||
is Double -> CodeBlock.of("%L", value)
|
||||
is Char -> CodeBlock.of("$value.toChar()")
|
||||
is Byte -> CodeBlock.of("$value.toByte()")
|
||||
is Short -> CodeBlock.of("$value.toShort()")
|
||||
// Int or Boolean
|
||||
else -> CodeBlock.of("%L", value)
|
||||
}
|
||||
|
||||
internal inline fun KSPLogger.check(condition: Boolean, message: () -> String) {
|
||||
check(condition, null, message)
|
||||
}
|
||||
|
||||
internal inline fun KSPLogger.check(condition: Boolean, element: KSNode?, message: () -> String) {
|
||||
if (!condition) {
|
||||
error(message(), element)
|
||||
}
|
||||
}
|
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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.ksp
|
||||
|
||||
import com.google.devtools.ksp.processing.KSPLogger
|
||||
import com.google.devtools.ksp.processing.Resolver
|
||||
import com.google.devtools.ksp.symbol.KSClassDeclaration
|
||||
import com.google.devtools.ksp.symbol.KSDeclaration
|
||||
import com.squareup.kotlinpoet.AnnotationSpec
|
||||
import com.squareup.kotlinpoet.KModifier
|
||||
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.TargetProperty
|
||||
import com.squareup.moshi.kotlin.codegen.api.rawType
|
||||
|
||||
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(
|
||||
logger: KSPLogger,
|
||||
resolver: Resolver,
|
||||
originalType: KSDeclaration,
|
||||
): PropertyGenerator? {
|
||||
if (jsonIgnore) {
|
||||
if (!hasDefault) {
|
||||
logger.error(
|
||||
"No default value for transient/ignored property $name",
|
||||
originalType
|
||||
)
|
||||
return null
|
||||
}
|
||||
return PropertyGenerator(this, DelegateKey(type, emptyList()), true)
|
||||
}
|
||||
|
||||
if (!isVisible) {
|
||||
logger.error(
|
||||
"property $name is not visible",
|
||||
originalType
|
||||
)
|
||||
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
|
||||
for (jsonQualifier in qualifiers) {
|
||||
val qualifierRawType = jsonQualifier.typeName.rawType()
|
||||
// Check Java types since that covers both Java and Kotlin annotations.
|
||||
resolver.getClassDeclarationByName(qualifierRawType.canonicalName)?.let { annotationElement ->
|
||||
annotationElement.findAnnotationWithType<Retention>()?.let {
|
||||
if (it.value != AnnotationRetention.RUNTIME) {
|
||||
logger.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)
|
||||
)
|
||||
}
|
||||
|
||||
internal val KSClassDeclaration.isJsonQualifier: Boolean
|
||||
get() = isAnnotationPresent(JsonQualifier::class)
|
@@ -0,0 +1,280 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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.ksp
|
||||
|
||||
import com.google.devtools.ksp.KspExperimental
|
||||
import com.google.devtools.ksp.getDeclaredProperties
|
||||
import com.google.devtools.ksp.getVisibility
|
||||
import com.google.devtools.ksp.isInternal
|
||||
import com.google.devtools.ksp.isLocal
|
||||
import com.google.devtools.ksp.isPublic
|
||||
import com.google.devtools.ksp.processing.KSPLogger
|
||||
import com.google.devtools.ksp.processing.Resolver
|
||||
import com.google.devtools.ksp.symbol.ClassKind
|
||||
import com.google.devtools.ksp.symbol.ClassKind.CLASS
|
||||
import com.google.devtools.ksp.symbol.KSAnnotated
|
||||
import com.google.devtools.ksp.symbol.KSClassDeclaration
|
||||
import com.google.devtools.ksp.symbol.KSDeclaration
|
||||
import com.google.devtools.ksp.symbol.KSPropertyDeclaration
|
||||
import com.google.devtools.ksp.symbol.KSType
|
||||
import com.google.devtools.ksp.symbol.KSTypeParameter
|
||||
import com.google.devtools.ksp.symbol.Modifier
|
||||
import com.google.devtools.ksp.symbol.Origin
|
||||
import com.squareup.kotlinpoet.AnnotationSpec
|
||||
import com.squareup.kotlinpoet.ClassName
|
||||
import com.squareup.kotlinpoet.KModifier
|
||||
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
|
||||
import com.squareup.kotlinpoet.PropertySpec
|
||||
import com.squareup.kotlinpoet.TypeName
|
||||
import com.squareup.kotlinpoet.ksp.TypeParameterResolver
|
||||
import com.squareup.kotlinpoet.ksp.toClassName
|
||||
import com.squareup.kotlinpoet.ksp.toKModifier
|
||||
import com.squareup.kotlinpoet.ksp.toTypeName
|
||||
import com.squareup.kotlinpoet.ksp.toTypeParameterResolver
|
||||
import com.squareup.kotlinpoet.ksp.toTypeVariableName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonQualifier
|
||||
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.unwrapTypeAlias
|
||||
|
||||
/** Returns a target type for [type] or null if it cannot be used with code gen. */
|
||||
internal fun targetType(
|
||||
type: KSDeclaration,
|
||||
resolver: Resolver,
|
||||
logger: KSPLogger,
|
||||
): TargetType? {
|
||||
if (type !is KSClassDeclaration) {
|
||||
logger.error("@JsonClass can't be applied to ${type.qualifiedName?.asString()}: must be a Kotlin class", type)
|
||||
return null
|
||||
}
|
||||
logger.check(type.classKind != ClassKind.ENUM_CLASS, type) {
|
||||
"@JsonClass with 'generateAdapter = \"true\"' can't be applied to ${type.qualifiedName?.asString()}: code gen for enums is not supported or necessary"
|
||||
}
|
||||
logger.check(type.classKind == CLASS && type.origin == Origin.KOTLIN, type) {
|
||||
"@JsonClass can't be applied to ${type.qualifiedName?.asString()}: must be a Kotlin class"
|
||||
}
|
||||
logger.check(Modifier.INNER !in type.modifiers, type) {
|
||||
"@JsonClass can't be applied to ${type.qualifiedName?.asString()}: must not be an inner class"
|
||||
}
|
||||
logger.check(Modifier.SEALED !in type.modifiers, type) {
|
||||
"@JsonClass can't be applied to ${type.qualifiedName?.asString()}: must not be sealed"
|
||||
}
|
||||
logger.check(Modifier.ABSTRACT !in type.modifiers, type) {
|
||||
"@JsonClass can't be applied to ${type.qualifiedName?.asString()}: must not be abstract"
|
||||
}
|
||||
logger.check(!type.isLocal(), type) {
|
||||
"@JsonClass can't be applied to ${type.qualifiedName?.asString()}: must not be local"
|
||||
}
|
||||
logger.check(type.isPublic() || type.isInternal(), type) {
|
||||
"@JsonClass can't be applied to ${type.qualifiedName?.asString()}: must be internal or public"
|
||||
}
|
||||
|
||||
val classTypeParamsResolver = type.typeParameters.toTypeParameterResolver(
|
||||
sourceTypeHint = type.qualifiedName!!.asString()
|
||||
)
|
||||
val typeVariables = type.typeParameters.map { it.toTypeVariableName(classTypeParamsResolver) }
|
||||
val appliedType = AppliedType(type)
|
||||
|
||||
val constructor = primaryConstructor(resolver, type, classTypeParamsResolver, logger)
|
||||
?: run {
|
||||
logger.error("No primary constructor found on $type", type)
|
||||
return null
|
||||
}
|
||||
if (constructor.visibility != KModifier.INTERNAL && constructor.visibility != KModifier.PUBLIC) {
|
||||
logger.error(
|
||||
"@JsonClass can't be applied to $type: " +
|
||||
"primary constructor is not internal or public",
|
||||
type
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
val properties = mutableMapOf<String, TargetProperty>()
|
||||
|
||||
val originalType = appliedType.type
|
||||
for (superclass in appliedType.superclasses(resolver)) {
|
||||
val classDecl = superclass.type
|
||||
if (!classDecl.isKotlinClass()) {
|
||||
logger.error(
|
||||
"""
|
||||
@JsonClass can't be applied to $type: supertype $superclass is not a Kotlin type.
|
||||
Origin=${classDecl.origin}
|
||||
Annotations=${classDecl.annotations.joinToString(prefix = "[", postfix = "]") { it.shortName.getShortName() }}
|
||||
""".trimIndent(),
|
||||
type
|
||||
)
|
||||
return null
|
||||
}
|
||||
val supertypeProperties = declaredProperties(
|
||||
constructor = constructor,
|
||||
originalType = originalType,
|
||||
classDecl = classDecl,
|
||||
resolver = resolver,
|
||||
typeParameterResolver = classDecl.typeParameters
|
||||
.toTypeParameterResolver(classTypeParamsResolver)
|
||||
)
|
||||
for ((name, property) in supertypeProperties) {
|
||||
properties.putIfAbsent(name, property)
|
||||
}
|
||||
}
|
||||
val visibility = type.getVisibility().toKModifier() ?: KModifier.PUBLIC
|
||||
// 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<KSDeclaration>(type) { it.parentDeclaration }
|
||||
.filterIsInstance<KSClassDeclaration>()
|
||||
.any { it.isInternal() }
|
||||
if (forceInternal) KModifier.INTERNAL else visibility
|
||||
}
|
||||
return TargetType(
|
||||
typeName = type.toClassName().withTypeArguments(typeVariables),
|
||||
constructor = constructor,
|
||||
properties = properties,
|
||||
typeVariables = typeVariables,
|
||||
isDataClass = Modifier.DATA in type.modifiers,
|
||||
visibility = resolvedVisibility,
|
||||
)
|
||||
}
|
||||
|
||||
private fun ClassName.withTypeArguments(arguments: List<TypeName>): TypeName {
|
||||
return if (arguments.isEmpty()) {
|
||||
this
|
||||
} else {
|
||||
this.parameterizedBy(arguments)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(KspExperimental::class)
|
||||
internal fun primaryConstructor(
|
||||
resolver: Resolver,
|
||||
targetType: KSClassDeclaration,
|
||||
typeParameterResolver: TypeParameterResolver,
|
||||
logger: KSPLogger
|
||||
): TargetConstructor? {
|
||||
val primaryConstructor = targetType.primaryConstructor ?: return null
|
||||
|
||||
val parameters = LinkedHashMap<String, TargetParameter>()
|
||||
for ((index, parameter) in primaryConstructor.parameters.withIndex()) {
|
||||
val name = parameter.name!!.getShortName()
|
||||
parameters[name] = TargetParameter(
|
||||
name = name,
|
||||
index = index,
|
||||
type = parameter.type.toTypeName(typeParameterResolver),
|
||||
hasDefault = parameter.hasDefault,
|
||||
qualifiers = parameter.qualifiers(resolver),
|
||||
jsonName = parameter.jsonName()
|
||||
)
|
||||
}
|
||||
|
||||
val kmConstructorSignature: String = resolver.mapToJvmSignature(primaryConstructor)
|
||||
?: run {
|
||||
logger.error("No primary constructor found.", primaryConstructor)
|
||||
return null
|
||||
}
|
||||
return TargetConstructor(
|
||||
parameters,
|
||||
primaryConstructor.getVisibility().toKModifier() ?: KModifier.PUBLIC,
|
||||
kmConstructorSignature
|
||||
)
|
||||
}
|
||||
|
||||
private fun KSAnnotated?.qualifiers(resolver: Resolver): Set<AnnotationSpec> {
|
||||
if (this == null) return setOf()
|
||||
return annotations
|
||||
.filter {
|
||||
it.annotationType.resolve().declaration.isAnnotationPresent(JsonQualifier::class)
|
||||
}
|
||||
.mapTo(mutableSetOf()) {
|
||||
it.toAnnotationSpec(resolver)
|
||||
}
|
||||
}
|
||||
|
||||
private fun KSAnnotated?.jsonName(): String? {
|
||||
return this?.findAnnotationWithType<Json>()?.name?.takeUnless { it == Json.UNSET_NAME }
|
||||
}
|
||||
|
||||
private fun KSAnnotated?.jsonIgnore(): Boolean {
|
||||
return this?.findAnnotationWithType<Json>()?.ignore ?: false
|
||||
}
|
||||
|
||||
@OptIn(KspExperimental::class)
|
||||
private fun declaredProperties(
|
||||
constructor: TargetConstructor,
|
||||
originalType: KSClassDeclaration,
|
||||
classDecl: KSClassDeclaration,
|
||||
resolver: Resolver,
|
||||
typeParameterResolver: TypeParameterResolver,
|
||||
): Map<String, TargetProperty> {
|
||||
val result = mutableMapOf<String, TargetProperty>()
|
||||
for (property in classDecl.getDeclaredProperties()) {
|
||||
val initialType = property.type.resolve()
|
||||
val resolvedType = if (initialType.declaration is KSTypeParameter) {
|
||||
property.asMemberOf(originalType.asType())
|
||||
} else {
|
||||
initialType
|
||||
}
|
||||
val propertySpec = property.toPropertySpec(resolver, resolvedType, typeParameterResolver)
|
||||
val name = propertySpec.name
|
||||
val parameter = constructor.parameters[name]
|
||||
val isTransient = Modifier.JAVA_TRANSIENT in property.modifiers ||
|
||||
property.isAnnotationPresent(Transient::class) ||
|
||||
Modifier.JAVA_TRANSIENT in resolver.effectiveJavaModifiers(property)
|
||||
result[name] = TargetProperty(
|
||||
propertySpec = propertySpec,
|
||||
parameter = parameter,
|
||||
visibility = property.getVisibility().toKModifier() ?: KModifier.PUBLIC,
|
||||
jsonName = parameter?.jsonName ?: property.jsonName() ?: name,
|
||||
jsonIgnore = isTransient || parameter?.jsonIgnore == true || property.jsonIgnore()
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private fun KSPropertyDeclaration.toPropertySpec(
|
||||
resolver: Resolver,
|
||||
resolvedType: KSType,
|
||||
typeParameterResolver: TypeParameterResolver
|
||||
): PropertySpec {
|
||||
return PropertySpec.builder(
|
||||
name = simpleName.getShortName(),
|
||||
type = resolvedType.toTypeName(typeParameterResolver).unwrapTypeAlias()
|
||||
)
|
||||
.mutable(isMutable)
|
||||
.addModifiers(modifiers.map { KModifier.valueOf(it.name) })
|
||||
.apply {
|
||||
addAnnotations(
|
||||
this@toPropertySpec.annotations
|
||||
.mapNotNull {
|
||||
if ((it.annotationType.resolve().unwrapTypeAlias().declaration as KSClassDeclaration).isJsonQualifier
|
||||
) {
|
||||
it.toAnnotationSpec(resolver)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
.asIterable()
|
||||
)
|
||||
}
|
||||
.build()
|
||||
}
|
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* Copyright 2020 Google LLC
|
||||
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
*
|
||||
* 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.ksp
|
||||
|
||||
import com.google.devtools.ksp.processing.Resolver
|
||||
import com.google.devtools.ksp.symbol.KSAnnotated
|
||||
import com.google.devtools.ksp.symbol.KSAnnotation
|
||||
import com.google.devtools.ksp.symbol.KSClassDeclaration
|
||||
import com.google.devtools.ksp.symbol.KSType
|
||||
import com.google.devtools.ksp.symbol.KSValueArgument
|
||||
import java.lang.reflect.InvocationHandler
|
||||
import java.lang.reflect.Method
|
||||
import java.lang.reflect.Proxy
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/*
|
||||
* Copied experimental utilities from KSP.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Find a class in the compilation classpath for the given name.
|
||||
*
|
||||
* @param name fully qualified name of the class to be loaded; using '.' as separator.
|
||||
* @return a KSClassDeclaration, or null if not found.
|
||||
*/
|
||||
internal fun Resolver.getClassDeclarationByName(name: String): KSClassDeclaration? =
|
||||
getClassDeclarationByName(getKSNameFromString(name))
|
||||
|
||||
internal fun <T : Annotation> KSAnnotated.getAnnotationsByType(annotationKClass: KClass<T>): Sequence<T> {
|
||||
return this.annotations.filter {
|
||||
it.shortName.getShortName() == annotationKClass.simpleName && it.annotationType.resolve().declaration
|
||||
.qualifiedName?.asString() == annotationKClass.qualifiedName
|
||||
}.map { it.toAnnotation(annotationKClass.java) }
|
||||
}
|
||||
|
||||
internal fun <T : Annotation> KSAnnotated.isAnnotationPresent(annotationKClass: KClass<T>): Boolean =
|
||||
getAnnotationsByType(annotationKClass).firstOrNull() != null
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <T : Annotation> KSAnnotation.toAnnotation(annotationClass: Class<T>): T {
|
||||
return Proxy.newProxyInstance(
|
||||
annotationClass.classLoader,
|
||||
arrayOf(annotationClass),
|
||||
createInvocationHandler(annotationClass)
|
||||
) as T
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
private fun KSAnnotation.createInvocationHandler(clazz: Class<*>): InvocationHandler {
|
||||
val cache = ConcurrentHashMap<Pair<Class<*>, Any>, Any>(arguments.size)
|
||||
return InvocationHandler { proxy, method, _ ->
|
||||
if (method.name == "toString" && arguments.none { it.name?.asString() == "toString" }) {
|
||||
clazz.canonicalName +
|
||||
arguments.map { argument: KSValueArgument ->
|
||||
// handles default values for enums otherwise returns null
|
||||
val methodName = argument.name?.asString()
|
||||
val value = proxy.javaClass.methods.find { m -> m.name == methodName }?.invoke(proxy)
|
||||
"$methodName=$value"
|
||||
}.toList()
|
||||
} else {
|
||||
val argument = try {
|
||||
arguments.first { it.name?.asString() == method.name }
|
||||
} catch (e: NullPointerException) {
|
||||
throw IllegalArgumentException("This is a bug using the default KClass for an annotation", e)
|
||||
}
|
||||
when (val result = argument.value ?: method.defaultValue) {
|
||||
is Proxy -> result
|
||||
is List<*> -> {
|
||||
val value = { result.asArray(method) }
|
||||
cache.getOrPut(Pair(method.returnType, result), value)
|
||||
}
|
||||
else -> {
|
||||
when {
|
||||
method.returnType.isEnum -> {
|
||||
val value = { result.asEnum(method.returnType) }
|
||||
cache.getOrPut(Pair(method.returnType, result), value)
|
||||
}
|
||||
method.returnType.isAnnotation -> {
|
||||
val value = { (result as KSAnnotation).asAnnotation(method.returnType) }
|
||||
cache.getOrPut(Pair(method.returnType, result), value)
|
||||
}
|
||||
method.returnType.name == "java.lang.Class" -> {
|
||||
val value = { (result as KSType).asClass() }
|
||||
cache.getOrPut(Pair(method.returnType, result), value)
|
||||
}
|
||||
method.returnType.name == "byte" -> {
|
||||
val value = { result.asByte() }
|
||||
cache.getOrPut(Pair(method.returnType, result), value)
|
||||
}
|
||||
method.returnType.name == "short" -> {
|
||||
val value = { result.asShort() }
|
||||
cache.getOrPut(Pair(method.returnType, result), value)
|
||||
}
|
||||
else -> result // original value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun KSAnnotation.asAnnotation(
|
||||
annotationInterface: Class<*>,
|
||||
): Any {
|
||||
return Proxy.newProxyInstance(
|
||||
this.javaClass.classLoader, arrayOf(annotationInterface),
|
||||
this.createInvocationHandler(annotationInterface)
|
||||
) as Proxy
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun List<*>.asArray(method: Method) =
|
||||
when (method.returnType.componentType.name) {
|
||||
"boolean" -> (this as List<Boolean>).toBooleanArray()
|
||||
"byte" -> (this as List<Byte>).toByteArray()
|
||||
"short" -> (this as List<Short>).toShortArray()
|
||||
"char" -> (this as List<Char>).toCharArray()
|
||||
"double" -> (this as List<Double>).toDoubleArray()
|
||||
"float" -> (this as List<Float>).toFloatArray()
|
||||
"int" -> (this as List<Int>).toIntArray()
|
||||
"long" -> (this as List<Long>).toLongArray()
|
||||
"java.lang.Class" -> (this as List<KSType>).map {
|
||||
Class.forName(it.declaration.qualifiedName!!.asString())
|
||||
}.toTypedArray()
|
||||
"java.lang.String" -> (this as List<String>).toTypedArray()
|
||||
else -> { // arrays of enums or annotations
|
||||
when {
|
||||
method.returnType.componentType.isEnum -> {
|
||||
this.toArray(method) { result -> result.asEnum(method.returnType.componentType) }
|
||||
}
|
||||
method.returnType.componentType.isAnnotation -> {
|
||||
this.toArray(method) { result ->
|
||||
(result as KSAnnotation).asAnnotation(method.returnType.componentType)
|
||||
}
|
||||
}
|
||||
else -> throw IllegalStateException("Unable to process type ${method.returnType.componentType.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun List<*>.toArray(method: Method, valueProvider: (Any) -> Any): Array<Any?> {
|
||||
val array: Array<Any?> = java.lang.reflect.Array.newInstance(
|
||||
method.returnType.componentType,
|
||||
this.size
|
||||
) as Array<Any?>
|
||||
for (r in 0 until this.size) {
|
||||
array[r] = this[r]?.let { valueProvider.invoke(it) }
|
||||
}
|
||||
return array
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <T> Any.asEnum(returnType: Class<T>): T =
|
||||
returnType.getDeclaredMethod("valueOf", String::class.java)
|
||||
.invoke(
|
||||
null,
|
||||
// Change from upstream KSP - https://github.com/google/ksp/pull/685
|
||||
if (this is KSType) {
|
||||
this.declaration.simpleName.getShortName()
|
||||
} else {
|
||||
this.toString()
|
||||
}
|
||||
) as T
|
||||
|
||||
private fun Any.asByte(): Byte = if (this is Int) this.toByte() else this as Byte
|
||||
|
||||
private fun Any.asShort(): Short = if (this is Int) this.toShort() else this as Short
|
||||
|
||||
private fun KSType.asClass() = Class.forName(this.declaration.qualifiedName!!.asString())
|
@@ -0,0 +1 @@
|
||||
com.squareup.moshi.kotlin.codegen.apt.JsonClassCodegenProcessor,ISOLATING
|
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import com.squareup.moshi.kotlin.codegen.apt.JsonClassCodegenProcessorTest;
|
||||
|
||||
/** For {@link JsonClassCodegenProcessorTest#extendJavaType}. */
|
||||
public class JavaSuperclass {
|
||||
public int a = 1;
|
||||
}
|
@@ -0,0 +1,805 @@
|
||||
/*
|
||||
* 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.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 var 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): KotlinCompilation.Result {
|
||||
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)
|
||||
}
|
||||
}
|
@@ -0,0 +1,825 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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.ksp
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
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.KotlinCompilation
|
||||
import com.tschuchort.compiletesting.SourceFile
|
||||
import com.tschuchort.compiletesting.SourceFile.Companion.java
|
||||
import com.tschuchort.compiletesting.SourceFile.Companion.kotlin
|
||||
import com.tschuchort.compiletesting.kspArgs
|
||||
import com.tschuchort.compiletesting.kspIncremental
|
||||
import com.tschuchort.compiletesting.kspSourcesDir
|
||||
import com.tschuchort.compiletesting.symbolProcessorProviders
|
||||
import org.junit.Ignore
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
|
||||
/** Execute kotlinc to confirm that either files are generated or errors are printed. */
|
||||
class JsonClassSymbolProcessorTest {
|
||||
|
||||
@Rule @JvmField var temporaryFolder: TemporaryFolder = TemporaryFolder()
|
||||
|
||||
@Test
|
||||
fun privateConstructor() {
|
||||
val result = compile(
|
||||
kotlin(
|
||||
"source.kt",
|
||||
"""
|
||||
package test
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
class PrivateConstructor private constructor(var a: Int, var 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",
|
||||
"""
|
||||
package test
|
||||
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",
|
||||
"""
|
||||
package test
|
||||
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",
|
||||
"""
|
||||
package test
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
interface Interface
|
||||
"""
|
||||
)
|
||||
)
|
||||
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
|
||||
assertThat(result.messages).contains(
|
||||
"@JsonClass can't be applied to test.Interface: must be a Kotlin class"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun interfacesDoNotErrorWhenGeneratorNotSet() {
|
||||
val result = compile(
|
||||
kotlin(
|
||||
"source.kt",
|
||||
"""
|
||||
package test
|
||||
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",
|
||||
"""
|
||||
package test
|
||||
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(
|
||||
"@JsonClass can't be applied to test.AbstractClass: must not be abstract"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sealedClassesNotSupported() {
|
||||
val result = compile(
|
||||
kotlin(
|
||||
"source.kt",
|
||||
"""
|
||||
package test
|
||||
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(
|
||||
"@JsonClass can't be applied to test.SealedClass: must not be sealed"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun innerClassesNotSupported() {
|
||||
val result = compile(
|
||||
kotlin(
|
||||
"source.kt",
|
||||
"""
|
||||
package test
|
||||
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(
|
||||
"@JsonClass can't be applied to test.Outer.InnerClass: must not be an inner class"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun enumClassesNotSupported() {
|
||||
val result = compile(
|
||||
kotlin(
|
||||
"source.kt",
|
||||
"""
|
||||
package test
|
||||
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(
|
||||
"@JsonClass with 'generateAdapter = \"true\"' can't be applied to test.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",
|
||||
"""
|
||||
package test
|
||||
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(
|
||||
"@JsonClass can't be applied to LocalClass: must not be local"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun privateClassesNotSupported() {
|
||||
val result = compile(
|
||||
kotlin(
|
||||
"source.kt",
|
||||
"""
|
||||
package test
|
||||
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(
|
||||
"@JsonClass can't be applied to test.PrivateClass: must be internal or public"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun objectDeclarationsNotSupported() {
|
||||
val result = compile(
|
||||
kotlin(
|
||||
"source.kt",
|
||||
"""
|
||||
package test
|
||||
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(
|
||||
"@JsonClass can't be applied to test.ObjectDeclaration: must be a Kotlin class"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun objectExpressionsNotSupported() {
|
||||
val result = compile(
|
||||
kotlin(
|
||||
"source.kt",
|
||||
"""
|
||||
package test
|
||||
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(
|
||||
"@JsonClass can't be applied to test.expression: must be a Kotlin class"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun requiredTransientConstructorParameterFails() {
|
||||
val result = compile(
|
||||
kotlin(
|
||||
"source.kt",
|
||||
"""
|
||||
package test
|
||||
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(
|
||||
"No default value for transient/ignored property a"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun requiredIgnoredConstructorParameterFails() {
|
||||
val result = compile(
|
||||
kotlin(
|
||||
"source.kt",
|
||||
"""
|
||||
package test
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
class RequiredTransientConstructorParameter(@Json(ignore = true) var a: Int)
|
||||
"""
|
||||
)
|
||||
)
|
||||
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
|
||||
assertThat(result.messages).contains(
|
||||
"No default value for transient/ignored property a"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nonPropertyConstructorParameter() {
|
||||
val result = compile(
|
||||
kotlin(
|
||||
"source.kt",
|
||||
"""
|
||||
package test
|
||||
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(
|
||||
"No property for required constructor parameter a"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun badGeneratedAnnotation() {
|
||||
val result = prepareCompilation(
|
||||
kotlin(
|
||||
"source.kt",
|
||||
"""
|
||||
package test
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Foo(val a: Int)
|
||||
"""
|
||||
)
|
||||
).apply {
|
||||
kspArgs[OPTION_GENERATED] = "javax.annotation.GeneratedBlerg"
|
||||
}.compile()
|
||||
assertThat(result.messages).contains(
|
||||
"Invalid option value for $OPTION_GENERATED"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun disableProguardGeneration() {
|
||||
val compilation = prepareCompilation(
|
||||
kotlin(
|
||||
"source.kt",
|
||||
"""
|
||||
package test
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Foo(val a: Int)
|
||||
"""
|
||||
)
|
||||
).apply {
|
||||
kspArgs[OPTION_GENERATE_PROGUARD_RULES] = "false"
|
||||
}
|
||||
val result = compilation.compile()
|
||||
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK)
|
||||
assertThat(compilation.kspSourcesDir.walkTopDown().filter { it.extension == "pro" }.toList()).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun multipleErrors() {
|
||||
val result = compile(
|
||||
kotlin(
|
||||
"source.kt",
|
||||
"""
|
||||
package test
|
||||
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",
|
||||
"""
|
||||
package test
|
||||
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",
|
||||
"""
|
||||
package test
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.squareup.moshi.kotlin.codegen.JavaSuperclass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
class ExtendsJavaType(var b: Int) : JavaSuperclass()
|
||||
"""
|
||||
),
|
||||
java(
|
||||
"JavaSuperclass.java",
|
||||
"""
|
||||
package com.squareup.moshi.kotlin.codegen;
|
||||
public class JavaSuperclass {
|
||||
public int a = 1;
|
||||
}
|
||||
"""
|
||||
)
|
||||
)
|
||||
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",
|
||||
"""
|
||||
package test
|
||||
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, no FIELD site target necessary
|
||||
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nonRuntimeQualifier() {
|
||||
val result = compile(
|
||||
kotlin(
|
||||
"source.kt",
|
||||
"""
|
||||
package test
|
||||
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",
|
||||
"""
|
||||
package test
|
||||
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.
|
||||
// TODO loading compiled classes from results not supported in KSP yet
|
||||
// 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 compilation = prepareCompilation(
|
||||
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
|
||||
)
|
||||
"""
|
||||
)
|
||||
)
|
||||
val result = compilation.compile()
|
||||
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK)
|
||||
|
||||
compilation.kspSourcesDir.walkTopDown().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
|
||||
inheritClassPath = true
|
||||
symbolProcessorProviders = listOf(JsonClassSymbolProcessorProvider())
|
||||
sources = sourceFiles.asList()
|
||||
verbose = false
|
||||
kspIncremental = true // The default now
|
||||
}
|
||||
}
|
||||
|
||||
private fun compile(vararg sourceFiles: SourceFile): KotlinCompilation.Result {
|
||||
return prepareCompilation(*sourceFiles).compile()
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user