Skip to content

Fix getJvmName for @JvmRecord data class properties#2813

Open
MariusVolkhart wants to merge 4 commits into
google:mainfrom
MariusVolkhart:mv/record
Open

Fix getJvmName for @JvmRecord data class properties#2813
MariusVolkhart wants to merge 4 commits into
google:mainfrom
MariusVolkhart:mv/record

Conversation

@MariusVolkhart

Copy link
Copy Markdown

@JvmRecord data classes compile to Java records, whose component accessors use bare property names (e.g. name()) rather than bean-style getters (getName()). ResolverAAImpl.getJvmName unconditionally used JvmAbi.getterName() which always added the get prefix.

Add a check for the @JvmRecord annotation alongside the existing annotation class check, since both use bare property names as accessor names.

Fixes #2812

@google-cla

google-cla Bot commented Feb 26, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@MariusVolkhart

MariusVolkhart commented Feb 26, 2026

Copy link
Copy Markdown
Author

CLA violation is due to Claude Code. I didn't see anything in the CONTRIBUTING.md indicating agentic AI was not allowed, and wanted to be clear on the fact that it was used as part of this change. Please advise if you want me to make changes or are unable to accept the PR as a result. Thank you.

@hfmehmed

hfmehmed commented Mar 5, 2026

Copy link
Copy Markdown
Collaborator

To me it seems like the tool requires all the owners of the commit (you and anthropic agent) need to sign the CLA. Obviously, the agent itself can't sign it so probably to the time being the workaround is not commit code using the agent

@jaschdoc jaschdoc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey thank you for opening the PR and apologies for the slow response. I left some comments that aim to improve the test suite and readability. Cheers!

Comment on lines +8 to +12
@JvmRecord
data class TestRecordClass(val x: Int, val y: String)
// FILE: TestLibRecordClass.kt
@JvmRecord
data class TestLibRecordClass(val x: Int, val y: String)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How are these classes distinct except for the name? It's better if the classes are a bit more diverse. I'm thinking an approximation of a matrix of the following:

  • Classes with different number of parameters
  • Different property names
  • Classes extending other types
  • Classes with type parameters
  • Classes marked as JvmRecords that override a val declared in a non-annotation interface, e.g., interface Example { val x: Int }
  • Classes with different names (like you already have)
  • Classes with var and val properties

If you have type parameters and or normal parameters and a class that extends another type, then it's also a good idea to override some parameters and add some locally, e.g.,

interface Example<A> {
    val x: A
}

data class Ext<A, B>(override val x: A, val y: B) : Example<A>

@MariusVolkhart MariusVolkhart Jun 12, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Records can't extend classes or have var constructor properties, so those cells became interface supertypes (incl. the generic override shape) and a body var without a backing field — noted in the test file. Also added an aliased import, @get:JvmName, and a library-module record. Expected names verified against javap.

class JvmNameRecordProcessor : AbstractTestProcessor() {
val results = mutableListOf<String>()
override fun toResult(): List<String> {
return results

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be sorted to ensure results are reproducible.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — results are sorted now.

Comment on lines +460 to +466
if (containingClass?.classKind == ClassKind.ANNOTATION_CLASS ||
containingClass != null && containingClass.annotations.any {
it.annotationType.resolve().declaration.qualifiedName?.asString() == "kotlin.jvm.JvmRecord"
}
) {
return name
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can improve the readability of this by extracting some variables so it reads

if (isAnnotationClass || isJvmRecord) {
    return name
}

Additionally, we can shortcircuit the expensive type resolution by checking for the short name first in the lambda:

any {
    it.shortName.asString() == "JvmRecord" ||
        it.annotationType.resolve().declaration.qualifiedName?.asString() == "kotlin.jvm.Record
}

Lastly, maybe we can rename name to propertyName so it's clearer what the name. I know you didn't change it, but let's improve it.

@MariusVolkhart MariusVolkhart Jun 12, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, with one tweak: comparing source short names breaks on import aliases (import kotlin.jvm.JvmRecord as JR), so the check compares ClassIds on the analysis-api symbol instead — same as explictJvmName does for @JvmName just above in util.kt.

import org.junit.jupiter.api.parallel.ExecutionMode

@Execution(ExecutionMode.SAME_THREAD)
class KSPAA17Test : AbstractKSPAATest() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you try adding the test to the existing test suite?

@MariusVolkhart MariusVolkhart Jun 12, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes — a // JVM_TARGET: 17 directive in the testData collides with the hardcoded default in AbstractKSPTest ("Too many values passed to JVM_TARGET: [1.8, 17]"); that's why the separate class existed. Removed the hardcoded default so file directives work, and the test now lives in KSPAATest. Isolated in its own commit — easy to drop if you'd rather keep this PR minimal.

cls.getAllProperties().map {
"(${it.getter?.let { resolver.getJvmName(it) }}, " +
"${it.setter?.let { resolver.getJvmName(it) }})"
}.toList().joinToString()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's more stable if you use resolver.getSymbolsWithAnnotation("kotlin.jvm.JvmRecord") and filter for class declaration instances.
It might also be a bit cleaner if you flatmap over getAllProperties and then return a list of size at most two, that contains the getter and setter names which you can the call joinToString on, e.g.,

getAllProperties().flatMap {
    // create a list that contains the jvm getter name if it exists and the setter name if it exists
}

The names should probably also be qualified / prefixed with the class names.

@MariusVolkhart MariusVolkhart Jun 12, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. One wrinkle: getSymbolsWithAnnotation only returns current-compilation symbols, so the library-module record is looked up by name. That path exposed a real bug — @JvmRecord isn't retained in class files, so library records were reported as getW. Fixed in a separate commit by recognizing the java.lang.Record supertype instead.

@jaschdoc

Copy link
Copy Markdown
Collaborator

@MariusVolkhart, just pinging to see if you still want to contribute :)

@JvmRecord data classes compile to Java records, whose component
accessors use bare property names (e.g. name()) rather than bean-style
getters (getName()). ResolverAAImpl.getJvmName unconditionally used
JvmAbi.getterName() which always added the get prefix.

Add a check for the @JvmRecord annotation alongside the existing
annotation class check, since both use bare property names as
accessor names.

Fixes google#2812
The KSPAA17Test class existed only to run one test at JVM target 17,
because a // JVM_TARGET: directive in a testData file collides with the
hardcoded JVM_TARGET default in AbstractKSPTest: the test framework
merges default directives with file-level ones instead of overriding,
failing with "Too many values passed to JVM_TARGET: [1.8, 17]".

Remove the hardcoded default and fall back to JvmTarget.DEFAULT at the
point of consumption in AbstractKSPAATest, which is the same value the
directive used to inject. Any test can now pick its JVM target with a
one-line directive instead of a dedicated test class, and the record
test moves into KSPAATest alongside the other jvmName tests.
The @JvmRecord annotation is not retained in class files, so the
annotation-based check never matches a record class consumed from a
dependency jar, and accessors were reported with the bean-style get
prefix. The analysis API exposes java.lang.Record as a supertype of
deserialized record classes, so recognize library records by that
supertype, keeping the annotation check for source classes where the
implicit supertype is not present.

Covering this required two test-infrastructure fixes: library-module
compilation now forwards the module's JVM target (records need 16+),
and the compiler invocation now fails the test on a non-OK exit code
instead of silently producing no class files, which previously
surfaced as misleading downstream resolution failures.

The testData also gains an @get:JvmName record property documenting
that an explicit JvmName takes precedence over record accessor naming.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

getJvmName returns incorrect accessor names for @JvmRecord data classes

3 participants