Change the directory structure to match our modules (#1451)

This commit is contained in:
Jesse Wilson
2021-12-08 23:52:51 -05:00
committed by GitHub
parent d5d172c3bb
commit 7578984f25
68 changed files with 24 additions and 265 deletions

View 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()))
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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 }
}

View File

@@ -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>)

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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
)

View File

@@ -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
}

View File

@@ -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()
}
}

View File

@@ -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
}
}
}

View File

@@ -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
}

View File

@@ -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.")
}
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}

View File

@@ -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))
}
}
}

View File

@@ -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
}
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}

View File

@@ -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)
}
}

View File

@@ -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)

View File

@@ -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()
}

View File

@@ -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())

View File

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

View File

@@ -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;
}

View File

@@ -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)
}
}

View File

@@ -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()
}
}