Kotlin 2.1 and friends (#1966)

* Kotlin 2.1 and friends

* fix kotlin 2.1 aliases nullability (#1982)

---------

Co-authored-by: Florian LE FICHER <florian.leficher@gmail.com>
This commit is contained in:
Jake Wharton
2025-08-27 20:46:36 -04:00
committed by GitHub
parent 956776edbb
commit ae4e728e7a
9 changed files with 139 additions and 96 deletions

View File

@@ -2,11 +2,10 @@
autoService = "1.1.1" autoService = "1.1.1"
jdk = "21" jdk = "21"
jvmTarget = "1.8" jvmTarget = "1.8"
kotlin = "2.0.0" kotlin = "2.1.21"
# No 0.5.0 full release yet due to KSP's 1.0.21 release being busted for CLI/programmatic use kotlinCompileTesting = "0.7.1"
kotlinCompileTesting = "0.5.0-alpha07"
kotlinpoet = "2.2.0" kotlinpoet = "2.2.0"
ksp = "2.0.21-1.0.25" ksp = "2.1.21-2.0.1"
[plugins] [plugins]
dokka = { id = "org.jetbrains.dokka", version = "2.0.0" } dokka = { id = "org.jetbrains.dokka", version = "2.0.0" }

View File

@@ -15,6 +15,7 @@
*/ */
package com.squareup.moshi.kotlin.codegen.ksp package com.squareup.moshi.kotlin.codegen.ksp
import com.google.devtools.ksp.getClassDeclarationByName
import com.google.devtools.ksp.processing.KSPLogger import com.google.devtools.ksp.processing.KSPLogger
import com.google.devtools.ksp.processing.Resolver import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.symbol.KSClassDeclaration import com.google.devtools.ksp.symbol.KSClassDeclaration

View File

@@ -30,8 +30,10 @@ import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSDeclaration import com.google.devtools.ksp.symbol.KSDeclaration
import com.google.devtools.ksp.symbol.KSPropertyDeclaration import com.google.devtools.ksp.symbol.KSPropertyDeclaration
import com.google.devtools.ksp.symbol.KSType import com.google.devtools.ksp.symbol.KSType
import com.google.devtools.ksp.symbol.KSTypeAlias
import com.google.devtools.ksp.symbol.KSTypeParameter import com.google.devtools.ksp.symbol.KSTypeParameter
import com.google.devtools.ksp.symbol.Modifier import com.google.devtools.ksp.symbol.Modifier
import com.google.devtools.ksp.symbol.Nullability
import com.google.devtools.ksp.symbol.Origin import com.google.devtools.ksp.symbol.Origin
import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.ClassName
@@ -258,7 +260,8 @@ private fun KSPropertyDeclaration.toPropertySpec(
): PropertySpec { ): PropertySpec {
return PropertySpec.builder( return PropertySpec.builder(
name = simpleName.getShortName(), name = simpleName.getShortName(),
type = resolvedType.toTypeName(typeParameterResolver).unwrapTypeAlias(), type = resolvedType.toTypeName(typeParameterResolver).unwrapTypeAlias()
.fixTypeAliasNullability(resolvedType),
) )
.mutable(isMutable) .mutable(isMutable)
.addModifiers(modifiers.map { KModifier.valueOf(it.name) }) .addModifiers(modifiers.map { KModifier.valueOf(it.name) })
@@ -278,3 +281,13 @@ private fun KSPropertyDeclaration.toPropertySpec(
} }
.build() .build()
} }
private fun TypeName.fixTypeAliasNullability(resolvedType: KSType): TypeName {
return if (resolvedType.declaration is KSTypeAlias) {
copy(
nullable = resolvedType.nullability == Nullability.NULLABLE,
)
} else {
this
}
}

View File

@@ -16,10 +16,10 @@
*/ */
package com.squareup.moshi.kotlin.codegen.ksp 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.KSAnnotated
import com.google.devtools.ksp.symbol.KSAnnotation import com.google.devtools.ksp.symbol.KSAnnotation
import com.google.devtools.ksp.symbol.KSClassDeclaration import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSDeclaration
import com.google.devtools.ksp.symbol.KSType import com.google.devtools.ksp.symbol.KSType
import com.google.devtools.ksp.symbol.KSValueArgument import com.google.devtools.ksp.symbol.KSValueArgument
import java.lang.reflect.InvocationHandler import java.lang.reflect.InvocationHandler
@@ -32,15 +32,6 @@ import kotlin.reflect.KClass
* Copied experimental utilities from KSP. * 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> { internal fun <T : Annotation> KSAnnotated.getAnnotationsByType(annotationKClass: KClass<T>): Sequence<T> {
return this.annotations.filter { return this.annotations.filter {
it.shortName.getShortName() == annotationKClass.simpleName && it.shortName.getShortName() == annotationKClass.simpleName &&
@@ -74,46 +65,67 @@ private fun KSAnnotation.createInvocationHandler(clazz: Class<*>): InvocationHan
"$methodName=$value" "$methodName=$value"
}.toList() }.toList()
} else { } else {
val argument = try { val argument = arguments.first { it.name?.asString() == method.name }
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) { when (val result = argument.value ?: method.defaultValue) {
is Proxy -> result is Proxy -> result
is List<*> -> { is List<*> -> {
val value = { result.asArray(method) } val value = { result.asArray(method, clazz) }
cache.getOrPut(Pair(method.returnType, result), value) cache.getOrPut(Pair(method.returnType, result), value)
} }
else -> { else -> {
when { when {
// Workaround for java annotation value array type
// https://github.com/google/ksp/issues/1329
method.returnType.isArray -> {
if (result !is Array<*>) {
val value = { result.asArray(method, clazz) }
cache.getOrPut(Pair(method.returnType, value), value)
} else {
throw IllegalStateException("unhandled value type, please file a bug at https://github.com/google/ksp/issues/new")
}
}
method.returnType.isEnum -> { method.returnType.isEnum -> {
val value = { result.asEnum(method.returnType) } val value = { result.asEnum(method.returnType) }
cache.getOrPut(Pair(method.returnType, result), value) cache.getOrPut(Pair(method.returnType, result), value)
} }
method.returnType.isAnnotation -> { method.returnType.isAnnotation -> {
val value = { (result as KSAnnotation).asAnnotation(method.returnType) } val value = { (result as KSAnnotation).asAnnotation(method.returnType) }
cache.getOrPut(Pair(method.returnType, result), value) cache.getOrPut(Pair(method.returnType, result), value)
} }
method.returnType.name == "java.lang.Class" -> { method.returnType.name == "java.lang.Class" -> {
val value = { (result as KSType).asClass() } cache.getOrPut(Pair(method.returnType, result)) {
cache.getOrPut(Pair(method.returnType, result), value) when (result) {
is KSType -> result.asClass(clazz)
// Handles com.intellij.psi.impl.source.PsiImmediateClassType using reflection
// since api doesn't contain a reference to this
else -> Class.forName(
result.javaClass.methods
.first { it.name == "getCanonicalText" }
.invoke(result, false) as String,
)
}
}
} }
method.returnType.name == "byte" -> { method.returnType.name == "byte" -> {
val value = { result.asByte() } val value = { result.asByte() }
cache.getOrPut(Pair(method.returnType, result), value) cache.getOrPut(Pair(method.returnType, result), value)
} }
method.returnType.name == "short" -> { method.returnType.name == "short" -> {
val value = { result.asShort() } val value = { result.asShort() }
cache.getOrPut(Pair(method.returnType, result), value) cache.getOrPut(Pair(method.returnType, result), value)
} }
method.returnType.name == "long" -> {
val value = { result.asLong() }
cache.getOrPut(Pair(method.returnType, result), value)
}
method.returnType.name == "float" -> {
val value = { result.asFloat() }
cache.getOrPut(Pair(method.returnType, result), value)
}
method.returnType.name == "double" -> {
val value = { result.asDouble() }
cache.getOrPut(Pair(method.returnType, result), value)
}
else -> result // original value else -> result // original value
} }
} }
@@ -134,42 +146,28 @@ private fun KSAnnotation.asAnnotation(
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
private fun List<*>.asArray(method: Method) = private fun List<*>.asArray(method: Method, proxyClass: Class<*>) =
when (method.returnType.componentType.name) { when (method.returnType.componentType.name) {
"boolean" -> (this as List<Boolean>).toBooleanArray() "boolean" -> (this as List<Boolean>).toBooleanArray()
"byte" -> (this as List<Byte>).toByteArray() "byte" -> (this as List<Byte>).toByteArray()
"short" -> (this as List<Short>).toShortArray() "short" -> (this as List<Short>).toShortArray()
"char" -> (this as List<Char>).toCharArray() "char" -> (this as List<Char>).toCharArray()
"double" -> (this as List<Double>).toDoubleArray() "double" -> (this as List<Double>).toDoubleArray()
"float" -> (this as List<Float>).toFloatArray() "float" -> (this as List<Float>).toFloatArray()
"int" -> (this as List<Int>).toIntArray() "int" -> (this as List<Int>).toIntArray()
"long" -> (this as List<Long>).toLongArray() "long" -> (this as List<Long>).toLongArray()
"java.lang.Class" -> (this as List<KSType>).asClasses(proxyClass).toTypedArray()
"java.lang.Class" -> (this as List<KSType>).map {
Class.forName(it.declaration.qualifiedName!!.asString())
}.toTypedArray()
"java.lang.String" -> (this as List<String>).toTypedArray() "java.lang.String" -> (this as List<String>).toTypedArray()
else -> { // arrays of enums or annotations else -> { // arrays of enums or annotations
when { when {
method.returnType.componentType.isEnum -> { method.returnType.componentType.isEnum -> {
this.toArray(method) { result -> result.asEnum(method.returnType.componentType) } this.toArray(method) { result -> result.asEnum(method.returnType.componentType) }
} }
method.returnType.componentType.isAnnotation -> { method.returnType.componentType.isAnnotation -> {
this.toArray(method) { result -> this.toArray(method) { result ->
(result as KSAnnotation).asAnnotation(method.returnType.componentType) (result as KSAnnotation).asAnnotation(method.returnType.componentType)
} }
} }
else -> throw IllegalStateException("Unable to process type ${method.returnType.componentType.name}") else -> throw IllegalStateException("Unable to process type ${method.returnType.componentType.name}")
} }
} }
@@ -192,9 +190,10 @@ private fun <T> Any.asEnum(returnType: Class<T>): T =
returnType.getDeclaredMethod("valueOf", String::class.java) returnType.getDeclaredMethod("valueOf", String::class.java)
.invoke( .invoke(
null, null,
// Change from upstream KSP - https://github.com/google/ksp/pull/685
if (this is KSType) { if (this is KSType) {
this.declaration.simpleName.getShortName() this.declaration.simpleName.getShortName()
} else if (this is KSClassDeclaration) {
this.simpleName.getShortName()
} else { } else {
this.toString() this.toString()
}, },
@@ -204,4 +203,53 @@ private fun Any.asByte(): Byte = if (this is Int) this.toByte() else this as Byt
private fun Any.asShort(): Short = if (this is Int) this.toShort() else this as Short 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()) private fun Any.asLong(): Long = if (this is Int) this.toLong() else this as Long
private fun Any.asFloat(): Float = if (this is Int) this.toFloat() else this as Float
private fun Any.asDouble(): Double = if (this is Int) this.toDouble() else this as Double
// for Class/KClass member
internal class KSTypeNotPresentException(val ksType: KSType, cause: Throwable) : RuntimeException(cause)
// for Class[]/Array<KClass<*>> member.
internal class KSTypesNotPresentException(val ksTypes: List<KSType>, cause: Throwable) : RuntimeException(cause)
private fun KSType.asClass(proxyClass: Class<*>) = try {
Class.forName(this.declaration.toJavaClassName(), true, proxyClass.classLoader)
} catch (e: Exception) {
throw KSTypeNotPresentException(this, e)
}
private fun List<KSType>.asClasses(proxyClass: Class<*>) = try {
this.map { type -> type.asClass(proxyClass) }
} catch (e: Exception) {
throw KSTypesNotPresentException(this, e)
}
private fun Any.asArray(method: Method, proxyClass: Class<*>) = listOf(this).asArray(method, proxyClass)
private fun KSDeclaration.toJavaClassName(): String {
val nameDelimiter = '.'
val packageNameString = packageName.asString()
val qualifiedNameString = qualifiedName!!.asString()
val simpleNames = qualifiedNameString
.removePrefix("${packageNameString}$nameDelimiter")
.split(nameDelimiter)
return if (simpleNames.size > 1) {
buildString {
append(packageNameString)
append(nameDelimiter)
simpleNames.forEachIndexed { index, s ->
if (index > 0) {
append('$')
}
append(s)
}
}
} else {
qualifiedNameString
}
}

View File

@@ -35,8 +35,9 @@ tasks.withType<Test>().configureEach {
tasks.withType<KotlinCompile>().configureEach { tasks.withType<KotlinCompile>().configureEach {
compilerOptions { compilerOptions {
allWarningsAsErrors.set(true) allWarningsAsErrors.set(true)
freeCompilerArgs.add( freeCompilerArgs.addAll(
"-opt-in=kotlin.ExperimentalStdlibApi", "-opt-in=kotlin.ExperimentalStdlibApi",
"-Xannotation-default-target=param-property",
) )
} }
} }

View File

@@ -36,8 +36,9 @@ tasks.withType<Test>().configureEach {
tasks.withType<KotlinCompile>().configureEach { tasks.withType<KotlinCompile>().configureEach {
compilerOptions { compilerOptions {
allWarningsAsErrors.set(true) allWarningsAsErrors.set(true)
freeCompilerArgs.add( freeCompilerArgs.addAll(
"-opt-in=kotlin.ExperimentalStdlibApi", "-opt-in=kotlin.ExperimentalStdlibApi",
"-Xannotation-default-target=param-property",
) )
} }
} }

View File

@@ -487,7 +487,7 @@ class DualKotlinTest {
val parameterized: GenericClass<TypeAlias>, val parameterized: GenericClass<TypeAlias>,
val wildcardIn: GenericClass<in TypeAlias>, val wildcardIn: GenericClass<in TypeAlias>,
val wildcardOut: GenericClass<out TypeAlias>, val wildcardOut: GenericClass<out TypeAlias>,
val complex: GenericClass<GenericTypeAlias>?, val complex: GenericClass<GenericTypeAlias?>?,
) )
// Regression test for https://github.com/square/moshi/issues/991 // Regression test for https://github.com/square/moshi/issues/991

View File

@@ -3,7 +3,8 @@ import com.vanniktech.maven.publish.KotlinJvm
import com.vanniktech.maven.publish.MavenPublishBaseExtension import com.vanniktech.maven.publish.MavenPublishBaseExtension
import org.gradle.jvm.tasks.Jar import org.gradle.jvm.tasks.Jar
import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation import org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompilerOptions
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation.Companion.MAIN_COMPILATION_NAME
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
@@ -12,49 +13,29 @@ plugins {
id("org.jetbrains.dokka") id("org.jetbrains.dokka")
} }
val mainSourceSet by sourceSets.named("main") kotlin.target {
val java16: SourceSet by sourceSets.creating { val main = compilations.getByName(MAIN_COMPILATION_NAME)
java { val java16 =
srcDir("src/main/java16") compilations.create("java16") {
associateWith(main)
defaultSourceSet.kotlin.srcDir("src/main/java16")
compileJavaTaskProvider.configure {
options.release = 16
}
compileTaskProvider.configure {
(compilerOptions as KotlinJvmCompilerOptions).jvmTarget = JvmTarget.JVM_16
} }
}
// We use newer JDKs but target 16 for maximum compatibility
val service = project.extensions.getByType<JavaToolchainService>()
val customLauncher =
service.launcherFor {
languageVersion.set(libs.versions.jdk.map(JavaLanguageVersion::of))
} }
tasks.named<JavaCompile>("compileJava16Java") { // Package our actual RecordJsonAdapter from java16 sources in and denote it as an MRJAR
options.release.set(16) tasks.named<Jar>(artifactsTaskName) {
}
tasks.named<KotlinCompile>("compileJava16Kotlin") {
kotlinJavaToolchain.toolchain.use(customLauncher)
compilerOptions.jvmTarget.set(JvmTarget.JVM_16)
}
// Grant our java16 sources access to internal APIs in the main source set
kotlin.target.compilations.run {
getByName("java16")
.associateWith(getByName(KotlinCompilation.MAIN_COMPILATION_NAME))
}
// Package our actual RecordJsonAdapter from java16 sources in and denote it as an MRJAR
tasks.named<Jar>("jar") {
from(java16.output) { from(java16.output) {
into("META-INF/versions/16") into("META-INF/versions/16")
exclude("META-INF")
} }
manifest { manifest {
attributes("Multi-Release" to "true") attributes("Multi-Release" to "true")
} }
}
configurations {
"java16Implementation" {
extendsFrom(api.get())
extendsFrom(implementation.get())
} }
} }
@@ -78,8 +59,6 @@ tasks
} }
dependencies { dependencies {
// So the j16 source set can "see" main Moshi sources
"java16Implementation"(mainSourceSet.output)
compileOnly(libs.jsr305) compileOnly(libs.jsr305)
api(libs.okio) api(libs.okio)

1
moshi/gradle.properties Normal file
View File

@@ -0,0 +1 @@
kotlin.build.archivesTaskOutputAsFriendModule=false