diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 118e6fb..f808729 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,13 +4,24 @@ on: [push, pull_request] jobs: build: - name: 'Java ${{ matrix.java-version }} | KSP ${{ matrix.use-ksp }}' + name: 'Java ${{ matrix.java-version }} | Kotlin ${{ matrix.kotlin-version }} | KSP ${{ matrix.use-ksp }}' runs-on: ubuntu-latest strategy: fail-fast: false matrix: use-ksp: [ true, false ] + kotlin-version: [ '1.5.31', '1.6.0-RC' ] + ksp-version: [ '1.5.31-1.0.0', '1.6.0-RC-1.0.0' ] + exclude: + - kotlin-version: '1.5.31' + ksp-version: '1.6.0-RC-1.0.0' + - kotlin-version: '1.6.0-RC' + ksp-version: '1.5.31-1.0.0' + + env: + MOSHI_KOTLIN_VERSION: '${{ matrix.kotlin-version }}' + MOSHI_KSP_VERSION: '${{ matrix.ksp-version }}' steps: - name: Checkout @@ -36,8 +47,8 @@ jobs: java-version: '17' - name: Test - run: ./gradlew build check --stacktrace -PuseKsp=${{ matrix.use-ksp }} + run: ./gradlew build check --stacktrace -PuseKsp=${{ matrix.use-ksp }} -PkotlinVersion=${{ matrix.kotlin-version }} - name: Publish (default branch only) - if: github.repository == 'square/moshi' && github.ref == 'refs/heads/master' + if: github.repository == 'square/moshi' && github.ref == 'refs/heads/master' && matrix.kotlin-version == '1.5.31' && matrix.use-ksp == 'false' run: ./gradlew publish -PmavenCentralUsername=${{ secrets.SONATYPE_NEXUS_USERNAME }} -PmavenCentralPassword=${{ secrets.SONATYPE_NEXUS_PASSWORD }} --stacktrace diff --git a/build.gradle.kts b/build.gradle.kts index 01a9070..e07e0ea 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -23,7 +23,12 @@ import java.net.URL buildscript { dependencies { - classpath(kotlin("gradle-plugin", version = libs.versions.kotlin.get())) + val kotlinVersion = System.getenv("MOSHI_KOTLIN_VERSION") + ?: libs.versions.kotlin.get() + val kspVersion = System.getenv("MOSHI_KSP_VERSION") + ?: libs.versions.ksp.get() + classpath(kotlin("gradle-plugin", version = kotlinVersion)) + classpath("com.google.devtools.ksp:symbol-processing-gradle-plugin:$kspVersion") // https://github.com/melix/japicmp-gradle-plugin/issues/36 classpath("com.google.guava:guava:28.2-jre") } @@ -125,8 +130,9 @@ subprojects { pluginManager.withPlugin("org.jetbrains.kotlin.jvm") { tasks.withType().configureEach { kotlinOptions { - @Suppress("SuspiciousCollectionReassignment") - freeCompilerArgs += listOf("-progressive") + // TODO re-enable when no longer supporting multiple kotlin versions +// @Suppress("SuspiciousCollectionReassignment") +// freeCompilerArgs += listOf("-progressive") jvmTarget = libs.versions.jvmTarget.get() } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 041af8e..1c00f98 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,7 +18,7 @@ gjf = "1.11.0" jvmTarget = "1.8" kotlin = "1.5.31" kotlinCompileTesting = "1.4.4" -kotlinpoet = "1.10.1" +kotlinpoet = "1.10.2" ksp = "1.5.31-1.0.0" ktlint = "0.41.0" diff --git a/kotlin/codegen/build.gradle.kts b/kotlin/codegen/build.gradle.kts index d6fa53c..ab1bc26 100644 --- a/kotlin/codegen/build.gradle.kts +++ b/kotlin/codegen/build.gradle.kts @@ -20,7 +20,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { kotlin("jvm") - alias(libs.plugins.ksp) + id("com.google.devtools.ksp") id("com.vanniktech.maven.publish") alias(libs.plugins.mavenShadow) } diff --git a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/AdapterGenerator.kt b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/AdapterGenerator.kt index d9a3f48..a1b2c0e 100644 --- a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/AdapterGenerator.kt +++ b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/AdapterGenerator.kt @@ -185,26 +185,31 @@ internal class AdapterGenerator( result.addAnnotation(COMMON_SUPPRESS) result.addType(generatedAdapter) val proguardConfig = if (generateProguardRules) { - generatedAdapter.createProguardRule() + generatedAdapter.createProguardRule(target.instantiateAnnotations) } else { null } return PreparedAdapter(result.build(), proguardConfig) } - private fun TypeSpec.createProguardRule(): ProguardConfig { - val adapterProperties = propertySpecs - .asSequence() - .filter { prop -> - prop.type.rawType() == JsonAdapter::class.asClassName() - } - .filter { prop -> prop.annotations.isNotEmpty() } - .mapTo(mutableSetOf()) { prop -> - QualifierAdapterProperty( - name = prop.name, - qualifiers = prop.annotations.mapTo(mutableSetOf()) { it.typeName.rawType() } - ) - } + private fun TypeSpec.createProguardRule(instantiateAnnotations: Boolean): ProguardConfig { + val adapterProperties = if (instantiateAnnotations) { + // Don't need to do anything special if we instantiate them directly! + emptySet() + } else { + propertySpecs + .asSequence() + .filter { prop -> + prop.type.rawType() == JsonAdapter::class.asClassName() + } + .filter { prop -> prop.annotations.isNotEmpty() } + .mapTo(mutableSetOf()) { prop -> + QualifierAdapterProperty( + name = prop.name, + qualifiers = prop.annotations.mapTo(mutableSetOf()) { it.typeName.rawType() } + ) + } + } val adapterConstructorParams = when (requireNotNull(primaryConstructor).parameters.size) { 1 -> listOf(CN_MOSHI.reflectionName()) diff --git a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/DelegateKey.kt b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/DelegateKey.kt index 42549fd..4dfea1a 100644 --- a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/DelegateKey.kt +++ b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/DelegateKey.kt @@ -17,6 +17,7 @@ package com.squareup.moshi.kotlin.codegen.api import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.CodeBlock import com.squareup.kotlinpoet.KModifier import com.squareup.kotlinpoet.MemberName import com.squareup.kotlinpoet.NameAllocator @@ -29,6 +30,7 @@ import com.squareup.kotlinpoet.TypeVariableName import com.squareup.kotlinpoet.WildcardTypeName import com.squareup.kotlinpoet.asClassName import com.squareup.kotlinpoet.asTypeName +import com.squareup.kotlinpoet.joinToCode import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Types import java.util.Locale @@ -36,7 +38,8 @@ import java.util.Locale /** A JsonAdapter that can be used to encode and decode a particular field. */ internal data class DelegateKey( private val type: TypeName, - private val jsonQualifiers: List + private val jsonQualifiers: List, + private val instantiateAnnotations: Boolean ) { val nullable get() = type.isNullable @@ -60,8 +63,12 @@ internal data class DelegateKey( moshiParameter, typeRenderer.render(type) ) + val (initializerString, args) = when { jsonQualifiers.isEmpty() -> ", %M()" to arrayOf(MemberName("kotlin.collections", "emptySet")) + instantiateAnnotations -> { + ", setOf(%L)" to arrayOf(jsonQualifiers.map { it.asInstantiationExpression() }.joinToCode()) + } else -> { ", %T.getFieldJsonQualifierAnnotations(javaClass, " + "%S)" to arrayOf(Types::class.asTypeName(), adapterName) @@ -70,12 +77,25 @@ internal data class DelegateKey( val finalArgs = arrayOf(*standardArgs, *args, propertyName) return PropertySpec.builder(adapterName, adapterTypeName, KModifier.PRIVATE) - .addAnnotations(jsonQualifiers) + .apply { + if (!instantiateAnnotations) { + addAnnotations(jsonQualifiers) + } + } .initializer("%N.adapter(%L$initializerString, %S)", *finalArgs) .build() } } +private fun AnnotationSpec.asInstantiationExpression(): CodeBlock { + // (args) + return CodeBlock.of( + "%T(%L)", + typeName, + members.joinToCode() + ) +} + /** * Returns a suggested variable name derived from a list of type names. This just concatenates, * yielding types like MapOfStringLong. diff --git a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/Options.kt b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/Options.kt index 05ff2e6..2b9f587 100644 --- a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/Options.kt +++ b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/Options.kt @@ -36,6 +36,14 @@ internal object Options { */ const val OPTION_GENERATE_PROGUARD_RULES: String = "moshi.generateProguardRules" + /** + * This boolean processing option controls whether or not Moshi will directly instantiate + * JsonQualifier annotations in Kotlin 1.6+. Note that this is enabled by default in Kotlin 1.6 + * but can be disabled to restore the legacy behavior of storing annotations on generated adapter + * fields and looking them up reflectively. + */ + const val OPTION_INSTANTIATE_ANNOTATIONS: String = "moshi.instantiateAnnotations" + val POSSIBLE_GENERATED_NAMES = arrayOf( ClassName("javax.annotation.processing", "Generated"), ClassName("javax.annotation", "Generated") diff --git a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TargetType.kt b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TargetType.kt index 8e1c5b1..7009b0c 100644 --- a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TargetType.kt +++ b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TargetType.kt @@ -26,7 +26,8 @@ internal data class TargetType( val properties: Map, val typeVariables: List, val isDataClass: Boolean, - val visibility: KModifier + val visibility: KModifier, + val instantiateAnnotations: Boolean ) { init { diff --git a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/apt/JsonClassCodegenProcessor.kt b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/apt/JsonClassCodegenProcessor.kt index 8864384..6050f88 100644 --- a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/apt/JsonClassCodegenProcessor.kt +++ b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/apt/JsonClassCodegenProcessor.kt @@ -23,6 +23,7 @@ import com.squareup.moshi.JsonClass import com.squareup.moshi.kotlin.codegen.api.AdapterGenerator import com.squareup.moshi.kotlin.codegen.api.Options.OPTION_GENERATED import com.squareup.moshi.kotlin.codegen.api.Options.OPTION_GENERATE_PROGUARD_RULES +import com.squareup.moshi.kotlin.codegen.api.Options.OPTION_INSTANTIATE_ANNOTATIONS import com.squareup.moshi.kotlin.codegen.api.Options.POSSIBLE_GENERATED_NAMES import com.squareup.moshi.kotlin.codegen.api.ProguardConfig import com.squareup.moshi.kotlin.codegen.api.PropertyGenerator @@ -59,6 +60,7 @@ public class JsonClassCodegenProcessor : AbstractProcessor() { private val annotation = JsonClass::class.java private var generatedType: ClassName? = null private var generateProguardRules: Boolean = true + private var instantiateAnnotations: Boolean = true override fun getSupportedAnnotationTypes(): Set = setOf(annotation.canonicalName) @@ -76,6 +78,7 @@ public class JsonClassCodegenProcessor : AbstractProcessor() { } generateProguardRules = processingEnv.options[OPTION_GENERATE_PROGUARD_RULES]?.toBooleanStrictOrNull() ?: true + instantiateAnnotations = processingEnv.options[OPTION_INSTANTIATE_ANNOTATIONS]?.toBooleanStrictOrNull() ?: true this.types = processingEnv.typeUtils this.elements = processingEnv.elementUtils @@ -135,11 +138,18 @@ public class JsonClassCodegenProcessor : AbstractProcessor() { element: TypeElement, cachedClassInspector: MoshiCachedClassInspector ): AdapterGenerator? { - val type = targetType(messager, elements, types, element, cachedClassInspector) ?: return null + val type = targetType( + messager, + elements, + types, + element, + cachedClassInspector, + instantiateAnnotations + ) ?: return null val properties = mutableMapOf() for (property in type.properties.values) { - val generator = property.generator(messager, element, elements) + val generator = property.generator(messager, element, elements, type.instantiateAnnotations) if (generator != null) { properties[property.name] = generator } diff --git a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/apt/metadata.kt b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/apt/metadata.kt index a081df2..40e20e8 100644 --- a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/apt/metadata.kt +++ b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/apt/metadata.kt @@ -71,6 +71,7 @@ private val VISIBILITY_MODIFIERS = setOf( KModifier.PROTECTED, KModifier.PUBLIC ) +private val ANNOTATION_INSTANTIATION_MIN_VERSION = KotlinVersion(1, 6, 0) private fun Collection.visibility(): KModifier { return find { it in VISIBILITY_MODIFIERS } ?: KModifier.PUBLIC @@ -122,7 +123,8 @@ internal fun targetType( elements: Elements, types: Types, element: TypeElement, - cachedClassInspector: MoshiCachedClassInspector + cachedClassInspector: MoshiCachedClassInspector, + instantiateAnnotationsEnabled: Boolean ): TargetType? { val typeMetadata = element.getAnnotation(Metadata::class.java) if (typeMetadata == null) { @@ -204,6 +206,11 @@ internal fun targetType( } } + val instantiateAnnotations = instantiateAnnotationsEnabled && run { + val (major, minor, patch) = typeMetadata.metadataVersion + val languageVersion = KotlinVersion(major, minor, patch) + languageVersion >= ANNOTATION_INSTANTIATION_MIN_VERSION + } val kotlinApi = cachedClassInspector.toTypeSpec(kmClass) val typeVariables = kotlinApi.typeVariables val appliedType = AppliedType.get(element) @@ -319,7 +326,8 @@ internal fun targetType( properties = properties, typeVariables = typeVariables, isDataClass = KModifier.DATA in kotlinApi.modifiers, - visibility = resolvedVisibility + visibility = resolvedVisibility, + instantiateAnnotations = instantiateAnnotations ) } @@ -418,7 +426,8 @@ private val TargetProperty.isVisible: Boolean internal fun TargetProperty.generator( messager: Messager, sourceElement: TypeElement, - elements: Elements + elements: Elements, + instantiateAnnotations: Boolean ): PropertyGenerator? { if (isTransient) { if (!hasDefault) { @@ -429,7 +438,7 @@ internal fun TargetProperty.generator( ) return null } - return PropertyGenerator(this, DelegateKey(type, emptyList()), true) + return PropertyGenerator(this, DelegateKey(type, emptyList(), instantiateAnnotations), true) } if (!isVisible) { @@ -452,6 +461,7 @@ internal fun TargetProperty.generator( // Check Java types since that covers both Java and Kotlin annotations. val annotationElement = elements.getTypeElement(qualifierRawType.canonicalName) ?: continue + annotationElement.getAnnotation(Retention::class.java)?.let { if (it.value != RetentionPolicy.RUNTIME) { messager.printMessage( @@ -460,12 +470,14 @@ internal fun TargetProperty.generator( ) } } - annotationElement.getAnnotation(Target::class.java)?.let { - if (ElementType.FIELD !in it.value) { - messager.printMessage( - ERROR, - "JsonQualifier @${qualifierRawType.simpleName} must support FIELD target" - ) + if (!instantiateAnnotations) { + annotationElement.getAnnotation(Target::class.java)?.let { + if (ElementType.FIELD !in it.value) { + messager.printMessage( + ERROR, + "JsonQualifier @${qualifierRawType.simpleName} must support FIELD target" + ) + } } } } @@ -478,7 +490,7 @@ internal fun TargetProperty.generator( return PropertyGenerator( this, - DelegateKey(type, jsonQualifierSpecs) + DelegateKey(type, jsonQualifierSpecs, instantiateAnnotations) ) } diff --git a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/JsonClassSymbolProcessorProvider.kt b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/JsonClassSymbolProcessorProvider.kt index 9776957..ce5f822 100644 --- a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/JsonClassSymbolProcessorProvider.kt +++ b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/JsonClassSymbolProcessorProvider.kt @@ -34,6 +34,7 @@ import com.squareup.moshi.JsonClass import com.squareup.moshi.kotlin.codegen.api.AdapterGenerator import com.squareup.moshi.kotlin.codegen.api.Options.OPTION_GENERATED import com.squareup.moshi.kotlin.codegen.api.Options.OPTION_GENERATE_PROGUARD_RULES +import com.squareup.moshi.kotlin.codegen.api.Options.OPTION_INSTANTIATE_ANNOTATIONS import com.squareup.moshi.kotlin.codegen.api.Options.POSSIBLE_GENERATED_NAMES import com.squareup.moshi.kotlin.codegen.api.ProguardConfig import com.squareup.moshi.kotlin.codegen.api.PropertyGenerator @@ -63,6 +64,8 @@ private class JsonClassSymbolProcessor( } } private val generateProguardRules = environment.options[OPTION_GENERATE_PROGUARD_RULES]?.toBooleanStrictOrNull() ?: true + private val instantiateAnnotations = (environment.options[OPTION_INSTANTIATE_ANNOTATIONS]?.toBooleanStrictOrNull() ?: true) && + environment.kotlinVersion.isAtLeast(1, 6) override fun process(resolver: Resolver): List { val generatedAnnotation = generatedOption?.let { @@ -122,11 +125,11 @@ private class JsonClassSymbolProcessor( resolver: Resolver, originalType: KSDeclaration, ): AdapterGenerator? { - val type = targetType(originalType, resolver, logger) ?: return null + val type = targetType(originalType, resolver, logger, instantiateAnnotations) ?: return null val properties = mutableMapOf() for (property in type.properties.values) { - val generator = property.generator(logger, resolver, originalType) + val generator = property.generator(logger, resolver, originalType, type.instantiateAnnotations) if (generator != null) { properties[property.name] = generator } diff --git a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/KspUtil.kt b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/KspUtil.kt index ce1e0f1..003b9fa 100644 --- a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/KspUtil.kt +++ b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/KspUtil.kt @@ -69,12 +69,12 @@ private fun addValueToBlock(value: Any, resolver: Resolver, member: CodeBlock.Bu when (value) { is List<*> -> { // Array type - member.add("[⇥⇥") + member.add("arrayOf(⇥⇥") value.forEachIndexed { index, innerValue -> if (index > 0) member.add(", ") addValueToBlock(innerValue!!, resolver, member) } - member.add("⇤⇤]") + member.add("⇤⇤)") } is KSType -> { val unwrapped = value.unwrapTypeAlias() diff --git a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/MoshiApiUtil.kt b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/MoshiApiUtil.kt index 747389e..12eb9ca 100644 --- a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/MoshiApiUtil.kt +++ b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/MoshiApiUtil.kt @@ -28,17 +28,6 @@ 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.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 @@ -55,7 +44,8 @@ private val TargetProperty.isVisible: Boolean internal fun TargetProperty.generator( logger: KSPLogger, resolver: Resolver, - originalType: KSDeclaration + originalType: KSDeclaration, + instantiateAnnotations: Boolean ): PropertyGenerator? { if (isTransient) { if (!hasDefault) { @@ -65,7 +55,7 @@ internal fun TargetProperty.generator( ) return null } - return PropertyGenerator(this, DelegateKey(type, emptyList()), true) + return PropertyGenerator(this, DelegateKey(type, emptyList(), instantiateAnnotations), true) } if (!isVisible) { @@ -93,11 +83,13 @@ internal fun TargetProperty.generator( ) } } - annotationElement.findAnnotationWithType()?.let { - if (AnnotationTarget.FIELD !in it.allowedTargets) { - logger.error( - "JsonQualifier @${qualifierRawType.simpleName} must support FIELD target" - ) + if (!instantiateAnnotations) { + annotationElement.findAnnotationWithType()?.let { + if (AnnotationTarget.FIELD !in it.allowedTargets) { + logger.error( + "JsonQualifier @${qualifierRawType.simpleName} must support FIELD target" + ) + } } } } @@ -111,7 +103,7 @@ internal fun TargetProperty.generator( return PropertyGenerator( this, - DelegateKey(type, jsonQualifierSpecs) + DelegateKey(type, jsonQualifierSpecs, instantiateAnnotations) ) } diff --git a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/TargetTypes.kt b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/TargetTypes.kt index 52d90c8..7ad227e 100644 --- a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/TargetTypes.kt +++ b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/TargetTypes.kt @@ -58,6 +58,7 @@ internal fun targetType( type: KSDeclaration, resolver: Resolver, logger: KSPLogger, + instantiateAnnotations: Boolean ): TargetType? { if (type !is KSClassDeclaration) { logger.error("@JsonClass can't be applied to ${type.qualifiedName?.asString()}: must be a Kotlin class", type) @@ -152,7 +153,8 @@ internal fun targetType( properties = properties, typeVariables = typeVariables, isDataClass = Modifier.DATA in type.modifiers, - visibility = resolvedVisibility + visibility = resolvedVisibility, + instantiateAnnotations = instantiateAnnotations ) } diff --git a/kotlin/codegen/src/test/java/com/squareup/moshi/kotlin/codegen/apt/JsonClassCodegenProcessorTest.kt b/kotlin/codegen/src/test/java/com/squareup/moshi/kotlin/codegen/apt/JsonClassCodegenProcessorTest.kt index 1da2e09..af37f71 100644 --- a/kotlin/codegen/src/test/java/com/squareup/moshi/kotlin/codegen/apt/JsonClassCodegenProcessorTest.kt +++ b/kotlin/codegen/src/test/java/com/squareup/moshi/kotlin/codegen/apt/JsonClassCodegenProcessorTest.kt @@ -18,9 +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.squareup.moshi.kotlin.codegen.api.Options.OPTION_INSTANTIATE_ANNOTATIONS import com.tschuchort.compiletesting.KotlinCompilation import com.tschuchort.compiletesting.SourceFile import com.tschuchort.compiletesting.SourceFile.Companion.kotlin @@ -28,6 +28,8 @@ import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder +import org.junit.runner.RunWith +import org.junit.runners.Parameterized import kotlin.reflect.KClass import kotlin.reflect.KClassifier import kotlin.reflect.KType @@ -38,7 +40,24 @@ import kotlin.reflect.full.createType import kotlin.reflect.full.declaredMemberProperties /** Execute kotlinc to confirm that either files are generated or errors are printed. */ -class JsonClassCodegenProcessorTest { +@RunWith(Parameterized::class) +class JsonClassCodegenProcessorTest( + private val languageVersion: String, + private val instantiateAnnotations: Boolean +) { + + companion object { + @JvmStatic + @Parameterized.Parameters(name = "languageVersion={0},instantiateAnnotations={1}") + fun data(): Collection> { + return listOf( + arrayOf("1.5", true), + arrayOf("1.6", true), + arrayOf("1.6", false) + ) + } + } + @Rule @JvmField var temporaryFolder: TemporaryFolder = TemporaryFolder() @Test @@ -455,8 +474,13 @@ class JsonClassCodegenProcessorTest { """ ) ) - assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR) - assertThat(result.messages).contains("JsonQualifier @UpperCase must support FIELD target") + if (languageVersion == "1.5" || !instantiateAnnotations) { + assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR) + assertThat(result.messages).contains("JsonQualifier @UpperCase must support FIELD target") + } else { + // We instantiate directly! + assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) + } } @Test @@ -665,19 +689,34 @@ class JsonClassCodegenProcessorTest { } """.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 (com.squareup.moshi.Moshi); - private com.squareup.moshi.JsonAdapter stringAtMyQualifierAdapter; + "moshi-testPackage.UsingQualifiers" -> { + if (languageVersion == "1.5" || !instantiateAnnotations) { + assertThat(generatedFile.readText()).contains( + """ + -if class testPackage.UsingQualifiers + -keepnames class testPackage.UsingQualifiers + -if class testPackage.UsingQualifiers + -keep class testPackage.UsingQualifiersJsonAdapter { + public (com.squareup.moshi.Moshi); + private com.squareup.moshi.JsonAdapter stringAtMyQualifierAdapter; + } + -if class testPackage.UsingQualifiers + -keep @interface testPackage.MyQualifier + """.trimIndent() + ) + } else { + assertThat(generatedFile.readText()).contains( + """ + -if class testPackage.UsingQualifiers + -keepnames class testPackage.UsingQualifiers + -if class testPackage.UsingQualifiers + -keep class testPackage.UsingQualifiersJsonAdapter { + public (com.squareup.moshi.Moshi); + } + """.trimIndent() + ) } - -if class testPackage.UsingQualifiers - -keep @interface testPackage.MyQualifier - """.trimIndent() - ) + } "moshi-testPackage.MixedTypes" -> assertThat(generatedFile.readText()).contains( """ -if class testPackage.MixedTypes @@ -704,25 +743,46 @@ class JsonClassCodegenProcessorTest { } """.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 (com.squareup.moshi.Moshi,java.lang.reflect.Type[]); - private com.squareup.moshi.JsonAdapter mutableListOfStringAtMyQualifierAdapter; + "moshi-testPackage.Complex" -> { + if (languageVersion == "1.5" || !instantiateAnnotations) { + assertThat(generatedFile.readText()).contains( + """ + -if class testPackage.Complex + -keepnames class testPackage.Complex + -if class testPackage.Complex + -keep class testPackage.ComplexJsonAdapter { + public (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 (java.lang.String,java.util.List,java.lang.Object,int,kotlin.jvm.internal.DefaultConstructorMarker); + } + """.trimIndent() + ) + } else { + assertThat(generatedFile.readText()).contains( + """ + -if class testPackage.Complex + -keepnames class testPackage.Complex + -if class testPackage.Complex + -keep class testPackage.ComplexJsonAdapter { + public (com.squareup.moshi.Moshi,java.lang.reflect.Type[]); + } + -if class testPackage.Complex + -keepnames class kotlin.jvm.internal.DefaultConstructorMarker + -if class testPackage.Complex + -keepclassmembers class testPackage.Complex { + public synthetic (java.lang.String,java.util.List,java.lang.Object,int,kotlin.jvm.internal.DefaultConstructorMarker); + } + """.trimIndent() + ) } - -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 (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 @@ -739,19 +799,34 @@ class JsonClassCodegenProcessorTest { } """.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 (com.squareup.moshi.Moshi); - private com.squareup.moshi.JsonAdapter stringAtNestedQualifierAdapter; + "moshi-testPackage.NestedType.NestedSimple" -> { + if (languageVersion == "1.5" || !instantiateAnnotations) { + 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 (com.squareup.moshi.Moshi); + private com.squareup.moshi.JsonAdapter stringAtNestedQualifierAdapter; + } + -if class testPackage.NestedType${'$'}NestedSimple + -keep @interface testPackage.NestedType${'$'}NestedQualifier + """.trimIndent() + ) + } else { + 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 (com.squareup.moshi.Moshi); + } + """.trimIndent() + ) } - -if class testPackage.NestedType${'$'}NestedSimple - -keep @interface testPackage.NestedType${'$'}NestedQualifier - """.trimIndent() - ) + } else -> error("Unexpected proguard file! ${generatedFile.name}") } } @@ -765,6 +840,8 @@ class JsonClassCodegenProcessorTest { inheritClassPath = true sources = sourceFiles.asList() verbose = false + kotlincArguments = listOf("-language-version", languageVersion) + kaptArgs[OPTION_INSTANTIATE_ANNOTATIONS] = "$instantiateAnnotations" } } diff --git a/kotlin/codegen/src/test/java/com/squareup/moshi/kotlin/codegen/ksp/JsonClassSymbolProcessorTest.kt b/kotlin/codegen/src/test/java/com/squareup/moshi/kotlin/codegen/ksp/JsonClassSymbolProcessorTest.kt index 74f4dc0..3b0e154 100644 --- a/kotlin/codegen/src/test/java/com/squareup/moshi/kotlin/codegen/ksp/JsonClassSymbolProcessorTest.kt +++ b/kotlin/codegen/src/test/java/com/squareup/moshi/kotlin/codegen/ksp/JsonClassSymbolProcessorTest.kt @@ -16,6 +16,7 @@ package com.squareup.moshi.kotlin.codegen.ksp import com.google.common.truth.Truth.assertThat +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 @@ -30,15 +31,41 @@ 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 +import org.junit.runner.RunWith +import org.junit.runners.Parameterized /** Execute kotlinc to confirm that either files are generated or errors are printed. */ -class JsonClassSymbolProcessorTest { +@RunWith(Parameterized::class) +class JsonClassSymbolProcessorTest( + private val languageVersion: String, + private val instantiateAnnotations: Boolean +) { + + companion object { + @JvmStatic + @Parameterized.Parameters(name = "languageVersion={0},instantiateAnnotations={1}") + fun data(): Collection> { + // TODO KSP doesn't recognize language version yet https://github.com/google/ksp/issues/611 + // toe-holds for the future + val runtimeVersion = KotlinVersion.CURRENT + return mutableListOf>().apply { + if (runtimeVersion.major != 1 || runtimeVersion.minor != 6) { + // True by default but still 1.5 + // Can't test when running with 1.6 because lang version is still 1.6 per above comment + add(arrayOf("1.5", true)) + } + // Redundant case, set to false but still 1.5 + add(arrayOf("1.5", false)) + if (runtimeVersion.major != 1 || runtimeVersion.minor != 5) { + // Happy case - 1.6 and true by default + // Can't test when running with 1.5 because lang version is still 1.5 per above comment + add(arrayOf("1.6", true)) + } + // 1.6 but explicitly disabled + add(arrayOf("1.6", false)) + } + } + } @Rule @JvmField var temporaryFolder: TemporaryFolder = TemporaryFolder() @@ -490,8 +517,13 @@ class JsonClassSymbolProcessorTest { """ ) ) - assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR) - assertThat(result.messages).contains("JsonQualifier @UpperCase must support FIELD target") + if (languageVersion == "1.5" || !instantiateAnnotations) { + assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR) + assertThat(result.messages).contains("JsonQualifier @UpperCase must support FIELD target") + } else { + // We instantiate directly! + assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) + } } @Test @@ -704,19 +736,34 @@ class JsonClassSymbolProcessorTest { } """.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 (com.squareup.moshi.Moshi); - private com.squareup.moshi.JsonAdapter stringAtMyQualifierAdapter; + "moshi-testPackage.UsingQualifiers" -> { + if (languageVersion == "1.5" || !instantiateAnnotations) { + assertThat(generatedFile.readText()).contains( + """ + -if class testPackage.UsingQualifiers + -keepnames class testPackage.UsingQualifiers + -if class testPackage.UsingQualifiers + -keep class testPackage.UsingQualifiersJsonAdapter { + public (com.squareup.moshi.Moshi); + private com.squareup.moshi.JsonAdapter stringAtMyQualifierAdapter; + } + -if class testPackage.UsingQualifiers + -keep @interface testPackage.MyQualifier + """.trimIndent() + ) + } else { + assertThat(generatedFile.readText()).contains( + """ + -if class testPackage.UsingQualifiers + -keepnames class testPackage.UsingQualifiers + -if class testPackage.UsingQualifiers + -keep class testPackage.UsingQualifiersJsonAdapter { + public (com.squareup.moshi.Moshi); + } + """.trimIndent() + ) } - -if class testPackage.UsingQualifiers - -keep @interface testPackage.MyQualifier - """.trimIndent() - ) + } "moshi-testPackage.MixedTypes" -> assertThat(generatedFile.readText()).contains( """ -if class testPackage.MixedTypes @@ -743,25 +790,46 @@ class JsonClassSymbolProcessorTest { } """.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 (com.squareup.moshi.Moshi,java.lang.reflect.Type[]); - private com.squareup.moshi.JsonAdapter mutableListOfStringAtMyQualifierAdapter; + "moshi-testPackage.Complex" -> { + if (languageVersion == "1.5" || !instantiateAnnotations) { + assertThat(generatedFile.readText()).contains( + """ + -if class testPackage.Complex + -keepnames class testPackage.Complex + -if class testPackage.Complex + -keep class testPackage.ComplexJsonAdapter { + public (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 (java.lang.String,java.util.List,java.lang.Object,int,kotlin.jvm.internal.DefaultConstructorMarker); + } + """.trimIndent() + ) + } else { + assertThat(generatedFile.readText()).contains( + """ + -if class testPackage.Complex + -keepnames class testPackage.Complex + -if class testPackage.Complex + -keep class testPackage.ComplexJsonAdapter { + public (com.squareup.moshi.Moshi,java.lang.reflect.Type[]); + } + -if class testPackage.Complex + -keepnames class kotlin.jvm.internal.DefaultConstructorMarker + -if class testPackage.Complex + -keepclassmembers class testPackage.Complex { + public synthetic (java.lang.String,java.util.List,java.lang.Object,int,kotlin.jvm.internal.DefaultConstructorMarker); + } + """.trimIndent() + ) } - -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 (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 @@ -778,19 +846,34 @@ class JsonClassSymbolProcessorTest { } """.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 (com.squareup.moshi.Moshi); - private com.squareup.moshi.JsonAdapter stringAtNestedQualifierAdapter; + "moshi-testPackage.NestedType.NestedSimple" -> { + if (languageVersion == "1.5" || !instantiateAnnotations) { + 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 (com.squareup.moshi.Moshi); + private com.squareup.moshi.JsonAdapter stringAtNestedQualifierAdapter; + } + -if class testPackage.NestedType${'$'}NestedSimple + -keep @interface testPackage.NestedType${'$'}NestedQualifier + """.trimIndent() + ) + } else { + 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 (com.squareup.moshi.Moshi); + } + """.trimIndent() + ) } - -if class testPackage.NestedType${'$'}NestedSimple - -keep @interface testPackage.NestedType${'$'}NestedQualifier - """.trimIndent() - ) + } else -> error("Unexpected proguard file! ${generatedFile.name}") } } @@ -805,20 +888,12 @@ class JsonClassSymbolProcessorTest { sources = sourceFiles.asList() verbose = false kspIncremental = true // The default now + kotlincArguments = listOf("-language-version", languageVersion) + kspArgs[Options.OPTION_INSTANTIATE_ANNOTATIONS] = "$instantiateAnnotations" } } 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) - } } diff --git a/kotlin/tests/build.gradle.kts b/kotlin/tests/build.gradle.kts index 475bd36..4b808e2 100644 --- a/kotlin/tests/build.gradle.kts +++ b/kotlin/tests/build.gradle.kts @@ -19,7 +19,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { kotlin("jvm") kotlin("kapt") apply false - alias(libs.plugins.ksp) apply false + id("com.google.devtools.ksp") apply false } val useKsp = hasProperty("useKsp") @@ -34,11 +34,14 @@ tasks.withType().configureEach { jvmArgs("--add-opens=java.base/java.io=ALL-UNNAMED") } +val useWError = findProperty("kotlinLanguageVersion")?.toString() + ?.startsWith("1.5") + ?: false tasks.withType().configureEach { kotlinOptions { + allWarningsAsErrors = useWError @Suppress("SuspiciousCollectionReassignment") freeCompilerArgs += listOf( - "-Werror", "-Xopt-in=kotlin.ExperimentalStdlibApi" ) }