mirror of
https://github.com/fankes/moshi.git
synced 2025-10-18 23:49:21 +08:00
Upstream KSP implementation (#1393)
This commit is contained in:
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -4,6 +4,7 @@ on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: 'Java ${{ matrix.java-version }} | KSP ${{ matrix.use-ksp }}'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
@@ -11,6 +12,7 @@ jobs:
|
||||
matrix:
|
||||
java-version:
|
||||
- 16
|
||||
use-ksp: [ true, false ]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -36,7 +38,7 @@ jobs:
|
||||
java-version: ${{ matrix.java-version }}
|
||||
|
||||
- name: Test
|
||||
run: ./gradlew build check --stacktrace
|
||||
run: ./gradlew build check --stacktrace -PuseKsp=${{ matrix.use-ksp }}
|
||||
|
||||
- name: Publish (default branch only)
|
||||
if: github.repository == 'square/moshi' && github.ref == 'refs/heads/master' && matrix.java-version == '16'
|
||||
|
@@ -16,34 +16,32 @@
|
||||
|
||||
# For Dokka https://github.com/Kotlin/dokka/issues/1405
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 \
|
||||
--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \
|
||||
--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
|
||||
--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED \
|
||||
--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED \
|
||||
--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED \
|
||||
--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
|
||||
--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED \
|
||||
--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED \
|
||||
--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
|
||||
--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED \
|
||||
--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED \
|
||||
--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
|
||||
--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED \
|
||||
--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
|
||||
--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
|
||||
--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
|
||||
--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
|
||||
--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
|
||||
--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
|
||||
--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
|
||||
|
||||
# TODO move this to DSL in Kotlin 1.5.30 https://youtrack.jetbrains.com/issue/KT-44266
|
||||
kotlin.daemon.jvmargs=-Dfile.encoding=UTF-8 \
|
||||
--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \
|
||||
--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
|
||||
--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED \
|
||||
--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED \
|
||||
--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED \
|
||||
--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
|
||||
--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED \
|
||||
--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED \
|
||||
--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
|
||||
--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED \
|
||||
--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED \
|
||||
--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
|
||||
--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED \
|
||||
--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
|
||||
--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
|
||||
--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
|
||||
--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
|
||||
--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
|
||||
--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
|
||||
--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
|
||||
|
||||
# Kapt doesn't read the above jvm args when workers are used
|
||||
# https://youtrack.jetbrains.com/issue/KT-48402
|
||||
kapt.use.worker.api=false
|
||||
kapt.include.compile.classpath=false
|
||||
|
||||
GROUP=com.squareup.moshi
|
||||
|
@@ -15,16 +15,17 @@
|
||||
[versions]
|
||||
autoService = "1.0"
|
||||
gjf = "1.11.0"
|
||||
incap = "0.3"
|
||||
jvmTarget = "1.8"
|
||||
kotlin = "1.5.21"
|
||||
kotlinCompileTesting = "1.4.3"
|
||||
kotlinpoet = "1.10.0"
|
||||
kotlin = "1.5.31"
|
||||
kotlinCompileTesting = "1.4.4"
|
||||
kotlinpoet = "1.10.1"
|
||||
ksp = "1.5.31-1.0.0"
|
||||
ktlint = "0.41.0"
|
||||
|
||||
[plugins]
|
||||
dokka = { id = "org.jetbrains.dokka", version = "1.5.0" }
|
||||
dokka = { id = "org.jetbrains.dokka", version = "1.5.30" }
|
||||
japicmp = { id = "me.champeau.gradle.japicmp", version = "0.2.9" }
|
||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||
mavenPublish = { id = "com.vanniktech.maven.publish", version = "0.17.0" }
|
||||
mavenShadow = { id = "com.github.johnrengelman.shadow", version = "7.0.0" }
|
||||
spotless = { id = "com.diffplug.spotless", version = "5.14.2" }
|
||||
@@ -33,20 +34,22 @@ spotless = { id = "com.diffplug.spotless", version = "5.14.2" }
|
||||
asm = "org.ow2.asm:asm:9.2"
|
||||
autoCommon = "com.google.auto:auto-common:1.1"
|
||||
autoService = { module = "com.google.auto.service:auto-service-annotations", version.ref = "autoService" }
|
||||
autoService-processor = { module = "com.google.auto.service:auto-service", version.ref = "autoService" }
|
||||
guava = { module = "com.google.guava:guava", version = "30.1.1-jre" }
|
||||
incap = { module = "net.ltgt.gradle.incap:incap", version.ref = "incap" }
|
||||
incap-processor = { module = "net.ltgt.gradle.incap:incap-processor", version.ref = "incap" }
|
||||
autoService-ksp = "dev.zacsweers.autoservice:auto-service-ksp:1.0.0"
|
||||
guava = "com.google.guava:guava:30.1.1-jre"
|
||||
jsr305 = "com.google.code.findbugs:jsr305:3.0.2"
|
||||
kotlin-compilerEmbeddable = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "kotlin" }
|
||||
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
|
||||
kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet" }
|
||||
kotlinpoet-metadata = { module = "com.squareup:kotlinpoet-metadata", version.ref = "kotlinpoet" }
|
||||
kotlinpoet-ksp = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlinpoet" }
|
||||
kotlinxMetadata = "org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.3.0"
|
||||
ksp = { module = "com.google.devtools.ksp:symbol-processing", version.ref = "ksp" }
|
||||
ksp-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" }
|
||||
okio = "com.squareup.okio:okio:2.10.0"
|
||||
|
||||
# Test libs
|
||||
assertj = "org.assertj:assertj-core:3.11.1"
|
||||
junit = "junit:junit:4.13.2"
|
||||
kotlinCompileTesting = { module = "com.github.tschuchortdev:kotlin-compile-testing", version.ref = "kotlinCompileTesting" }
|
||||
kotlinCompileTesting-ksp = { module = "com.github.tschuchortdev:kotlin-compile-testing-ksp", version.ref ="kotlinCompileTesting" }
|
||||
truth = "com.google.truth:truth:1.1.3"
|
||||
|
@@ -20,7 +20,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
kotlin("kapt")
|
||||
alias(libs.plugins.ksp)
|
||||
id("com.vanniktech.maven.publish")
|
||||
alias(libs.plugins.mavenShadow)
|
||||
}
|
||||
@@ -31,12 +31,12 @@ tasks.withType<KotlinCompile>().configureEach {
|
||||
freeCompilerArgs += listOf(
|
||||
"-Xopt-in=kotlin.RequiresOptIn",
|
||||
"-Xopt-in=com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview",
|
||||
"-Xopt-in=com.squareup.kotlinpoet.ksp.KotlinPoetKspPreview",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
// For kapt to work with kotlin-compile-testing
|
||||
jvmArgs(
|
||||
"--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED",
|
||||
"--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
|
||||
@@ -47,17 +47,15 @@ tasks.withType<Test>().configureEach {
|
||||
"--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED",
|
||||
"--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED",
|
||||
"--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
|
||||
"--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
|
||||
"--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED"
|
||||
)
|
||||
}
|
||||
|
||||
val shade: Configuration = configurations.maybeCreate("compileShaded")
|
||||
configurations.getByName("compileOnly").extendsFrom(shade)
|
||||
dependencies {
|
||||
// Use `api` because kapt will not resolve `runtime` dependencies without it, only `compile`
|
||||
// https://youtrack.jetbrains.com/issue/KT-41702
|
||||
api(project(":moshi"))
|
||||
api(kotlin("reflect"))
|
||||
implementation(project(":moshi"))
|
||||
implementation(kotlin("reflect"))
|
||||
shade(libs.kotlinxMetadata) {
|
||||
exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib")
|
||||
}
|
||||
@@ -67,16 +65,30 @@ dependencies {
|
||||
exclude(group = "com.squareup", module = "kotlinpoet")
|
||||
exclude(group = "com.google.guava")
|
||||
}
|
||||
api(libs.guava)
|
||||
api(libs.asm)
|
||||
shade(libs.kotlinpoet.ksp) {
|
||||
exclude(group = "org.jetbrains.kotlin")
|
||||
exclude(group = "com.squareup", module = "kotlinpoet")
|
||||
}
|
||||
implementation(libs.guava)
|
||||
implementation(libs.asm)
|
||||
|
||||
api(libs.autoService)
|
||||
kapt(libs.autoService.processor)
|
||||
api(libs.incap)
|
||||
kapt(libs.incap.processor)
|
||||
implementation(libs.autoService)
|
||||
ksp(libs.autoService.ksp)
|
||||
|
||||
// KSP deps
|
||||
compileOnly(libs.ksp)
|
||||
compileOnly(libs.ksp.api)
|
||||
compileOnly(libs.kotlin.compilerEmbeddable)
|
||||
// Always force the latest KSP version to match the one we're compiling against
|
||||
testImplementation(libs.ksp)
|
||||
testImplementation(libs.kotlin.compilerEmbeddable)
|
||||
testImplementation(libs.kotlinCompileTesting.ksp)
|
||||
|
||||
// Copy these again as they're not automatically included since they're shaded
|
||||
testImplementation(project(":moshi"))
|
||||
testImplementation(kotlin("reflect"))
|
||||
testImplementation(libs.kotlinpoet.metadata)
|
||||
testImplementation(libs.kotlinpoet.ksp)
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(libs.truth)
|
||||
testImplementation(libs.kotlinCompileTesting)
|
||||
|
@@ -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 }
|
||||
}
|
@@ -16,9 +16,6 @@
|
||||
package com.squareup.moshi.kotlin.codegen.api
|
||||
|
||||
import com.squareup.kotlinpoet.ClassName
|
||||
import javax.annotation.processing.Filer
|
||||
import javax.lang.model.element.Element
|
||||
import javax.tools.StandardLocation
|
||||
|
||||
/**
|
||||
* Represents a proguard configuration for a given spec. This covers three main areas:
|
||||
@@ -42,16 +39,11 @@ internal data class ProguardConfig(
|
||||
val targetConstructorParams: List<String>,
|
||||
val qualifierProperties: Set<QualifierAdapterProperty>
|
||||
) {
|
||||
private val outputFile = "META-INF/proguard/moshi-${targetClass.canonicalName}.pro"
|
||||
|
||||
/** Writes this to `filer`. */
|
||||
fun writeTo(filer: Filer, vararg originatingElements: Element) {
|
||||
filer.createResource(StandardLocation.CLASS_OUTPUT, "", outputFile, *originatingElements)
|
||||
.openWriter()
|
||||
.use(::writeTo)
|
||||
fun outputFilePathWithoutExtension(canonicalName: String): String {
|
||||
return "META-INF/proguard/moshi-$canonicalName"
|
||||
}
|
||||
|
||||
private fun writeTo(out: Appendable): Unit = out.run {
|
||||
fun writeTo(out: Appendable): Unit = out.run {
|
||||
//
|
||||
// -if class {the target class}
|
||||
// -keepnames class {the target class}
|
||||
|
@@ -217,8 +217,12 @@ internal fun List<TypeName>.toTypeVariableResolver(
|
||||
// replacement later that may add bounds referencing this.
|
||||
val id = typeVar.name
|
||||
parametersMap[id] = TypeVariableName(id)
|
||||
}
|
||||
|
||||
for (typeVar in this) {
|
||||
check(typeVar is TypeVariableName)
|
||||
// Now replace it with the full version.
|
||||
parametersMap[id] = typeVar.deepCopy(null) { it.stripTypeVarVariance(resolver) }
|
||||
parametersMap[typeVar.name] = typeVar.deepCopy(null) { it.stripTypeVarVariance(resolver) }
|
||||
}
|
||||
|
||||
return resolver
|
||||
|
@@ -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.")
|
||||
}
|
||||
}
|
@@ -21,9 +21,11 @@ import com.squareup.kotlinpoet.ClassName
|
||||
import com.squareup.kotlinpoet.metadata.classinspectors.ElementsClassInspector
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.squareup.moshi.kotlin.codegen.api.AdapterGenerator
|
||||
import com.squareup.moshi.kotlin.codegen.api.Options.OPTION_GENERATED
|
||||
import com.squareup.moshi.kotlin.codegen.api.Options.OPTION_GENERATE_PROGUARD_RULES
|
||||
import com.squareup.moshi.kotlin.codegen.api.Options.POSSIBLE_GENERATED_NAMES
|
||||
import com.squareup.moshi.kotlin.codegen.api.ProguardConfig
|
||||
import com.squareup.moshi.kotlin.codegen.api.PropertyGenerator
|
||||
import net.ltgt.gradle.incap.IncrementalAnnotationProcessor
|
||||
import net.ltgt.gradle.incap.IncrementalAnnotationProcessorType.ISOLATING
|
||||
import javax.annotation.processing.AbstractProcessor
|
||||
import javax.annotation.processing.Filer
|
||||
import javax.annotation.processing.Messager
|
||||
@@ -31,10 +33,12 @@ import javax.annotation.processing.ProcessingEnvironment
|
||||
import javax.annotation.processing.Processor
|
||||
import javax.annotation.processing.RoundEnvironment
|
||||
import javax.lang.model.SourceVersion
|
||||
import javax.lang.model.element.Element
|
||||
import javax.lang.model.element.TypeElement
|
||||
import javax.lang.model.util.Elements
|
||||
import javax.lang.model.util.Types
|
||||
import javax.tools.Diagnostic
|
||||
import javax.tools.StandardLocation
|
||||
|
||||
/**
|
||||
* An annotation processor that reads Kotlin data classes and generates Moshi JsonAdapters for them.
|
||||
@@ -45,34 +49,8 @@ import javax.tools.Diagnostic
|
||||
* adapter will also be internal).
|
||||
*/
|
||||
@AutoService(Processor::class)
|
||||
@IncrementalAnnotationProcessor(ISOLATING)
|
||||
public class JsonClassCodegenProcessor : AbstractProcessor() {
|
||||
|
||||
public companion object {
|
||||
/**
|
||||
* This annotation processing argument can be specified to have a `@Generated` annotation
|
||||
* included in the generated code. It is not encouraged unless you need it for static analysis
|
||||
* reasons and not enabled by default.
|
||||
*
|
||||
* Note that this can only be one of the following values:
|
||||
* * `"javax.annotation.processing.Generated"` (JRE 9+)
|
||||
* * `"javax.annotation.Generated"` (JRE <9)
|
||||
*/
|
||||
public const val OPTION_GENERATED: String = "moshi.generated"
|
||||
|
||||
/**
|
||||
* This boolean processing option can control proguard rule generation.
|
||||
* Normally, this is not recommended unless end-users build their own JsonAdapter look-up tool.
|
||||
* This is enabled by default.
|
||||
*/
|
||||
public const val OPTION_GENERATE_PROGUARD_RULES: String = "moshi.generateProguardRules"
|
||||
|
||||
private val POSSIBLE_GENERATED_NAMES = arrayOf(
|
||||
ClassName("javax.annotation.processing", "Generated"),
|
||||
ClassName("javax.annotation", "Generated")
|
||||
).associateBy { it.canonicalName }
|
||||
}
|
||||
|
||||
private lateinit var types: Types
|
||||
private lateinit var elements: Elements
|
||||
private lateinit var filer: Filer
|
||||
@@ -190,3 +168,10 @@ public class JsonClassCodegenProcessor : AbstractProcessor() {
|
||||
return AdapterGenerator(type, sortedProperties)
|
||||
}
|
||||
}
|
||||
|
||||
/** Writes this config to a [filer]. */
|
||||
private fun ProguardConfig.writeTo(filer: Filer, vararg originatingElements: Element) {
|
||||
filer.createResource(StandardLocation.CLASS_OUTPUT, "", "${outputFilePathWithoutExtension(targetClass.canonicalName)}.pro", *originatingElements)
|
||||
.openWriter()
|
||||
.use(::writeTo)
|
||||
}
|
||||
|
@@ -19,12 +19,10 @@ import com.squareup.kotlinpoet.AnnotationSpec
|
||||
import com.squareup.kotlinpoet.ClassName
|
||||
import com.squareup.kotlinpoet.DelicateKotlinPoetApi
|
||||
import com.squareup.kotlinpoet.KModifier
|
||||
import com.squareup.kotlinpoet.LambdaTypeName
|
||||
import com.squareup.kotlinpoet.ParameterizedTypeName
|
||||
import com.squareup.kotlinpoet.TypeName
|
||||
import com.squareup.kotlinpoet.TypeSpec
|
||||
import com.squareup.kotlinpoet.TypeVariableName
|
||||
import com.squareup.kotlinpoet.WildcardTypeName
|
||||
import com.squareup.kotlinpoet.asClassName
|
||||
import com.squareup.kotlinpoet.asTypeName
|
||||
import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview
|
||||
@@ -37,7 +35,6 @@ import com.squareup.kotlinpoet.metadata.isLocal
|
||||
import com.squareup.kotlinpoet.metadata.isPublic
|
||||
import com.squareup.kotlinpoet.metadata.isSealed
|
||||
import com.squareup.kotlinpoet.tag
|
||||
import com.squareup.kotlinpoet.tags.TypeAliasTag
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonQualifier
|
||||
import com.squareup.moshi.kotlin.codegen.api.DelegateKey
|
||||
@@ -46,15 +43,14 @@ import com.squareup.moshi.kotlin.codegen.api.TargetConstructor
|
||||
import com.squareup.moshi.kotlin.codegen.api.TargetParameter
|
||||
import com.squareup.moshi.kotlin.codegen.api.TargetProperty
|
||||
import com.squareup.moshi.kotlin.codegen.api.TargetType
|
||||
import com.squareup.moshi.kotlin.codegen.api.deepCopy
|
||||
import com.squareup.moshi.kotlin.codegen.api.rawType
|
||||
import com.squareup.moshi.kotlin.codegen.api.unwrapTypeAlias
|
||||
import kotlinx.metadata.KmConstructor
|
||||
import kotlinx.metadata.jvm.signature
|
||||
import java.lang.annotation.ElementType
|
||||
import java.lang.annotation.Retention
|
||||
import java.lang.annotation.RetentionPolicy
|
||||
import java.lang.annotation.Target
|
||||
import java.util.TreeSet
|
||||
import javax.annotation.processing.Messager
|
||||
import javax.lang.model.element.AnnotationMirror
|
||||
import javax.lang.model.element.Element
|
||||
@@ -516,30 +512,6 @@ private fun String.escapeDollarSigns(): String {
|
||||
return replace("\$", "\${\'\$\'}")
|
||||
}
|
||||
|
||||
internal fun TypeName.unwrapTypeAlias(): TypeName {
|
||||
return when (this) {
|
||||
is ClassName -> {
|
||||
tag<TypeAliasTag>()?.abbreviatedType?.let { unwrappedType ->
|
||||
// If any type is nullable, then the whole thing is nullable
|
||||
var isAnyNullable = isNullable
|
||||
// Keep track of all annotations across type levels. Sort them too for consistency.
|
||||
val runningAnnotations = TreeSet<AnnotationSpec>(compareBy { it.toString() }).apply {
|
||||
addAll(annotations)
|
||||
}
|
||||
val nestedUnwrappedType = unwrappedType.unwrapTypeAlias()
|
||||
runningAnnotations.addAll(nestedUnwrappedType.annotations)
|
||||
isAnyNullable = isAnyNullable || nestedUnwrappedType.isNullable
|
||||
nestedUnwrappedType.copy(nullable = isAnyNullable, annotations = runningAnnotations.toList())
|
||||
} ?: this
|
||||
}
|
||||
is ParameterizedTypeName -> deepCopy(TypeName::unwrapTypeAlias)
|
||||
is TypeVariableName -> deepCopy(transform = TypeName::unwrapTypeAlias)
|
||||
is WildcardTypeName -> deepCopy(TypeName::unwrapTypeAlias)
|
||||
is LambdaTypeName -> deepCopy(TypeName::unwrapTypeAlias)
|
||||
else -> throw UnsupportedOperationException("Type '${javaClass.simpleName}' is illegal. Only classes, parameterized types, wildcard types, or type variables are allowed.")
|
||||
}
|
||||
}
|
||||
|
||||
internal val TypeElement.metadata: Metadata
|
||||
get() {
|
||||
return getAnnotation(Metadata::class.java)
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
@@ -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)
|
@@ -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("\$", "\${\'\$\'}")
|
||||
}
|
@@ -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())
|
@@ -0,0 +1 @@
|
||||
com.squareup.moshi.kotlin.codegen.apt.JsonClassCodegenProcessor,ISOLATING
|
@@ -18,6 +18,9 @@ package com.squareup.moshi.kotlin.codegen.apt
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.kotlin.codegen.api.Options
|
||||
import com.squareup.moshi.kotlin.codegen.api.Options.OPTION_GENERATED
|
||||
import com.squareup.moshi.kotlin.codegen.api.Options.OPTION_GENERATE_PROGUARD_RULES
|
||||
import com.tschuchort.compiletesting.KotlinCompilation
|
||||
import com.tschuchort.compiletesting.SourceFile
|
||||
import com.tschuchort.compiletesting.SourceFile.Companion.kotlin
|
||||
@@ -324,7 +327,6 @@ class JsonClassCodegenProcessorTest {
|
||||
"""
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
@JsonClass(generateAdapter = true)
|
||||
class NonPropertyConstructorParameter(a: Int, val b: Int)
|
||||
"""
|
||||
@@ -349,10 +351,10 @@ class JsonClassCodegenProcessorTest {
|
||||
"""
|
||||
)
|
||||
).apply {
|
||||
kaptArgs[JsonClassCodegenProcessor.OPTION_GENERATED] = "javax.annotation.GeneratedBlerg"
|
||||
kaptArgs[OPTION_GENERATED] = "javax.annotation.GeneratedBlerg"
|
||||
}.compile()
|
||||
assertThat(result.messages).contains(
|
||||
"Invalid option value for ${JsonClassCodegenProcessor.OPTION_GENERATED}"
|
||||
"Invalid option value for $OPTION_GENERATED"
|
||||
)
|
||||
}
|
||||
@Test
|
||||
@@ -368,7 +370,7 @@ class JsonClassCodegenProcessorTest {
|
||||
"""
|
||||
)
|
||||
).apply {
|
||||
kaptArgs[JsonClassCodegenProcessor.OPTION_GENERATE_PROGUARD_RULES] = "false"
|
||||
kaptArgs[OPTION_GENERATE_PROGUARD_RULES] = "false"
|
||||
}.compile()
|
||||
assertThat(result.generatedFiles.filter { it.endsWith(".pro") }).isEmpty()
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
@@ -18,7 +18,15 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
kotlin("kapt")
|
||||
kotlin("kapt") apply false
|
||||
alias(libs.plugins.ksp) apply false
|
||||
}
|
||||
|
||||
val useKsp = hasProperty("useKsp")
|
||||
if (useKsp) {
|
||||
apply(plugin = "com.google.devtools.ksp")
|
||||
} else {
|
||||
apply(plugin = "org.jetbrains.kotlin.kapt")
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
@@ -37,9 +45,14 @@ tasks.withType<KotlinCompile>().configureEach {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
kaptTest(project(":kotlin:codegen"))
|
||||
if (useKsp) {
|
||||
"kspTest"(project(":kotlin:codegen"))
|
||||
} else {
|
||||
"kaptTest"(project(":kotlin:codegen"))
|
||||
}
|
||||
testImplementation(project(":moshi"))
|
||||
testImplementation(project(":kotlin:reflect"))
|
||||
testImplementation(project(":kotlin:tests:extra-moshi-test-module"))
|
||||
testImplementation(kotlin("reflect"))
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(libs.assertj)
|
||||
|
19
kotlin/tests/extra-moshi-test-module/build.gradle.kts
Normal file
19
kotlin/tests/extra-moshi-test-module/build.gradle.kts
Normal 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")
|
||||
}
|
@@ -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
|
@@ -22,12 +22,13 @@ import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.JsonAdapter.Factory
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.squareup.moshi.JsonDataException
|
||||
import com.squareup.moshi.JsonQualifier
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.ToJson
|
||||
import com.squareup.moshi.Types
|
||||
import com.squareup.moshi.adapter
|
||||
import com.squareup.moshi.kotlin.codegen.test.extra.AbstractClassInModuleA
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import com.squareup.moshi.rawType
|
||||
import com.squareup.moshi.supertypeOf
|
||||
import org.intellij.lang.annotations.Language
|
||||
import org.junit.Assert.fail
|
||||
import org.junit.Test
|
||||
@@ -63,11 +64,11 @@ class DualKotlinTest(useReflection: Boolean) {
|
||||
object : Factory {
|
||||
override fun create(
|
||||
type: Type,
|
||||
annotations: Set<Annotation>,
|
||||
annotations: MutableSet<out Annotation>,
|
||||
moshi: Moshi
|
||||
): JsonAdapter<*>? {
|
||||
// Prevent falling back to generated adapter lookup
|
||||
val rawType = type.rawType
|
||||
val rawType = Types.getRawType(type)
|
||||
val metadataClass = Class.forName("kotlin.Metadata") as Class<out Annotation>
|
||||
check(rawType.isEnum || !rawType.isAnnotationPresent(metadataClass)) {
|
||||
"Unhandled Kotlin type in reflective test! $rawType"
|
||||
@@ -248,21 +249,21 @@ class DualKotlinTest(useReflection: Boolean) {
|
||||
val hasNonNullConstructorParameterAdapter =
|
||||
localMoshi.adapter<HasNonNullConstructorParameter>()
|
||||
assertThat(
|
||||
//language=JSON
|
||||
hasNonNullConstructorParameterAdapter
|
||||
//language=JSON
|
||||
.fromJson("{\"a\":null}")
|
||||
).isEqualTo(HasNonNullConstructorParameter("fallback"))
|
||||
|
||||
val hasNullableConstructorParameterAdapter =
|
||||
localMoshi.adapter<HasNullableConstructorParameter>()
|
||||
assertThat(
|
||||
//language=JSON
|
||||
hasNullableConstructorParameterAdapter
|
||||
//language=JSON
|
||||
.fromJson("{\"a\":null}")
|
||||
).isEqualTo(HasNullableConstructorParameter("fallback"))
|
||||
//language=JSON
|
||||
assertThat(
|
||||
hasNullableConstructorParameterAdapter
|
||||
//language=JSON
|
||||
.toJson(HasNullableConstructorParameter(null))
|
||||
).isEqualTo("{\"a\":\"fallback\"}")
|
||||
}
|
||||
@@ -284,7 +285,7 @@ class DualKotlinTest(useReflection: Boolean) {
|
||||
assertThat(decoded.a).isEqualTo(null)
|
||||
}
|
||||
|
||||
@Test fun inlineClass() {
|
||||
@Test fun valueClass() {
|
||||
val adapter = moshi.adapter<ValueClass>()
|
||||
|
||||
val inline = ValueClass(5)
|
||||
@@ -297,6 +298,13 @@ class DualKotlinTest(useReflection: Boolean) {
|
||||
"""{"i":6}"""
|
||||
val result = adapter.fromJson(testJson)!!
|
||||
assertThat(result.i).isEqualTo(6)
|
||||
|
||||
// TODO doesn't work yet.
|
||||
// need to invoke the constructor_impl$default static method, invoke constructor with result
|
||||
// val testEmptyJson =
|
||||
// """{}"""
|
||||
// val result2 = adapter.fromJson(testEmptyJson)!!
|
||||
// assertThat(result2.i).isEqualTo(0)
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@@ -339,6 +347,50 @@ class DualKotlinTest(useReflection: Boolean) {
|
||||
abstract class Asset<A : Asset<A>>
|
||||
abstract class AssetMetaData<A : Asset<A>>
|
||||
|
||||
// Regression test for https://github.com/ZacSweers/MoshiX/issues/125
|
||||
@Test fun selfReferencingTypeVars() {
|
||||
val adapter = moshi.adapter<StringNodeNumberNode>()
|
||||
|
||||
val data = StringNodeNumberNode().also {
|
||||
it.t = StringNodeNumberNode().also {
|
||||
it.text = "child 1"
|
||||
}
|
||||
it.text = "root"
|
||||
it.r = NumberStringNode().also {
|
||||
it.number = 0
|
||||
it.t = NumberStringNode().also {
|
||||
it.number = 1
|
||||
}
|
||||
it.r = StringNodeNumberNode().also {
|
||||
it.text = "grand child 1"
|
||||
}
|
||||
}
|
||||
}
|
||||
assertThat(adapter.toJson(data))
|
||||
//language=JSON
|
||||
.isEqualTo(
|
||||
"""
|
||||
{"text":"root","t":{"text":"child 1"},"r":{"number":0,"t":{"number":1},"r":{"text":"grand child 1"}}}
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
open class Node<T : Node<T, R>, R : Node<R, T>> {
|
||||
var t: T? = null
|
||||
var r: R? = null
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
class StringNodeNumberNode : Node<StringNodeNumberNode, NumberStringNode>() {
|
||||
var text: String = ""
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
class NumberStringNode : Node<NumberStringNode, StringNodeNumberNode>() {
|
||||
var number: Int = 0
|
||||
}
|
||||
|
||||
// Regression test for https://github.com/square/moshi/issues/968
|
||||
@Test fun abstractSuperProperties() {
|
||||
val adapter = moshi.adapter<InternalAbstractProperty>()
|
||||
@@ -447,7 +499,7 @@ class DualKotlinTest(useReflection: Boolean) {
|
||||
@Test fun typeAliasUnwrapping() {
|
||||
val adapter = moshi
|
||||
.newBuilder()
|
||||
.add(supertypeOf<Int>(), moshi.adapter<Int>())
|
||||
.add(Types.supertypeOf(Int::class.javaObjectType), moshi.adapter<Int>())
|
||||
.build()
|
||||
.adapter<TypeAliasUnwrapping>()
|
||||
|
||||
@@ -587,8 +639,7 @@ class DualKotlinTest(useReflection: Boolean) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class OutDeclaration<out T>(val input: T)
|
||||
|
||||
// Regression test for https://github.com/square/moshi/issues/1244
|
||||
@Test fun backwardReferencingTypeVarsAndIntersectionTypes() {
|
||||
@Test fun intersectionTypes() {
|
||||
val adapter = moshi.adapter<IntersectionTypes<IntersectionTypesEnum>>()
|
||||
|
||||
@Language("JSON")
|
||||
@@ -625,7 +676,7 @@ data class GenericClass<T>(val value: T)
|
||||
// Has to be outside since value classes are only allowed on top level
|
||||
@JvmInline
|
||||
@JsonClass(generateAdapter = true)
|
||||
value class ValueClass(val i: Int)
|
||||
value class ValueClass(val i: Int = 0)
|
||||
|
||||
typealias A = Int
|
||||
typealias NullableA = A?
|
||||
@@ -634,3 +685,23 @@ typealias NullableB = B?
|
||||
typealias C = NullableA
|
||||
typealias D = C
|
||||
typealias E = D
|
||||
|
||||
// Regression test for enum constants in annotations and array types
|
||||
// https://github.com/ZacSweers/MoshiX/issues/103
|
||||
@Retention(RUNTIME)
|
||||
@JsonQualifier
|
||||
annotation class UpperCase(val foo: Array<Foo>)
|
||||
|
||||
enum class Foo { BAR }
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ClassWithQualifier(
|
||||
@UpperCase(foo = [Foo.BAR])
|
||||
val a: Int
|
||||
)
|
||||
|
||||
// Regression for https://github.com/ZacSweers/MoshiX/issues/120
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class DataClassInModuleB(
|
||||
val id: String
|
||||
) : AbstractClassInModuleA()
|
||||
|
@@ -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
|
||||
}
|
@@ -31,5 +31,6 @@ include(":examples")
|
||||
include(":kotlin:reflect")
|
||||
include(":kotlin:codegen")
|
||||
include(":kotlin:tests")
|
||||
include(":kotlin:tests:extra-moshi-test-module")
|
||||
|
||||
enableFeaturePreview("VERSION_CATALOGS")
|
||||
|
Reference in New Issue
Block a user