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..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,15 +578,22 @@ class ResolverAAImpl( return it } - if (accessor.receiver.closestClassDeclaration()?.classKind == ClassKind.ANNOTATION_CLASS) { - return 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() - val name = accessor.receiver.simpleName.asString() + // Annotation classes and @JvmRecord data classes both use bare property names + // as accessor names (no get/set prefix). + 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..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 @@ -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,19 @@ internal fun KaCallableSymbol.explictJvmName(): String? { }?.arguments?.single()?.expression?.toValue() as? String } +private val javaLangRecordClassId = ClassId.fromString("java/lang/Record") + +internal fun KaClassSymbol.isJvmRecord(): Boolean { + 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 } + } +} + @OptIn(SymbolInternals::class) internal val KaDeclarationSymbol.internalSuffix: String get() = analyze { 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..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 @@ -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 @@ -60,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 }) @@ -78,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) } @@ -86,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( @@ -129,7 +140,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/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 new file mode 100644 index 0000000000..feafb4a440 --- /dev/null +++ b/kotlin-analysis-api/testData/jvmNameRecord.kt @@ -0,0 +1,64 @@ +// TEST PROCESSOR: JvmNameRecordProcessor +// EXPECTED: +// Aliased.z: z +// Couple.first: first +// 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: lib +// FILE: LibRecord.kt +@JvmRecord +data class LibRecord(val w: Int) +// MODULE: main(lib) +// FILE: records.kt +interface Named { + val name: String +} + +interface Generic { + val g: A +} + +@JvmRecord +data class Single(val x: Int) + +@JvmRecord +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) {} +} + +// 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 + +@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 new file mode 100644 index 0000000000..0ef76e7a61 --- /dev/null +++ b/test-utils/src/main/kotlin/com/google/devtools/ksp/processor/JvmNameRecordProcessor.kt @@ -0,0 +1,36 @@ +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() + override fun toResult(): List { + return results + } + + @OptIn(KspExperimental::class) + override fun process(resolver: Resolver): List { + // 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( + 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() + } +}