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

@@ -15,6 +15,7 @@
*/
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.Resolver
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.KSPropertyDeclaration
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.Modifier
import com.google.devtools.ksp.symbol.Nullability
import com.google.devtools.ksp.symbol.Origin
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ClassName
@@ -258,7 +260,8 @@ private fun KSPropertyDeclaration.toPropertySpec(
): PropertySpec {
return PropertySpec.builder(
name = simpleName.getShortName(),
type = resolvedType.toTypeName(typeParameterResolver).unwrapTypeAlias(),
type = resolvedType.toTypeName(typeParameterResolver).unwrapTypeAlias()
.fixTypeAliasNullability(resolvedType),
)
.mutable(isMutable)
.addModifiers(modifiers.map { KModifier.valueOf(it.name) })
@@ -278,3 +281,13 @@ private fun KSPropertyDeclaration.toPropertySpec(
}
.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
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.KSDeclaration
import com.google.devtools.ksp.symbol.KSType
import com.google.devtools.ksp.symbol.KSValueArgument
import java.lang.reflect.InvocationHandler
@@ -32,15 +32,6 @@ 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 &&
@@ -74,46 +65,67 @@ private fun KSAnnotation.createInvocationHandler(clazz: Class<*>): InvocationHan
"$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)
}
val argument = arguments.first { it.name?.asString() == method.name }
when (val result = argument.value ?: method.defaultValue) {
is Proxy -> result
is List<*> -> {
val value = { result.asArray(method) }
val value = { result.asArray(method, clazz) }
cache.getOrPut(Pair(method.returnType, result), value)
}
else -> {
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 -> {
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)
cache.getOrPut(Pair(method.returnType, result)) {
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" -> {
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)
}
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
}
}
@@ -134,42 +146,28 @@ private fun KSAnnotation.asAnnotation(
}
@Suppress("UNCHECKED_CAST")
private fun List<*>.asArray(method: Method) =
private fun List<*>.asArray(method: Method, proxyClass: Class<*>) =
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.Class" -> (this as List<KSType>).asClasses(proxyClass).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}")
}
}
@@ -192,9 +190,10 @@ 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 if (this is KSClassDeclaration) {
this.simpleName.getShortName()
} else {
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 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
}
}