Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 -> ""
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<String>()
classpath.addAll(dependencies.map { it.canonicalPath })
Expand All @@ -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)
}

Expand All @@ -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<String>::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"
}
Comment on lines +94 to +97

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.

Please revert this :)

}

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(
Expand Down Expand Up @@ -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 } +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,12 @@ abstract class KSPUnitTestSuite(
runTest("$AA_PATH/jvmName.kt")
}

@TestMetadata("jvmNameRecord.kt")
@Test
fun testJvmNameRecord() {

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 would be great if you could annotate the test with the @Bug annotation. That way, in the future we have some reference as to why the test is there. Example:

@Bug("https://github.com/google/ksp/issues/2919", BugState.FIXED)

runTest("../kotlin-analysis-api/testData/jvmNameRecord.kt")

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.

You can just use "$AA_PATH/jvmNameRecord.kt" here

}

@TestMetadata("lateinitProperties.kt")
@Test
fun testLateinitProperties() {
Expand Down
64 changes: 64 additions & 0 deletions kotlin-analysis-api/testData/jvmNameRecord.kt

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.

Can we add a test where one of the properties of the data class is also a data class / jvm record?

Also, missing copyright header :)

Original file line number Diff line number Diff line change
@@ -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<A> {
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<A, B>(override val g: A, val h: B) : Generic<A>

// @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

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.

In addition to this, can we also create a test where we use a type alias for the annotation?


@JR
data class Aliased(val z: Int)

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.

Missing copyright header :)

Original file line number Diff line number Diff line change
@@ -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<String>()
override fun toResult(): List<String> {
return results
Comment thread
jaschdoc marked this conversation as resolved.
}

@OptIn(KspExperimental::class)
override fun process(resolver: Resolver): List<KSAnnotated> {
// 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<KSClassDeclaration>()
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) },

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 should also get the jvm name of the setter parameter.

)
"${cls.simpleName.asString()}.${property.simpleName.asString()}: ${accessorNames.joinToString()}"
}
}
.sorted()
.let { results.addAll(it) }
return emptyList()
}
}