-
Notifications
You must be signed in to change notification settings - Fork 380
Description
Summary
Resolver.getJvmName(accessor) returns getName for a property getter on a @JvmRecord Kotlin data class, but the actual JVM bytecode uses record component accessors (name(), not getName()). This causes downstream frameworks (e.g. Micronaut Data) that rely on getJvmName to generate incorrect reflective calls, resulting in NoSuchMethodError at runtime.
Environment
- KSP version: 2.2.21-2.0.5 (also reproducible on
mainat 2.3.0) - Kotlin version: 2.2.21+
- JDK: 17+
Reproducer
Given this Kotlin source:
@JvmRecord
data class Author(
val id: Int,
val name: String,
)A KSP processor calling:
val cls = resolver.getClassDeclarationByName("Author")!!
val nameGetter = cls.getAllProperties().first { it.simpleName.asString() == "name" }.getter!!
val jvmName = resolver.getJvmName(nameGetter)
println(jvmName) // prints "getName" — should print "name"Expected behavior
resolver.getJvmName(getter) should return "name" (the record component accessor), matching the actual JVM bytecode produced by the Kotlin compiler for @JvmRecord classes.
Actual behavior
Returns "getName" (the bean-style getter), which does not exist on the compiled Java record class.
Root cause
In ResolverAAImpl.getJvmName(accessor: KSPropertyAccessor) (line 445 of ResolverAAImpl.kt), the name is computed as:
val name = accessor.receiver.simpleName.asString()
val prefixedName = when (accessor) {
is KSPropertyGetter -> JvmAbi.getterName(name) // unconditionally → "getName"
is KSPropertySetter -> JvmAbi.setterName(name)
else -> ""
}JvmAbi.getterName() unconditionally adds a get prefix. There is no check for whether the containing class is a @JvmRecord class, which suppresses the prefix at the JVM level.
Why KSP1 worked correctly
The KSP1 implementation (ResolverImpl) delegated to typeMapper.mapFunctionName(descriptor, OwnerKind.IMPLEMENTATION), which is the Kotlin compiler's internal KotlinTypeMapper. That function explicitly checks for java.lang.Record supertypes and returns the bare property name when the class is a record. KSP2 replaced this with a manual JvmAbi.getterName() call and lost the record-awareness.
Impact
Any KSP processor that uses getJvmName to determine how to reflectively access properties on @JvmRecord classes will get the wrong method name. Known affected framework: Micronaut Data's micronaut-inject-kotlin processor, which generates bean introspection code using getJvmName — causing NoSuchMethodError at runtime for all @JvmRecord entities.
Proposed fix
In ResolverAAImpl.getJvmName(accessor), before computing the prefixed name, check whether the containing class has the kotlin.jvm.JvmRecord annotation. If so, return the bare property name (matching the Kotlin compiler's bytecode output):
// Annotation classes and @JvmRecord data classes both use bare property names
// as accessor names (no get/set prefix).
if (containingClass?.classKind == ClassKind.ANNOTATION_CLASS ||
containingClass != null && containingClass.annotations.any {
it.annotationType.resolve().declaration.qualifiedName?.asString() == "kotlin.jvm.JvmRecord"
}
) {
return name
}For setters, records don't have setters at all (record components are final), so the setter branch is unaffected.