From 3ea3c0e69227ddd7eb17e49892319169a68ae7f2 Mon Sep 17 00:00:00 2001 From: Marius Volkhart Date: Thu, 26 Feb 2026 12:20:00 -0500 Subject: [PATCH 1/4] Fix getJvmName for @JvmRecord data class properties @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 --- .../devtools/ksp/impl/ResolverAAImpl.kt | 15 +++++-- .../google/devtools/ksp/test/KSPAA17Test.kt | 43 +++++++++++++++++++ kotlin-analysis-api/testData/jvmNameRecord.kt | 12 ++++++ .../ksp/processor/JvmNameRecordProcessor.kt | 28 ++++++++++++ 4 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/KSPAA17Test.kt create mode 100644 kotlin-analysis-api/testData/jvmNameRecord.kt create mode 100644 test-utils/src/main/kotlin/com/google/devtools/ksp/processor/JvmNameRecordProcessor.kt diff --git a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/ResolverAAImpl.kt b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/ResolverAAImpl.kt index a48634de78..98ebfb464a 100644 --- a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/ResolverAAImpl.kt +++ b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/ResolverAAImpl.kt @@ -577,11 +577,18 @@ class ResolverAAImpl( return it } - if (accessor.receiver.closestClassDeclaration()?.classKind == ClassKind.ANNOTATION_CLASS) { - return accessor.receiver.simpleName.asString() - } - val name = accessor.receiver.simpleName.asString() + val containingClass = accessor.receiver.closestClassDeclaration() + + // 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 + } // https://kotlinlang.org/docs/java-to-kotlin-interop.html#properties val prefixedName = when (accessor) { is KSPropertyGetter -> JvmAbi.getterName(name) diff --git a/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/KSPAA17Test.kt b/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/KSPAA17Test.kt new file mode 100644 index 0000000000..e03fb8f1c0 --- /dev/null +++ b/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/KSPAA17Test.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2022 Google LLC + * Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.devtools.ksp.test + +import org.jetbrains.kotlin.config.JvmTarget +import org.jetbrains.kotlin.test.TestMetadata +import org.jetbrains.kotlin.test.builders.TestConfigurationBuilder +import org.jetbrains.kotlin.test.directives.JvmEnvironmentConfigurationDirectives +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.parallel.Execution +import org.junit.jupiter.api.parallel.ExecutionMode + +@Execution(ExecutionMode.SAME_THREAD) +class KSPAA17Test : AbstractKSPAATest() { + + override fun configureTest(builder: TestConfigurationBuilder) { + builder.defaultDirectives { + -JvmEnvironmentConfigurationDirectives.JVM_TARGET + JvmEnvironmentConfigurationDirectives.JVM_TARGET with JvmTarget.JVM_17 + } + } + + @TestMetadata("jvmNameRecord.kt") + @Test + fun testJvmNameRecord() { + runTest("../kotlin-analysis-api/testData/jvmNameRecord.kt") + } +} diff --git a/kotlin-analysis-api/testData/jvmNameRecord.kt b/kotlin-analysis-api/testData/jvmNameRecord.kt new file mode 100644 index 0000000000..a4d239b63a --- /dev/null +++ b/kotlin-analysis-api/testData/jvmNameRecord.kt @@ -0,0 +1,12 @@ +// TEST PROCESSOR: JvmNameRecordProcessor +// EXPECTED: +// (x, null), (y, null) +// (x, null), (y, null) +// END +// MODULE: main +// FILE: TestRecordClass.kt +@JvmRecord +data class TestRecordClass(val x: Int, val y: String) +// FILE: TestLibRecordClass.kt +@JvmRecord +data class TestLibRecordClass(val x: Int, val y: String) diff --git a/test-utils/src/main/kotlin/com/google/devtools/ksp/processor/JvmNameRecordProcessor.kt b/test-utils/src/main/kotlin/com/google/devtools/ksp/processor/JvmNameRecordProcessor.kt new file mode 100644 index 0000000000..38e67b861e --- /dev/null +++ b/test-utils/src/main/kotlin/com/google/devtools/ksp/processor/JvmNameRecordProcessor.kt @@ -0,0 +1,28 @@ +package com.google.devtools.ksp.processor + +import com.google.devtools.ksp.KspExperimental +import com.google.devtools.ksp.getClassDeclarationByName +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.symbol.KSAnnotated + +class JvmNameRecordProcessor : AbstractTestProcessor() { + val results = mutableListOf() + override fun toResult(): List { + return results + } + + @OptIn(KspExperimental::class) + override fun process(resolver: Resolver): List { + listOf("TestRecordClass", "TestLibRecordClass").forEach { clsName -> + resolver.getClassDeclarationByName(clsName)?.let { cls -> + results.add( + cls.getAllProperties().map { + "(${it.getter?.let { resolver.getJvmName(it) }}, " + + "${it.setter?.let { resolver.getJvmName(it) }})" + }.toList().joinToString() + ) + } + } + return emptyList() + } +} From 43df4e0b1f7be8f52f152cb7c4d5da48da18b40e Mon Sep 17 00:00:00 2001 From: Marius Volkhart Date: Fri, 12 Jun 2026 12:37:59 -0400 Subject: [PATCH 2/4] fixup! Fix getJvmName for @JvmRecord data class properties --- .../devtools/ksp/impl/ResolverAAImpl.kt | 19 +++---- .../devtools/ksp/impl/symbol/kotlin/util.kt | 7 +++ kotlin-analysis-api/testData/jvmNameRecord.kt | 53 ++++++++++++++++--- .../ksp/processor/JvmNameRecordProcessor.kt | 23 ++++---- 4 files changed, 77 insertions(+), 25 deletions(-) diff --git a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/ResolverAAImpl.kt b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/ResolverAAImpl.kt index 98ebfb464a..d6a68edde4 100644 --- a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/ResolverAAImpl.kt +++ b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/ResolverAAImpl.kt @@ -65,6 +65,7 @@ import com.google.devtools.ksp.impl.symbol.kotlin.findParentOfType import com.google.devtools.ksp.impl.symbol.kotlin.fullyExpand import com.google.devtools.ksp.impl.symbol.kotlin.inlineSuffix import com.google.devtools.ksp.impl.symbol.kotlin.internalSuffix +import com.google.devtools.ksp.impl.symbol.kotlin.isJvmRecord import com.google.devtools.ksp.impl.symbol.kotlin.toKSDeclaration import com.google.devtools.ksp.impl.symbol.kotlin.toKSName import com.google.devtools.ksp.impl.symbol.kotlin.toKtClassSymbol @@ -577,22 +578,22 @@ class ResolverAAImpl( return it } - val name = accessor.receiver.simpleName.asString() + val propertyName = accessor.receiver.simpleName.asString() val containingClass = accessor.receiver.closestClassDeclaration() + val isAnnotationClass = containingClass?.classKind == ClassKind.ANNOTATION_CLASS + val isJvmRecord = containingClass is KSClassDeclarationImpl && + containingClass.ktClassOrObjectSymbol.isJvmRecord() + // 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 + if (isAnnotationClass || isJvmRecord) { + return propertyName } // https://kotlinlang.org/docs/java-to-kotlin-interop.html#properties val prefixedName = when (accessor) { - is KSPropertyGetter -> JvmAbi.getterName(name) - is KSPropertySetter -> JvmAbi.setterName(name) + is KSPropertyGetter -> JvmAbi.getterName(propertyName) + is KSPropertySetter -> JvmAbi.setterName(propertyName) else -> "" } diff --git a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/symbol/kotlin/util.kt b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/symbol/kotlin/util.kt index b595a0292b..0f0335a459 100644 --- a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/symbol/kotlin/util.kt +++ b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/symbol/kotlin/util.kt @@ -140,6 +140,7 @@ import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmProtoBufUtil import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.ClassIdBasedLocality import org.jetbrains.kotlin.name.FqNameUnsafe +import org.jetbrains.kotlin.name.JvmStandardClassIds import org.jetbrains.kotlin.name.JvmStandardClassIds.JVM_SUPPRESS_WILDCARDS_ANNOTATION_FQ_NAME import org.jetbrains.kotlin.name.JvmStandardClassIds.JVM_WILDCARD_ANNOTATION_FQ_NAME import org.jetbrains.kotlin.psi.KtAnnotated @@ -1163,6 +1164,12 @@ internal fun KaCallableSymbol.explictJvmName(): String? { }?.arguments?.single()?.expression?.toValue() as? String } +internal fun KaClassSymbol.isJvmRecord(): Boolean { + return annotations.any { + it.classId == JvmStandardClassIds.Annotations.JvmRecord + } +} + @OptIn(SymbolInternals::class) internal val KaDeclarationSymbol.internalSuffix: String get() = analyze { diff --git a/kotlin-analysis-api/testData/jvmNameRecord.kt b/kotlin-analysis-api/testData/jvmNameRecord.kt index a4d239b63a..0b248372d6 100644 --- a/kotlin-analysis-api/testData/jvmNameRecord.kt +++ b/kotlin-analysis-api/testData/jvmNameRecord.kt @@ -1,12 +1,53 @@ // TEST PROCESSOR: JvmNameRecordProcessor // EXPECTED: -// (x, null), (y, null) -// (x, null), (y, null) +// Aliased.z: z +// Couple.first: first +// Couple.second: second +// GenericRecord.g: g +// GenericRecord.h: h +// NamedRecord.id: id +// NamedRecord.name: name +// Single.x: x +// WithBody.a: a +// WithBody.computed: computed +// WithBody.mutable: mutable, mutable // END // MODULE: main -// FILE: TestRecordClass.kt +// FILE: records.kt +interface Named { + val name: String +} + +interface Generic { + val g: A +} + @JvmRecord -data class TestRecordClass(val x: Int, val y: String) -// FILE: TestLibRecordClass.kt +data class Single(val x: Int) + @JvmRecord -data class TestLibRecordClass(val x: Int, val y: String) +data class Couple(val first: Int, val second: String) + +// @JvmRecord classes cannot extend other classes (they extend java.lang.Record), +// so implementing interfaces is the only supertype case. +@JvmRecord +data class NamedRecord(override val name: String, val id: Int) : Named + +@JvmRecord +data class GenericRecord(override val g: A, val h: B) : Generic + +// @JvmRecord constructor properties must be vals; mutable properties are only +// possible in the class body and without a backing field. +@JvmRecord +data class WithBody(val a: Int) { + val computed: Int + get() = a * 2 + var mutable: Int + get() = a + set(value) {} +} +// FILE: aliased.kt +import kotlin.jvm.JvmRecord as JR + +@JR +data class Aliased(val z: Int) diff --git a/test-utils/src/main/kotlin/com/google/devtools/ksp/processor/JvmNameRecordProcessor.kt b/test-utils/src/main/kotlin/com/google/devtools/ksp/processor/JvmNameRecordProcessor.kt index 38e67b861e..c556caab87 100644 --- a/test-utils/src/main/kotlin/com/google/devtools/ksp/processor/JvmNameRecordProcessor.kt +++ b/test-utils/src/main/kotlin/com/google/devtools/ksp/processor/JvmNameRecordProcessor.kt @@ -1,9 +1,9 @@ package com.google.devtools.ksp.processor import com.google.devtools.ksp.KspExperimental -import com.google.devtools.ksp.getClassDeclarationByName import com.google.devtools.ksp.processing.Resolver import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSClassDeclaration class JvmNameRecordProcessor : AbstractTestProcessor() { val results = mutableListOf() @@ -13,16 +13,19 @@ class JvmNameRecordProcessor : AbstractTestProcessor() { @OptIn(KspExperimental::class) override fun process(resolver: Resolver): List { - listOf("TestRecordClass", "TestLibRecordClass").forEach { clsName -> - resolver.getClassDeclarationByName(clsName)?.let { cls -> - results.add( - cls.getAllProperties().map { - "(${it.getter?.let { resolver.getJvmName(it) }}, " + - "${it.setter?.let { resolver.getJvmName(it) }})" - }.toList().joinToString() - ) + resolver.getSymbolsWithAnnotation("kotlin.jvm.JvmRecord") + .filterIsInstance() + .flatMap { cls -> + cls.getAllProperties().map { property -> + val accessorNames = listOfNotNull( + property.getter?.let { resolver.getJvmName(it) }, + property.setter?.let { resolver.getJvmName(it) }, + ) + "${cls.simpleName.asString()}.${property.simpleName.asString()}: ${accessorNames.joinToString()}" + } } - } + .sorted() + .let { results.addAll(it) } return emptyList() } } From 3dffe969897c81eaa5cdc0cfde8a6bcceed67cd9 Mon Sep 17 00:00:00 2001 From: Marius Volkhart Date: Fri, 12 Jun 2026 12:39:02 -0400 Subject: [PATCH 3/4] Allow per-file JVM_TARGET directives; fold record test into KSPAATest 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. --- .../devtools/ksp/test/AbstractKSPAATest.kt | 3 +- .../devtools/ksp/test/AbstractKSPTest.kt | 2 - .../google/devtools/ksp/test/KSPAA17Test.kt | 43 ------------------- .../devtools/ksp/test/KSPUnitTestSuite.kt | 6 +++ kotlin-analysis-api/testData/jvmNameRecord.kt | 1 + 5 files changed, 9 insertions(+), 46 deletions(-) delete mode 100644 kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/KSPAA17Test.kt diff --git a/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/AbstractKSPAATest.kt b/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/AbstractKSPAATest.kt index 4f193eee33..eaaf6e737e 100644 --- a/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/AbstractKSPAATest.kt +++ b/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/AbstractKSPAATest.kt @@ -25,6 +25,7 @@ import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler import org.jetbrains.kotlin.cli.jvm.config.javaSourceRoots import org.jetbrains.kotlin.cli.jvm.config.jvmModularRoots import org.jetbrains.kotlin.config.JVMConfigurationKeys +import org.jetbrains.kotlin.config.JvmTarget import org.jetbrains.kotlin.config.languageVersionSettings import org.jetbrains.kotlin.test.compileJavaFiles import org.jetbrains.kotlin.test.kotlinPathsForDistDirectoryForTests @@ -129,7 +130,7 @@ abstract class AbstractKSPAATest(val experimentalPsiResolution: Boolean) : Abstr sourceRoots = listOf(mainModule.kotlinSrc) javaSourceRoots = compilerConfiguration.javaSourceRoots.map { File(it) }.toList() jdkHome = compilerConfiguration.get(JVMConfigurationKeys.JDK_HOME) - jvmTarget = compilerConfiguration.get(JVMConfigurationKeys.JVM_TARGET)!!.description + jvmTarget = (compilerConfiguration.get(JVMConfigurationKeys.JVM_TARGET) ?: JvmTarget.DEFAULT).description languageVersion = compilerConfiguration.languageVersionSettings.languageVersion.versionString apiVersion = compilerConfiguration.languageVersionSettings.apiVersion.versionString libraries = mainModule.regularDependencies.map { it.dependencyModule.outDir } + diff --git a/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/AbstractKSPTest.kt b/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/AbstractKSPTest.kt index a05a7bca67..1deeb7a3f1 100644 --- a/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/AbstractKSPTest.kt +++ b/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/AbstractKSPTest.kt @@ -32,7 +32,6 @@ import org.jetbrains.kotlin.cli.jvm.config.addJvmClasspathRoots import org.jetbrains.kotlin.codegen.ClassBuilderFactories import org.jetbrains.kotlin.codegen.GenerationUtils import org.jetbrains.kotlin.codegen.forTestCompile.TestCompilePaths -import org.jetbrains.kotlin.config.JvmTarget import org.jetbrains.kotlin.platform.jvm.JvmPlatforms import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.test.ExecutionListenerBasedDisposableProvider @@ -155,7 +154,6 @@ abstract class AbstractKSPTest(frontend: FrontendKind<*>) : DisposableTest() { defaultDirectives { +JvmEnvironmentConfigurationDirectives.FULL_JDK - JvmEnvironmentConfigurationDirectives.JVM_TARGET with JvmTarget.DEFAULT +ConfigurationDirectives.WITH_STDLIB +LanguageSettingsDirectives.ALLOW_KOTLIN_PACKAGE } diff --git a/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/KSPAA17Test.kt b/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/KSPAA17Test.kt deleted file mode 100644 index e03fb8f1c0..0000000000 --- a/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/KSPAA17Test.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2022 Google LLC - * Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.devtools.ksp.test - -import org.jetbrains.kotlin.config.JvmTarget -import org.jetbrains.kotlin.test.TestMetadata -import org.jetbrains.kotlin.test.builders.TestConfigurationBuilder -import org.jetbrains.kotlin.test.directives.JvmEnvironmentConfigurationDirectives -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.parallel.Execution -import org.junit.jupiter.api.parallel.ExecutionMode - -@Execution(ExecutionMode.SAME_THREAD) -class KSPAA17Test : AbstractKSPAATest() { - - override fun configureTest(builder: TestConfigurationBuilder) { - builder.defaultDirectives { - -JvmEnvironmentConfigurationDirectives.JVM_TARGET - JvmEnvironmentConfigurationDirectives.JVM_TARGET with JvmTarget.JVM_17 - } - } - - @TestMetadata("jvmNameRecord.kt") - @Test - fun testJvmNameRecord() { - runTest("../kotlin-analysis-api/testData/jvmNameRecord.kt") - } -} diff --git a/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/KSPUnitTestSuite.kt b/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/KSPUnitTestSuite.kt index eb3cce41f7..b7eb497379 100644 --- a/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/KSPUnitTestSuite.kt +++ b/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/KSPUnitTestSuite.kt @@ -495,6 +495,12 @@ abstract class KSPUnitTestSuite( runTest("$AA_PATH/jvmName.kt") } + @TestMetadata("jvmNameRecord.kt") + @Test + fun testJvmNameRecord() { + runTest("../kotlin-analysis-api/testData/jvmNameRecord.kt") + } + @TestMetadata("lateinitProperties.kt") @Test fun testLateinitProperties() { diff --git a/kotlin-analysis-api/testData/jvmNameRecord.kt b/kotlin-analysis-api/testData/jvmNameRecord.kt index 0b248372d6..e1b376c95b 100644 --- a/kotlin-analysis-api/testData/jvmNameRecord.kt +++ b/kotlin-analysis-api/testData/jvmNameRecord.kt @@ -12,6 +12,7 @@ // WithBody.computed: computed // WithBody.mutable: mutable, mutable // END +// JVM_TARGET: 17 // MODULE: main // FILE: records.kt interface Named { From dbcb5dff78981fe377a3b96a446163a9607cc809 Mon Sep 17 00:00:00 2001 From: Marius Volkhart Date: Fri, 12 Jun 2026 13:06:22 -0400 Subject: [PATCH 4/4] Fix getJvmName for record classes from library modules 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. --- .../devtools/ksp/impl/symbol/kotlin/util.kt | 11 +++++++++-- .../devtools/ksp/test/AbstractKSPAATest.kt | 16 +++++++++++++--- kotlin-analysis-api/testData/jvmNameRecord.kt | 12 +++++++++++- .../ksp/processor/JvmNameRecordProcessor.kt | 7 ++++++- 4 files changed, 39 insertions(+), 7 deletions(-) diff --git a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/symbol/kotlin/util.kt b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/symbol/kotlin/util.kt index 0f0335a459..765e055c9d 100644 --- a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/symbol/kotlin/util.kt +++ b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/symbol/kotlin/util.kt @@ -1164,9 +1164,16 @@ internal fun KaCallableSymbol.explictJvmName(): String? { }?.arguments?.single()?.expression?.toValue() as? String } +private val javaLangRecordClassId = ClassId.fromString("java/lang/Record") + internal fun KaClassSymbol.isJvmRecord(): Boolean { - return annotations.any { - it.classId == JvmStandardClassIds.Annotations.JvmRecord + if (annotations.any { it.classId == JvmStandardClassIds.Annotations.JvmRecord }) { + return true + } + // The @JvmRecord annotation is not retained in class files, so compiled record + // classes are recognized by their java.lang.Record supertype instead. + return origin == KaSymbolOrigin.LIBRARY && analyze { + superTypes.any { (it as? KaClassType)?.classId == javaLangRecordClassId } } } diff --git a/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/AbstractKSPAATest.kt b/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/AbstractKSPAATest.kt index eaaf6e737e..cb0f6af61f 100644 --- a/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/AbstractKSPAATest.kt +++ b/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/AbstractKSPAATest.kt @@ -61,7 +61,8 @@ abstract class AbstractKSPAATest(val experimentalPsiResolution: Boolean) : Abstr sourcesPath: String, javaSourcePath: String, outDir: File, - moduleName: String + moduleName: String, + jvmTarget: JvmTarget? ) { val classpath = mutableListOf() classpath.addAll(dependencies.map { it.canonicalPath }) @@ -79,6 +80,9 @@ abstract class AbstractKSPAATest(val experimentalPsiResolution: Boolean) : Abstr "-module-name", moduleName, "-classpath", classpath.joinToString(File.pathSeparator) ) + if (jvmTarget != null) { + args += listOf("-jvm-target", jvmTarget.description) + } runJvmCompiler(args) } @@ -87,14 +91,20 @@ abstract class AbstractKSPAATest(val experimentalPsiResolution: Boolean) : Abstr val compilerClass = URLClassLoader(arrayOf(), javaClass.classLoader).loadClass(K2JVMCompiler::class.java.name) val compiler = compilerClass.getDeclaredConstructor().newInstance() val execMethod = compilerClass.getMethod("exec", PrintStream::class.java, Array::class.java) - execMethod.invoke(compiler, PrintStream(outStream), args.toTypedArray()) + val exitCode = execMethod.invoke(compiler, PrintStream(outStream), args.toTypedArray()) + check(exitCode.toString() == "OK") { + "Kotlin compilation failed with exit code $exitCode:\n$outStream" + } } override fun compileModule(module: TestModule, testServices: TestServices) { module.writeKtFiles() val javaFiles = module.writeJavaFiles() val dependencies = module.allDependencies.map { outDirForModule(it.dependencyModule.name) } - compileKotlin(dependencies, module.kotlinSrc.path, module.javaDir.path, module.outDir, module.name) + val jvmTarget = testServices.compilerConfigurationProvider + .getCompilerConfiguration(module, CompilationStage.FIRST) + .get(JVMConfigurationKeys.JVM_TARGET) + compileKotlin(dependencies, module.kotlinSrc.path, module.javaDir.path, module.outDir, module.name, jvmTarget) val classpath = (dependencies + KtTestUtil.getAnnotationsJar() + module.outDir) .joinToString(File.pathSeparator) { it.absolutePath } val options = listOf( diff --git a/kotlin-analysis-api/testData/jvmNameRecord.kt b/kotlin-analysis-api/testData/jvmNameRecord.kt index e1b376c95b..feafb4a440 100644 --- a/kotlin-analysis-api/testData/jvmNameRecord.kt +++ b/kotlin-analysis-api/testData/jvmNameRecord.kt @@ -5,15 +5,21 @@ // Couple.second: second // GenericRecord.g: g // GenericRecord.h: h +// LibRecord.w: w // NamedRecord.id: id // NamedRecord.name: name // Single.x: x // WithBody.a: a // WithBody.computed: computed // WithBody.mutable: mutable, mutable +// WithJvmName.n: customName // END // JVM_TARGET: 17 -// MODULE: main +// MODULE: lib +// FILE: LibRecord.kt +@JvmRecord +data class LibRecord(val w: Int) +// MODULE: main(lib) // FILE: records.kt interface Named { val name: String @@ -47,6 +53,10 @@ data class WithBody(val a: Int) { get() = a set(value) {} } + +// An explicit @JvmName takes precedence over the record accessor naming. +@JvmRecord +data class WithJvmName(@get:JvmName("customName") val n: Int) // FILE: aliased.kt import kotlin.jvm.JvmRecord as JR diff --git a/test-utils/src/main/kotlin/com/google/devtools/ksp/processor/JvmNameRecordProcessor.kt b/test-utils/src/main/kotlin/com/google/devtools/ksp/processor/JvmNameRecordProcessor.kt index c556caab87..0ef76e7a61 100644 --- a/test-utils/src/main/kotlin/com/google/devtools/ksp/processor/JvmNameRecordProcessor.kt +++ b/test-utils/src/main/kotlin/com/google/devtools/ksp/processor/JvmNameRecordProcessor.kt @@ -1,6 +1,7 @@ package com.google.devtools.ksp.processor import com.google.devtools.ksp.KspExperimental +import com.google.devtools.ksp.getClassDeclarationByName import com.google.devtools.ksp.processing.Resolver import com.google.devtools.ksp.symbol.KSAnnotated import com.google.devtools.ksp.symbol.KSClassDeclaration @@ -13,8 +14,12 @@ class JvmNameRecordProcessor : AbstractTestProcessor() { @OptIn(KspExperimental::class) override fun process(resolver: Resolver): List { - resolver.getSymbolsWithAnnotation("kotlin.jvm.JvmRecord") + // getSymbolsWithAnnotation only returns symbols in the current compilation, + // so the record class from the library module is looked up explicitly. + val sourceRecords = resolver.getSymbolsWithAnnotation("kotlin.jvm.JvmRecord") .filterIsInstance() + val libRecords = listOfNotNull(resolver.getClassDeclarationByName("LibRecord")) + (sourceRecords + libRecords) .flatMap { cls -> cls.getAllProperties().map { property -> val accessorNames = listOfNotNull(