Upstream KSP implementation (#1393)

This commit is contained in:
Zac Sweers
2021-10-16 01:52:04 -04:00
committed by GitHub
parent de8bbf12f5
commit 2db351f8ed
25 changed files with 2164 additions and 131 deletions

View File

@@ -4,6 +4,7 @@ on: [push, pull_request]
jobs:
build:
name: 'Java ${{ matrix.java-version }} | KSP ${{ matrix.use-ksp }}'
runs-on: ubuntu-latest
strategy:
@@ -11,6 +12,7 @@ jobs:
matrix:
java-version:
- 16
use-ksp: [ true, false ]
steps:
- name: Checkout
@@ -36,7 +38,7 @@ jobs:
java-version: ${{ matrix.java-version }}
- name: Test
run: ./gradlew build check --stacktrace
run: ./gradlew build check --stacktrace -PuseKsp=${{ matrix.use-ksp }}
- name: Publish (default branch only)
if: github.repository == 'square/moshi' && github.ref == 'refs/heads/master' && matrix.java-version == '16'

View File

@@ -16,34 +16,32 @@
# For Dokka https://github.com/Kotlin/dokka/issues/1405
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 \
--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \
--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED \
--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED \
--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED \
--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-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
# TODO move this to DSL in Kotlin 1.5.30 https://youtrack.jetbrains.com/issue/KT-44266
kotlin.daemon.jvmargs=-Dfile.encoding=UTF-8 \
--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \
--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED \
--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED \
--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED \
--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-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
# Kapt doesn't read the above jvm args when workers are used
# https://youtrack.jetbrains.com/issue/KT-48402
kapt.use.worker.api=false
kapt.include.compile.classpath=false
GROUP=com.squareup.moshi

View File

@@ -15,16 +15,17 @@
[versions]
autoService = "1.0"
gjf = "1.11.0"
incap = "0.3"
jvmTarget = "1.8"
kotlin = "1.5.21"
kotlinCompileTesting = "1.4.3"
kotlinpoet = "1.10.0"
kotlin = "1.5.31"
kotlinCompileTesting = "1.4.4"
kotlinpoet = "1.10.1"
ksp = "1.5.31-1.0.0"
ktlint = "0.41.0"
[plugins]
dokka = { id = "org.jetbrains.dokka", version = "1.5.0" }
dokka = { id = "org.jetbrains.dokka", version = "1.5.30" }
japicmp = { id = "me.champeau.gradle.japicmp", version = "0.2.9" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
mavenPublish = { id = "com.vanniktech.maven.publish", version = "0.17.0" }
mavenShadow = { id = "com.github.johnrengelman.shadow", version = "7.0.0" }
spotless = { id = "com.diffplug.spotless", version = "5.14.2" }
@@ -33,20 +34,22 @@ spotless = { id = "com.diffplug.spotless", version = "5.14.2" }
asm = "org.ow2.asm:asm:9.2"
autoCommon = "com.google.auto:auto-common:1.1"
autoService = { module = "com.google.auto.service:auto-service-annotations", version.ref = "autoService" }
autoService-processor = { module = "com.google.auto.service:auto-service", version.ref = "autoService" }
guava = { module = "com.google.guava:guava", version = "30.1.1-jre" }
incap = { module = "net.ltgt.gradle.incap:incap", version.ref = "incap" }
incap-processor = { module = "net.ltgt.gradle.incap:incap-processor", version.ref = "incap" }
autoService-ksp = "dev.zacsweers.autoservice:auto-service-ksp:1.0.0"
guava = "com.google.guava:guava:30.1.1-jre"
jsr305 = "com.google.code.findbugs:jsr305:3.0.2"
kotlin-compilerEmbeddable = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "kotlin" }
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet" }
kotlinpoet-metadata = { module = "com.squareup:kotlinpoet-metadata", version.ref = "kotlinpoet" }
kotlinpoet-ksp = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlinpoet" }
kotlinxMetadata = "org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.3.0"
ksp = { module = "com.google.devtools.ksp:symbol-processing", version.ref = "ksp" }
ksp-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" }
okio = "com.squareup.okio:okio:2.10.0"
# Test libs
assertj = "org.assertj:assertj-core:3.11.1"
junit = "junit:junit:4.13.2"
kotlinCompileTesting = { module = "com.github.tschuchortdev:kotlin-compile-testing", version.ref = "kotlinCompileTesting" }
kotlinCompileTesting-ksp = { module = "com.github.tschuchortdev:kotlin-compile-testing-ksp", version.ref ="kotlinCompileTesting" }
truth = "com.google.truth:truth:1.1.3"

View File

@@ -20,7 +20,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm")
kotlin("kapt")
alias(libs.plugins.ksp)
id("com.vanniktech.maven.publish")
alias(libs.plugins.mavenShadow)
}
@@ -31,12 +31,12 @@ tasks.withType<KotlinCompile>().configureEach {
freeCompilerArgs += listOf(
"-Xopt-in=kotlin.RequiresOptIn",
"-Xopt-in=com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview",
"-Xopt-in=com.squareup.kotlinpoet.ksp.KotlinPoetKspPreview",
)
}
}
tasks.withType<Test>().configureEach {
// For kapt to work with kotlin-compile-testing
jvmArgs(
"--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
@@ -47,17 +47,15 @@ tasks.withType<Test>().configureEach {
"--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",
"--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED"
)
}
val shade: Configuration = configurations.maybeCreate("compileShaded")
configurations.getByName("compileOnly").extendsFrom(shade)
dependencies {
// Use `api` because kapt will not resolve `runtime` dependencies without it, only `compile`
// https://youtrack.jetbrains.com/issue/KT-41702
api(project(":moshi"))
api(kotlin("reflect"))
implementation(project(":moshi"))
implementation(kotlin("reflect"))
shade(libs.kotlinxMetadata) {
exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib")
}
@@ -67,16 +65,30 @@ dependencies {
exclude(group = "com.squareup", module = "kotlinpoet")
exclude(group = "com.google.guava")
}
api(libs.guava)
api(libs.asm)
shade(libs.kotlinpoet.ksp) {
exclude(group = "org.jetbrains.kotlin")
exclude(group = "com.squareup", module = "kotlinpoet")
}
implementation(libs.guava)
implementation(libs.asm)
api(libs.autoService)
kapt(libs.autoService.processor)
api(libs.incap)
kapt(libs.incap.processor)
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.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)

View File

@@ -0,0 +1,43 @@
/*
* 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
internal 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)
*/
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.
*/
const val OPTION_GENERATE_PROGUARD_RULES: String = "moshi.generateProguardRules"
val POSSIBLE_GENERATED_NAMES = arrayOf(
ClassName("javax.annotation.processing", "Generated"),
ClassName("javax.annotation", "Generated")
).associateBy { it.canonicalName }
}

View File

@@ -16,9 +16,6 @@
package com.squareup.moshi.kotlin.codegen.api
import com.squareup.kotlinpoet.ClassName
import javax.annotation.processing.Filer
import javax.lang.model.element.Element
import javax.tools.StandardLocation
/**
* Represents a proguard configuration for a given spec. This covers three main areas:
@@ -42,16 +39,11 @@ internal data class ProguardConfig(
val targetConstructorParams: List<String>,
val qualifierProperties: Set<QualifierAdapterProperty>
) {
private val outputFile = "META-INF/proguard/moshi-${targetClass.canonicalName}.pro"
/** Writes this to `filer`. */
fun writeTo(filer: Filer, vararg originatingElements: Element) {
filer.createResource(StandardLocation.CLASS_OUTPUT, "", outputFile, *originatingElements)
.openWriter()
.use(::writeTo)
fun outputFilePathWithoutExtension(canonicalName: String): String {
return "META-INF/proguard/moshi-$canonicalName"
}
private fun writeTo(out: Appendable): Unit = out.run {
fun writeTo(out: Appendable): Unit = out.run {
//
// -if class {the target class}
// -keepnames class {the target class}

View File

@@ -217,8 +217,12 @@ internal fun List<TypeName>.toTypeVariableResolver(
// 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[id] = typeVar.deepCopy(null) { it.stripTypeVarVariance(resolver) }
parametersMap[typeVar.name] = typeVar.deepCopy(null) { it.stripTypeVarVariance(resolver) }
}
return resolver

View File

@@ -0,0 +1,77 @@
/*
* 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.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
internal fun TypeName.unwrapTypeAliasReal(): TypeName {
return tag<TypeAliasTag>()?.abbreviatedType?.let { unwrappedType ->
// If any type is nullable, then the whole thing is nullable
var isAnyNullable = isNullable
// 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)
isAnyNullable = isAnyNullable || nestedUnwrappedType.isNullable
nestedUnwrappedType.copy(nullable = isAnyNullable, annotations = runningAnnotations.toList())
} ?: this
}
internal fun TypeName.unwrapTypeAlias(): TypeName {
return when (this) {
is ClassName -> unwrapTypeAliasReal()
is ParameterizedTypeName -> {
if (TypeAliasTag::class in tags) {
unwrapTypeAliasReal()
} else {
deepCopy(TypeName::unwrapTypeAlias)
}
}
is TypeVariableName -> {
if (TypeAliasTag::class in tags) {
unwrapTypeAliasReal()
} else {
deepCopy(transform = TypeName::unwrapTypeAlias)
}
}
is WildcardTypeName -> {
if (TypeAliasTag::class in tags) {
unwrapTypeAliasReal()
} else {
deepCopy(TypeName::unwrapTypeAlias)
}
}
is LambdaTypeName -> {
if (TypeAliasTag::class in tags) {
unwrapTypeAliasReal()
} else {
deepCopy(TypeName::unwrapTypeAlias)
}
}
else -> throw UnsupportedOperationException("Type '${javaClass.simpleName}' is illegal. Only classes, parameterized types, wildcard types, or type variables are allowed.")
}
}

View File

@@ -21,9 +21,11 @@ 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.POSSIBLE_GENERATED_NAMES
import com.squareup.moshi.kotlin.codegen.api.ProguardConfig
import com.squareup.moshi.kotlin.codegen.api.PropertyGenerator
import net.ltgt.gradle.incap.IncrementalAnnotationProcessor
import net.ltgt.gradle.incap.IncrementalAnnotationProcessorType.ISOLATING
import javax.annotation.processing.AbstractProcessor
import javax.annotation.processing.Filer
import javax.annotation.processing.Messager
@@ -31,10 +33,12 @@ 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.
@@ -45,34 +49,8 @@ import javax.tools.Diagnostic
* adapter will also be internal).
*/
@AutoService(Processor::class)
@IncrementalAnnotationProcessor(ISOLATING)
public class JsonClassCodegenProcessor : AbstractProcessor() {
public companion object {
/**
* This annotation processing argument 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 control 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"
private val POSSIBLE_GENERATED_NAMES = arrayOf(
ClassName("javax.annotation.processing", "Generated"),
ClassName("javax.annotation", "Generated")
).associateBy { it.canonicalName }
}
private lateinit var types: Types
private lateinit var elements: Elements
private lateinit var filer: Filer
@@ -190,3 +168,10 @@ public class JsonClassCodegenProcessor : AbstractProcessor() {
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

@@ -19,12 +19,10 @@ import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.DelicateKotlinPoetApi
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.LambdaTypeName
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.WildcardTypeName
import com.squareup.kotlinpoet.asClassName
import com.squareup.kotlinpoet.asTypeName
import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview
@@ -37,7 +35,6 @@ 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.kotlinpoet.tags.TypeAliasTag
import com.squareup.moshi.Json
import com.squareup.moshi.JsonQualifier
import com.squareup.moshi.kotlin.codegen.api.DelegateKey
@@ -46,15 +43,14 @@ 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.deepCopy
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.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target
import java.util.TreeSet
import javax.annotation.processing.Messager
import javax.lang.model.element.AnnotationMirror
import javax.lang.model.element.Element
@@ -516,30 +512,6 @@ private fun String.escapeDollarSigns(): String {
return replace("\$", "\${\'\$\'}")
}
internal fun TypeName.unwrapTypeAlias(): TypeName {
return when (this) {
is ClassName -> {
tag<TypeAliasTag>()?.abbreviatedType?.let { unwrappedType ->
// If any type is nullable, then the whole thing is nullable
var isAnyNullable = isNullable
// 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)
isAnyNullable = isAnyNullable || nestedUnwrappedType.isNullable
nestedUnwrappedType.copy(nullable = isAnyNullable, annotations = runningAnnotations.toList())
} ?: this
}
is ParameterizedTypeName -> deepCopy(TypeName::unwrapTypeAlias)
is TypeVariableName -> deepCopy(transform = TypeName::unwrapTypeAlias)
is WildcardTypeName -> deepCopy(TypeName::unwrapTypeAlias)
is LambdaTypeName -> deepCopy(TypeName::unwrapTypeAlias)
else -> throw UnsupportedOperationException("Type '${javaClass.simpleName}' is illegal. Only classes, parameterized types, wildcard types, or type variables are allowed.")
}
}
internal val TypeElement.metadata: Metadata
get() {
return getAnnotation(Metadata::class.java)

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 supertypes of this, recursively. Includes both interface and class supertypes. */
fun supertypes(
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 {
fun get(type: KSClassDeclaration): AppliedType {
return AppliedType(type)
}
}
}

View File

@@ -0,0 +1,167 @@
/*
* 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.ksp.addOriginatingKSFile
import com.squareup.kotlinpoet.ksp.toClassName
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 {
val annotationType = resolver.getClassDeclarationByName(resolver.getKSNameFromString(it))
?: run {
logger.error("Generated annotation type doesn't exist: $it")
return emptyList()
}
AnnotationSpec.builder(annotationType.toClassName())
.addMember("value = [%S]", JsonClassSymbolProcessor::class.java.canonicalName)
.addMember("comments = %S", "https://github.com/square/moshi")
.build()
}
resolver.getSymbolsWithAnnotation(JSON_CLASS_NAME)
.asSequence()
.forEach { type ->
// For the smart cast
if (type !is KSDeclaration) {
logger.error("@JsonClass can't be applied to $type: must be a Kotlin class", type)
return@forEach
}
val jsonClassAnnotation = type.findAnnotationWithType<JsonClass>() ?: return@forEach
val generator = jsonClassAnnotation.generator
if (generator.isNotEmpty()) return@forEach
if (!jsonClassAnnotation.generateAdapter) return@forEach
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("[⇥⇥")
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,119 @@
/*
* 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.kotlinpoet.asClassName
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 VISIBILITY_MODIFIERS = setOf(
KModifier.INTERNAL,
KModifier.PRIVATE,
KModifier.PROTECTED,
KModifier.PUBLIC
)
internal fun Collection<KModifier>.visibility(): KModifier {
return find { it in VISIBILITY_MODIFIERS } ?: KModifier.PUBLIC
}
private val TargetProperty.isTransient get() = propertySpec.annotations.any { it.typeName == Transient::class.asClassName() }
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 (isTransient) {
if (!hasDefault) {
logger.error(
"No default value for transient 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"
)
}
}
annotationElement.findAnnotationWithType<Target>()?.let {
if (AnnotationTarget.FIELD !in it.allowedTargets) {
logger.error(
"JsonQualifier @${qualifierRawType.simpleName} must support FIELD target"
)
}
}
}
}
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 `element`, 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.get(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 (supertype in appliedType.supertypes(resolver)) {
val classDecl = supertype.type
if (!classDecl.isKotlinClass()) {
logger.error(
"""
@JsonClass can't be applied to $type: supertype $supertype 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
}
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]
result[name] = TargetProperty(
propertySpec = propertySpec,
parameter = parameter,
visibility = property.getVisibility().toKModifier() ?: KModifier.PUBLIC,
jsonName = parameter?.jsonName ?: property.jsonName()
?: name.escapeDollarSigns()
)
}
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 {
if (isAnnotationPresent(Transient::class)) {
addAnnotation(Transient::class)
}
addAnnotations(
this@toPropertySpec.annotations
.mapNotNull {
if ((it.annotationType.resolve().unwrapTypeAlias().declaration as KSClassDeclaration).isJsonQualifier
) {
it.toAnnotationSpec(resolver)
} else {
null
}
}
.asIterable()
)
}
.build()
}
private fun String.escapeDollarSigns(): String {
return replace("\$", "\${\'\$\'}")
}

View File

@@ -0,0 +1,185 @@
/*
* 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.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

@@ -18,6 +18,9 @@ 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
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
@@ -324,7 +327,6 @@ class JsonClassCodegenProcessorTest {
"""
import com.squareup.moshi.JsonClass
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
class NonPropertyConstructorParameter(a: Int, val b: Int)
"""
@@ -349,10 +351,10 @@ class JsonClassCodegenProcessorTest {
"""
)
).apply {
kaptArgs[JsonClassCodegenProcessor.OPTION_GENERATED] = "javax.annotation.GeneratedBlerg"
kaptArgs[OPTION_GENERATED] = "javax.annotation.GeneratedBlerg"
}.compile()
assertThat(result.messages).contains(
"Invalid option value for ${JsonClassCodegenProcessor.OPTION_GENERATED}"
"Invalid option value for $OPTION_GENERATED"
)
}
@Test
@@ -368,7 +370,7 @@ class JsonClassCodegenProcessorTest {
"""
)
).apply {
kaptArgs[JsonClassCodegenProcessor.OPTION_GENERATE_PROGUARD_RULES] = "false"
kaptArgs[OPTION_GENERATE_PROGUARD_RULES] = "false"
}.compile()
assertThat(result.generatedFiles.filter { it.endsWith(".pro") }).isEmpty()
}

View File

@@ -0,0 +1,824 @@
/*
* 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
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
/** 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 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")
// TODO we throw eagerly currently and don't collect
// 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)
"""
)
)
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains("JsonQualifier @UpperCase must support FIELD target")
}
@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);
private com.squareup.moshi.JsonAdapter stringAtMyQualifierAdapter;
}
-if class testPackage.UsingQualifiers
-keep @interface testPackage.MyQualifier
""".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[]);
private com.squareup.moshi.JsonAdapter mutableListOfStringAtMyQualifierAdapter;
}
-if class testPackage.Complex
-keep @interface testPackage.MyQualifier
-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);
private com.squareup.moshi.JsonAdapter stringAtNestedQualifierAdapter;
}
-if class testPackage.NestedType${'$'}NestedSimple
-keep @interface testPackage.NestedType${'$'}NestedQualifier
""".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()
}
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

@@ -18,7 +18,15 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm")
kotlin("kapt")
kotlin("kapt") apply false
alias(libs.plugins.ksp) apply false
}
val useKsp = hasProperty("useKsp")
if (useKsp) {
apply(plugin = "com.google.devtools.ksp")
} else {
apply(plugin = "org.jetbrains.kotlin.kapt")
}
tasks.withType<Test>().configureEach {
@@ -37,9 +45,14 @@ tasks.withType<KotlinCompile>().configureEach {
}
dependencies {
kaptTest(project(":kotlin:codegen"))
if (useKsp) {
"kspTest"(project(":kotlin:codegen"))
} else {
"kaptTest"(project(":kotlin:codegen"))
}
testImplementation(project(":moshi"))
testImplementation(project(":kotlin:reflect"))
testImplementation(project(":kotlin:tests:extra-moshi-test-module"))
testImplementation(kotlin("reflect"))
testImplementation(libs.junit)
testImplementation(libs.assertj)

View File

@@ -0,0 +1,19 @@
/*
* 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.
*/
plugins {
kotlin("jvm")
}

View File

@@ -0,0 +1,18 @@
/*
* 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.test.extra
public abstract class AbstractClassInModuleA

View File

@@ -22,12 +22,13 @@ import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonAdapter.Factory
import com.squareup.moshi.JsonClass
import com.squareup.moshi.JsonDataException
import com.squareup.moshi.JsonQualifier
import com.squareup.moshi.Moshi
import com.squareup.moshi.ToJson
import com.squareup.moshi.Types
import com.squareup.moshi.adapter
import com.squareup.moshi.kotlin.codegen.test.extra.AbstractClassInModuleA
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.squareup.moshi.rawType
import com.squareup.moshi.supertypeOf
import org.intellij.lang.annotations.Language
import org.junit.Assert.fail
import org.junit.Test
@@ -63,11 +64,11 @@ class DualKotlinTest(useReflection: Boolean) {
object : Factory {
override fun create(
type: Type,
annotations: Set<Annotation>,
annotations: MutableSet<out Annotation>,
moshi: Moshi
): JsonAdapter<*>? {
// Prevent falling back to generated adapter lookup
val rawType = type.rawType
val rawType = Types.getRawType(type)
val metadataClass = Class.forName("kotlin.Metadata") as Class<out Annotation>
check(rawType.isEnum || !rawType.isAnnotationPresent(metadataClass)) {
"Unhandled Kotlin type in reflective test! $rawType"
@@ -248,21 +249,21 @@ class DualKotlinTest(useReflection: Boolean) {
val hasNonNullConstructorParameterAdapter =
localMoshi.adapter<HasNonNullConstructorParameter>()
assertThat(
hasNonNullConstructorParameterAdapter
//language=JSON
hasNonNullConstructorParameterAdapter
.fromJson("{\"a\":null}")
).isEqualTo(HasNonNullConstructorParameter("fallback"))
val hasNullableConstructorParameterAdapter =
localMoshi.adapter<HasNullableConstructorParameter>()
assertThat(
hasNullableConstructorParameterAdapter
//language=JSON
hasNullableConstructorParameterAdapter
.fromJson("{\"a\":null}")
).isEqualTo(HasNullableConstructorParameter("fallback"))
//language=JSON
assertThat(
hasNullableConstructorParameterAdapter
//language=JSON
.toJson(HasNullableConstructorParameter(null))
).isEqualTo("{\"a\":\"fallback\"}")
}
@@ -284,7 +285,7 @@ class DualKotlinTest(useReflection: Boolean) {
assertThat(decoded.a).isEqualTo(null)
}
@Test fun inlineClass() {
@Test fun valueClass() {
val adapter = moshi.adapter<ValueClass>()
val inline = ValueClass(5)
@@ -297,6 +298,13 @@ class DualKotlinTest(useReflection: Boolean) {
"""{"i":6}"""
val result = adapter.fromJson(testJson)!!
assertThat(result.i).isEqualTo(6)
// TODO doesn't work yet.
// need to invoke the constructor_impl$default static method, invoke constructor with result
// val testEmptyJson =
// """{}"""
// val result2 = adapter.fromJson(testEmptyJson)!!
// assertThat(result2.i).isEqualTo(0)
}
@JsonClass(generateAdapter = true)
@@ -339,6 +347,50 @@ class DualKotlinTest(useReflection: Boolean) {
abstract class Asset<A : Asset<A>>
abstract class AssetMetaData<A : Asset<A>>
// Regression test for https://github.com/ZacSweers/MoshiX/issues/125
@Test fun selfReferencingTypeVars() {
val adapter = moshi.adapter<StringNodeNumberNode>()
val data = StringNodeNumberNode().also {
it.t = StringNodeNumberNode().also {
it.text = "child 1"
}
it.text = "root"
it.r = NumberStringNode().also {
it.number = 0
it.t = NumberStringNode().also {
it.number = 1
}
it.r = StringNodeNumberNode().also {
it.text = "grand child 1"
}
}
}
assertThat(adapter.toJson(data))
//language=JSON
.isEqualTo(
"""
{"text":"root","t":{"text":"child 1"},"r":{"number":0,"t":{"number":1},"r":{"text":"grand child 1"}}}
""".trimIndent()
)
}
@JsonClass(generateAdapter = true)
open class Node<T : Node<T, R>, R : Node<R, T>> {
var t: T? = null
var r: R? = null
}
@JsonClass(generateAdapter = true)
class StringNodeNumberNode : Node<StringNodeNumberNode, NumberStringNode>() {
var text: String = ""
}
@JsonClass(generateAdapter = true)
class NumberStringNode : Node<NumberStringNode, StringNodeNumberNode>() {
var number: Int = 0
}
// Regression test for https://github.com/square/moshi/issues/968
@Test fun abstractSuperProperties() {
val adapter = moshi.adapter<InternalAbstractProperty>()
@@ -447,7 +499,7 @@ class DualKotlinTest(useReflection: Boolean) {
@Test fun typeAliasUnwrapping() {
val adapter = moshi
.newBuilder()
.add(supertypeOf<Int>(), moshi.adapter<Int>())
.add(Types.supertypeOf(Int::class.javaObjectType), moshi.adapter<Int>())
.build()
.adapter<TypeAliasUnwrapping>()
@@ -587,8 +639,7 @@ class DualKotlinTest(useReflection: Boolean) {
@JsonClass(generateAdapter = true)
data class OutDeclaration<out T>(val input: T)
// Regression test for https://github.com/square/moshi/issues/1244
@Test fun backwardReferencingTypeVarsAndIntersectionTypes() {
@Test fun intersectionTypes() {
val adapter = moshi.adapter<IntersectionTypes<IntersectionTypesEnum>>()
@Language("JSON")
@@ -625,7 +676,7 @@ data class GenericClass<T>(val value: T)
// Has to be outside since value classes are only allowed on top level
@JvmInline
@JsonClass(generateAdapter = true)
value class ValueClass(val i: Int)
value class ValueClass(val i: Int = 0)
typealias A = Int
typealias NullableA = A?
@@ -634,3 +685,23 @@ typealias NullableB = B?
typealias C = NullableA
typealias D = C
typealias E = D
// Regression test for enum constants in annotations and array types
// https://github.com/ZacSweers/MoshiX/issues/103
@Retention(RUNTIME)
@JsonQualifier
annotation class UpperCase(val foo: Array<Foo>)
enum class Foo { BAR }
@JsonClass(generateAdapter = true)
data class ClassWithQualifier(
@UpperCase(foo = [Foo.BAR])
val a: Int
)
// Regression for https://github.com/ZacSweers/MoshiX/issues/120
@JsonClass(generateAdapter = true)
data class DataClassInModuleB(
val id: String
) : AbstractClassInModuleA()

View File

@@ -0,0 +1,49 @@
/*
* 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
import com.google.common.truth.Truth.assertThat
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter
import org.junit.Test
// Regression tests specific to Moshi-KSP
class MoshiKspTest {
private val moshi = Moshi.Builder().build()
// Regression test for https://github.com/ZacSweers/MoshiX/issues/44
@Test
fun onlyInterfaceSupertypes() {
val adapter = moshi.adapter<SimpleImpl>()
//language=JSON
val json = """{"a":"aValue","b":"bValue"}"""
val expected = SimpleImpl("aValue", "bValue")
val instance = adapter.fromJson(json)!!
assertThat(instance).isEqualTo(expected)
val encoded = adapter.toJson(instance)
assertThat(encoded).isEqualTo(json)
}
interface SimpleInterface {
val a: String
}
// NOTE the Any() superclass is important to test that we're detecting the farthest parent class
// correct.y
@JsonClass(generateAdapter = true)
data class SimpleImpl(override val a: String, val b: String) : Any(), SimpleInterface
}

View File

@@ -31,5 +31,6 @@ include(":examples")
include(":kotlin:reflect")
include(":kotlin:codegen")
include(":kotlin:tests")
include(":kotlin:tests:extra-moshi-test-module")
enableFeaturePreview("VERSION_CATALOGS")