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: jobs:
build: build:
name: 'Java ${{ matrix.java-version }} | KSP ${{ matrix.use-ksp }}'
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
@@ -11,6 +12,7 @@ jobs:
matrix: matrix:
java-version: java-version:
- 16 - 16
use-ksp: [ true, false ]
steps: steps:
- name: Checkout - name: Checkout
@@ -36,7 +38,7 @@ jobs:
java-version: ${{ matrix.java-version }} java-version: ${{ matrix.java-version }}
- name: Test - name: Test
run: ./gradlew build check --stacktrace run: ./gradlew build check --stacktrace -PuseKsp=${{ matrix.use-ksp }}
- name: Publish (default branch only) - name: Publish (default branch only)
if: github.repository == 'square/moshi' && github.ref == 'refs/heads/master' && matrix.java-version == '16' 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 # For Dokka https://github.com/Kotlin/dokka/issues/1405
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 \ 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.api=ALL-UNNAMED \
--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \ --add-opens=jdk.compiler/com.sun.tools.javac.code=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.comp=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.tree=ALL-UNNAMED \
--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \ --add-opens=jdk.compiler/com.sun.tools.javac.util=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
# TODO move this to DSL in Kotlin 1.5.30 https://youtrack.jetbrains.com/issue/KT-44266
kotlin.daemon.jvmargs=-Dfile.encoding=UTF-8 \ 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.api=ALL-UNNAMED \
--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \ --add-opens=jdk.compiler/com.sun.tools.javac.code=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.comp=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.tree=ALL-UNNAMED \
--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \ --add-opens=jdk.compiler/com.sun.tools.javac.util=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
# 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 kapt.include.compile.classpath=false
GROUP=com.squareup.moshi GROUP=com.squareup.moshi

View File

@@ -15,16 +15,17 @@
[versions] [versions]
autoService = "1.0" autoService = "1.0"
gjf = "1.11.0" gjf = "1.11.0"
incap = "0.3"
jvmTarget = "1.8" jvmTarget = "1.8"
kotlin = "1.5.21" kotlin = "1.5.31"
kotlinCompileTesting = "1.4.3" kotlinCompileTesting = "1.4.4"
kotlinpoet = "1.10.0" kotlinpoet = "1.10.1"
ksp = "1.5.31-1.0.0"
ktlint = "0.41.0" ktlint = "0.41.0"
[plugins] [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" } 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" } mavenPublish = { id = "com.vanniktech.maven.publish", version = "0.17.0" }
mavenShadow = { id = "com.github.johnrengelman.shadow", version = "7.0.0" } mavenShadow = { id = "com.github.johnrengelman.shadow", version = "7.0.0" }
spotless = { id = "com.diffplug.spotless", version = "5.14.2" } 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" asm = "org.ow2.asm:asm:9.2"
autoCommon = "com.google.auto:auto-common:1.1" autoCommon = "com.google.auto:auto-common:1.1"
autoService = { module = "com.google.auto.service:auto-service-annotations", version.ref = "autoService" } autoService = { module = "com.google.auto.service:auto-service-annotations", version.ref = "autoService" }
autoService-processor = { module = "com.google.auto.service:auto-service", version.ref = "autoService" } autoService-ksp = "dev.zacsweers.autoservice:auto-service-ksp:1.0.0"
guava = { module = "com.google.guava:guava", version = "30.1.1-jre" } guava = "com.google.guava:guava: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" }
jsr305 = "com.google.code.findbugs:jsr305:3.0.2" jsr305 = "com.google.code.findbugs:jsr305:3.0.2"
kotlin-compilerEmbeddable = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "kotlin" } kotlin-compilerEmbeddable = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "kotlin" }
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet" } kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet" }
kotlinpoet-metadata = { module = "com.squareup:kotlinpoet-metadata", 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" 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" okio = "com.squareup.okio:okio:2.10.0"
# Test libs # Test libs
assertj = "org.assertj:assertj-core:3.11.1" assertj = "org.assertj:assertj-core:3.11.1"
junit = "junit:junit:4.13.2" junit = "junit:junit:4.13.2"
kotlinCompileTesting = { module = "com.github.tschuchortdev:kotlin-compile-testing", version.ref = "kotlinCompileTesting" } 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" truth = "com.google.truth:truth:1.1.3"

View File

@@ -20,7 +20,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
kotlin("jvm") kotlin("jvm")
kotlin("kapt") alias(libs.plugins.ksp)
id("com.vanniktech.maven.publish") id("com.vanniktech.maven.publish")
alias(libs.plugins.mavenShadow) alias(libs.plugins.mavenShadow)
} }
@@ -31,12 +31,12 @@ tasks.withType<KotlinCompile>().configureEach {
freeCompilerArgs += listOf( freeCompilerArgs += listOf(
"-Xopt-in=kotlin.RequiresOptIn", "-Xopt-in=kotlin.RequiresOptIn",
"-Xopt-in=com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview", "-Xopt-in=com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview",
"-Xopt-in=com.squareup.kotlinpoet.ksp.KotlinPoetKspPreview",
) )
} }
} }
tasks.withType<Test>().configureEach { tasks.withType<Test>().configureEach {
// For kapt to work with kotlin-compile-testing
jvmArgs( jvmArgs(
"--add-opens=jdk.compiler/com.sun.tools.javac.api=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.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.parser=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.processing=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.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") val shade: Configuration = configurations.maybeCreate("compileShaded")
configurations.getByName("compileOnly").extendsFrom(shade) configurations.getByName("compileOnly").extendsFrom(shade)
dependencies { dependencies {
// Use `api` because kapt will not resolve `runtime` dependencies without it, only `compile` implementation(project(":moshi"))
// https://youtrack.jetbrains.com/issue/KT-41702 implementation(kotlin("reflect"))
api(project(":moshi"))
api(kotlin("reflect"))
shade(libs.kotlinxMetadata) { shade(libs.kotlinxMetadata) {
exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib") exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib")
} }
@@ -67,16 +65,30 @@ dependencies {
exclude(group = "com.squareup", module = "kotlinpoet") exclude(group = "com.squareup", module = "kotlinpoet")
exclude(group = "com.google.guava") exclude(group = "com.google.guava")
} }
api(libs.guava) shade(libs.kotlinpoet.ksp) {
api(libs.asm) exclude(group = "org.jetbrains.kotlin")
exclude(group = "com.squareup", module = "kotlinpoet")
}
implementation(libs.guava)
implementation(libs.asm)
api(libs.autoService) implementation(libs.autoService)
kapt(libs.autoService.processor) ksp(libs.autoService.ksp)
api(libs.incap)
kapt(libs.incap.processor) // 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 // 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.metadata)
testImplementation(libs.kotlinpoet.ksp)
testImplementation(libs.junit) testImplementation(libs.junit)
testImplementation(libs.truth) testImplementation(libs.truth)
testImplementation(libs.kotlinCompileTesting) 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 package com.squareup.moshi.kotlin.codegen.api
import com.squareup.kotlinpoet.ClassName 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: * 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 targetConstructorParams: List<String>,
val qualifierProperties: Set<QualifierAdapterProperty> val qualifierProperties: Set<QualifierAdapterProperty>
) { ) {
private val outputFile = "META-INF/proguard/moshi-${targetClass.canonicalName}.pro" fun outputFilePathWithoutExtension(canonicalName: String): String {
return "META-INF/proguard/moshi-$canonicalName"
/** Writes this to `filer`. */
fun writeTo(filer: Filer, vararg originatingElements: Element) {
filer.createResource(StandardLocation.CLASS_OUTPUT, "", outputFile, *originatingElements)
.openWriter()
.use(::writeTo)
} }
private fun writeTo(out: Appendable): Unit = out.run { fun writeTo(out: Appendable): Unit = out.run {
// //
// -if class {the target class} // -if class {the target class}
// -keepnames 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. // replacement later that may add bounds referencing this.
val id = typeVar.name val id = typeVar.name
parametersMap[id] = TypeVariableName(id) parametersMap[id] = TypeVariableName(id)
}
for (typeVar in this) {
check(typeVar is TypeVariableName)
// Now replace it with the full version. // 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 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.kotlinpoet.metadata.classinspectors.ElementsClassInspector
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.squareup.moshi.kotlin.codegen.api.AdapterGenerator 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 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.AbstractProcessor
import javax.annotation.processing.Filer import javax.annotation.processing.Filer
import javax.annotation.processing.Messager import javax.annotation.processing.Messager
@@ -31,10 +33,12 @@ import javax.annotation.processing.ProcessingEnvironment
import javax.annotation.processing.Processor import javax.annotation.processing.Processor
import javax.annotation.processing.RoundEnvironment import javax.annotation.processing.RoundEnvironment
import javax.lang.model.SourceVersion import javax.lang.model.SourceVersion
import javax.lang.model.element.Element
import javax.lang.model.element.TypeElement import javax.lang.model.element.TypeElement
import javax.lang.model.util.Elements import javax.lang.model.util.Elements
import javax.lang.model.util.Types import javax.lang.model.util.Types
import javax.tools.Diagnostic import javax.tools.Diagnostic
import javax.tools.StandardLocation
/** /**
* An annotation processor that reads Kotlin data classes and generates Moshi JsonAdapters for them. * 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). * adapter will also be internal).
*/ */
@AutoService(Processor::class) @AutoService(Processor::class)
@IncrementalAnnotationProcessor(ISOLATING)
public class JsonClassCodegenProcessor : AbstractProcessor() { 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 types: Types
private lateinit var elements: Elements private lateinit var elements: Elements
private lateinit var filer: Filer private lateinit var filer: Filer
@@ -190,3 +168,10 @@ public class JsonClassCodegenProcessor : AbstractProcessor() {
return AdapterGenerator(type, sortedProperties) 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.ClassName
import com.squareup.kotlinpoet.DelicateKotlinPoetApi import com.squareup.kotlinpoet.DelicateKotlinPoetApi
import com.squareup.kotlinpoet.KModifier import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.LambdaTypeName
import com.squareup.kotlinpoet.ParameterizedTypeName import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.TypeName import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeSpec import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.TypeVariableName import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.WildcardTypeName
import com.squareup.kotlinpoet.asClassName import com.squareup.kotlinpoet.asClassName
import com.squareup.kotlinpoet.asTypeName import com.squareup.kotlinpoet.asTypeName
import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview 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.isPublic
import com.squareup.kotlinpoet.metadata.isSealed import com.squareup.kotlinpoet.metadata.isSealed
import com.squareup.kotlinpoet.tag import com.squareup.kotlinpoet.tag
import com.squareup.kotlinpoet.tags.TypeAliasTag
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonQualifier import com.squareup.moshi.JsonQualifier
import com.squareup.moshi.kotlin.codegen.api.DelegateKey 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.TargetParameter
import com.squareup.moshi.kotlin.codegen.api.TargetProperty import com.squareup.moshi.kotlin.codegen.api.TargetProperty
import com.squareup.moshi.kotlin.codegen.api.TargetType 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.rawType
import com.squareup.moshi.kotlin.codegen.api.unwrapTypeAlias
import kotlinx.metadata.KmConstructor import kotlinx.metadata.KmConstructor
import kotlinx.metadata.jvm.signature import kotlinx.metadata.jvm.signature
import java.lang.annotation.ElementType import java.lang.annotation.ElementType
import java.lang.annotation.Retention import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target import java.lang.annotation.Target
import java.util.TreeSet
import javax.annotation.processing.Messager import javax.annotation.processing.Messager
import javax.lang.model.element.AnnotationMirror import javax.lang.model.element.AnnotationMirror
import javax.lang.model.element.Element import javax.lang.model.element.Element
@@ -516,30 +512,6 @@ private fun String.escapeDollarSigns(): String {
return replace("\$", "\${\'\$\'}") 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 internal val TypeElement.metadata: Metadata
get() { get() {
return getAnnotation(Metadata::class.java) 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.google.common.truth.Truth.assertThat
import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonReader 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.KotlinCompilation
import com.tschuchort.compiletesting.SourceFile import com.tschuchort.compiletesting.SourceFile
import com.tschuchort.compiletesting.SourceFile.Companion.kotlin import com.tschuchort.compiletesting.SourceFile.Companion.kotlin
@@ -324,7 +327,6 @@ class JsonClassCodegenProcessorTest {
""" """
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
class NonPropertyConstructorParameter(a: Int, val b: Int) class NonPropertyConstructorParameter(a: Int, val b: Int)
""" """
@@ -349,10 +351,10 @@ class JsonClassCodegenProcessorTest {
""" """
) )
).apply { ).apply {
kaptArgs[JsonClassCodegenProcessor.OPTION_GENERATED] = "javax.annotation.GeneratedBlerg" kaptArgs[OPTION_GENERATED] = "javax.annotation.GeneratedBlerg"
}.compile() }.compile()
assertThat(result.messages).contains( assertThat(result.messages).contains(
"Invalid option value for ${JsonClassCodegenProcessor.OPTION_GENERATED}" "Invalid option value for $OPTION_GENERATED"
) )
} }
@Test @Test
@@ -368,7 +370,7 @@ class JsonClassCodegenProcessorTest {
""" """
) )
).apply { ).apply {
kaptArgs[JsonClassCodegenProcessor.OPTION_GENERATE_PROGUARD_RULES] = "false" kaptArgs[OPTION_GENERATE_PROGUARD_RULES] = "false"
}.compile() }.compile()
assertThat(result.generatedFiles.filter { it.endsWith(".pro") }).isEmpty() 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 { plugins {
kotlin("jvm") 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 { tasks.withType<Test>().configureEach {
@@ -37,9 +45,14 @@ tasks.withType<KotlinCompile>().configureEach {
} }
dependencies { dependencies {
kaptTest(project(":kotlin:codegen")) if (useKsp) {
"kspTest"(project(":kotlin:codegen"))
} else {
"kaptTest"(project(":kotlin:codegen"))
}
testImplementation(project(":moshi")) testImplementation(project(":moshi"))
testImplementation(project(":kotlin:reflect")) testImplementation(project(":kotlin:reflect"))
testImplementation(project(":kotlin:tests:extra-moshi-test-module"))
testImplementation(kotlin("reflect")) testImplementation(kotlin("reflect"))
testImplementation(libs.junit) testImplementation(libs.junit)
testImplementation(libs.assertj) 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.JsonAdapter.Factory
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.squareup.moshi.JsonDataException import com.squareup.moshi.JsonDataException
import com.squareup.moshi.JsonQualifier
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.squareup.moshi.ToJson import com.squareup.moshi.ToJson
import com.squareup.moshi.Types
import com.squareup.moshi.adapter 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.kotlin.reflect.KotlinJsonAdapterFactory
import com.squareup.moshi.rawType
import com.squareup.moshi.supertypeOf
import org.intellij.lang.annotations.Language import org.intellij.lang.annotations.Language
import org.junit.Assert.fail import org.junit.Assert.fail
import org.junit.Test import org.junit.Test
@@ -63,11 +64,11 @@ class DualKotlinTest(useReflection: Boolean) {
object : Factory { object : Factory {
override fun create( override fun create(
type: Type, type: Type,
annotations: Set<Annotation>, annotations: MutableSet<out Annotation>,
moshi: Moshi moshi: Moshi
): JsonAdapter<*>? { ): JsonAdapter<*>? {
// Prevent falling back to generated adapter lookup // 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> val metadataClass = Class.forName("kotlin.Metadata") as Class<out Annotation>
check(rawType.isEnum || !rawType.isAnnotationPresent(metadataClass)) { check(rawType.isEnum || !rawType.isAnnotationPresent(metadataClass)) {
"Unhandled Kotlin type in reflective test! $rawType" "Unhandled Kotlin type in reflective test! $rawType"
@@ -248,21 +249,21 @@ class DualKotlinTest(useReflection: Boolean) {
val hasNonNullConstructorParameterAdapter = val hasNonNullConstructorParameterAdapter =
localMoshi.adapter<HasNonNullConstructorParameter>() localMoshi.adapter<HasNonNullConstructorParameter>()
assertThat( assertThat(
//language=JSON
hasNonNullConstructorParameterAdapter hasNonNullConstructorParameterAdapter
//language=JSON
.fromJson("{\"a\":null}") .fromJson("{\"a\":null}")
).isEqualTo(HasNonNullConstructorParameter("fallback")) ).isEqualTo(HasNonNullConstructorParameter("fallback"))
val hasNullableConstructorParameterAdapter = val hasNullableConstructorParameterAdapter =
localMoshi.adapter<HasNullableConstructorParameter>() localMoshi.adapter<HasNullableConstructorParameter>()
assertThat( assertThat(
//language=JSON
hasNullableConstructorParameterAdapter hasNullableConstructorParameterAdapter
//language=JSON
.fromJson("{\"a\":null}") .fromJson("{\"a\":null}")
).isEqualTo(HasNullableConstructorParameter("fallback")) ).isEqualTo(HasNullableConstructorParameter("fallback"))
//language=JSON
assertThat( assertThat(
hasNullableConstructorParameterAdapter hasNullableConstructorParameterAdapter
//language=JSON
.toJson(HasNullableConstructorParameter(null)) .toJson(HasNullableConstructorParameter(null))
).isEqualTo("{\"a\":\"fallback\"}") ).isEqualTo("{\"a\":\"fallback\"}")
} }
@@ -284,7 +285,7 @@ class DualKotlinTest(useReflection: Boolean) {
assertThat(decoded.a).isEqualTo(null) assertThat(decoded.a).isEqualTo(null)
} }
@Test fun inlineClass() { @Test fun valueClass() {
val adapter = moshi.adapter<ValueClass>() val adapter = moshi.adapter<ValueClass>()
val inline = ValueClass(5) val inline = ValueClass(5)
@@ -297,6 +298,13 @@ class DualKotlinTest(useReflection: Boolean) {
"""{"i":6}""" """{"i":6}"""
val result = adapter.fromJson(testJson)!! val result = adapter.fromJson(testJson)!!
assertThat(result.i).isEqualTo(6) 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) @JsonClass(generateAdapter = true)
@@ -339,6 +347,50 @@ class DualKotlinTest(useReflection: Boolean) {
abstract class Asset<A : Asset<A>> abstract class Asset<A : Asset<A>>
abstract class AssetMetaData<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 // Regression test for https://github.com/square/moshi/issues/968
@Test fun abstractSuperProperties() { @Test fun abstractSuperProperties() {
val adapter = moshi.adapter<InternalAbstractProperty>() val adapter = moshi.adapter<InternalAbstractProperty>()
@@ -447,7 +499,7 @@ class DualKotlinTest(useReflection: Boolean) {
@Test fun typeAliasUnwrapping() { @Test fun typeAliasUnwrapping() {
val adapter = moshi val adapter = moshi
.newBuilder() .newBuilder()
.add(supertypeOf<Int>(), moshi.adapter<Int>()) .add(Types.supertypeOf(Int::class.javaObjectType), moshi.adapter<Int>())
.build() .build()
.adapter<TypeAliasUnwrapping>() .adapter<TypeAliasUnwrapping>()
@@ -587,8 +639,7 @@ class DualKotlinTest(useReflection: Boolean) {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class OutDeclaration<out T>(val input: T) data class OutDeclaration<out T>(val input: T)
// Regression test for https://github.com/square/moshi/issues/1244 @Test fun intersectionTypes() {
@Test fun backwardReferencingTypeVarsAndIntersectionTypes() {
val adapter = moshi.adapter<IntersectionTypes<IntersectionTypesEnum>>() val adapter = moshi.adapter<IntersectionTypes<IntersectionTypesEnum>>()
@Language("JSON") @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 // Has to be outside since value classes are only allowed on top level
@JvmInline @JvmInline
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
value class ValueClass(val i: Int) value class ValueClass(val i: Int = 0)
typealias A = Int typealias A = Int
typealias NullableA = A? typealias NullableA = A?
@@ -634,3 +685,23 @@ typealias NullableB = B?
typealias C = NullableA typealias C = NullableA
typealias D = C typealias D = C
typealias E = D 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:reflect")
include(":kotlin:codegen") include(":kotlin:codegen")
include(":kotlin:tests") include(":kotlin:tests")
include(":kotlin:tests:extra-moshi-test-module")
enableFeaturePreview("VERSION_CATALOGS") enableFeaturePreview("VERSION_CATALOGS")