diff --git a/compiler/testData/codegen/boxModernJdk/testsWithJava17/javaRecordsViaKotlinReflection.kt b/compiler/testData/codegen/boxModernJdk/testsWithJava17/javaRecordsViaKotlinReflection.kt new file mode 100644 index 00000000000..c72172f4f55 --- /dev/null +++ b/compiler/testData/codegen/boxModernJdk/testsWithJava17/javaRecordsViaKotlinReflection.kt @@ -0,0 +1,37 @@ +// WITH_REFLECT +// ATTACH_DEBUGGERp +// ISSUE: KT-47760 + +// FILE: MyRecord.java +public record MyRecord(String stringField) {} + +// FILE: main.kt +import kotlin.reflect.full.* +import kotlin.reflect.KVisibility +import kotlin.reflect.jvm.isAccessible + +fun box(): String { + val expectedValue = "Hello" + val obj = MyRecord(expectedValue) + + // stringField() function + val function = MyRecord::class.functions.single { it.name == "stringField" } + val functionValue = function.call(obj) + if (functionValue != expectedValue) { + return "Fail: stringField() call returned $functionValue, expected $expectedValue" + } + + // stringField field + val property = MyRecord::class.memberProperties.single { it.name == "stringField" } + if (property.visibility != KVisibility.PRIVATE) { + return "Fail: field stringField is not private" + } + val getter = property.getter + getter.isAccessible = true + val propertyValue = getter.call(obj) + if (propertyValue != expectedValue) { + return "Fail: stringField field returned $propertyValue, expected $expectedValue" + } + + return "OK" +} diff --git a/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/codegen/BlackBoxModernJdkCodegenTestGenerated.java b/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/codegen/BlackBoxModernJdkCodegenTestGenerated.java index 8968c350e3d..378f0beb15a 100644 --- a/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/codegen/BlackBoxModernJdkCodegenTestGenerated.java +++ b/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/codegen/BlackBoxModernJdkCodegenTestGenerated.java @@ -129,6 +129,12 @@ public class BlackBoxModernJdkCodegenTestGenerated extends AbstractBlackBoxCodeg runTest("compiler/testData/codegen/boxModernJdk/testsWithJava17/compiledJavaSealedInterface.kt"); } + @Test + @TestMetadata("javaRecordsViaKotlinReflection.kt") + public void testJavaRecordsViaKotlinReflection() throws Exception { + runTest("compiler/testData/codegen/boxModernJdk/testsWithJava17/javaRecordsViaKotlinReflection.kt"); + } + @Test @TestMetadata("javaSealedClass.kt") public void testJavaSealedClass() throws Exception { diff --git a/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/codegen/IrBlackBoxModernJdkCodegenTestGenerated.java b/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/codegen/IrBlackBoxModernJdkCodegenTestGenerated.java index 7d0d20484c2..41e5d89aff6 100644 --- a/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/codegen/IrBlackBoxModernJdkCodegenTestGenerated.java +++ b/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/codegen/IrBlackBoxModernJdkCodegenTestGenerated.java @@ -129,6 +129,12 @@ public class IrBlackBoxModernJdkCodegenTestGenerated extends AbstractIrBlackBoxC runTest("compiler/testData/codegen/boxModernJdk/testsWithJava17/compiledJavaSealedInterface.kt"); } + @Test + @TestMetadata("javaRecordsViaKotlinReflection.kt") + public void testJavaRecordsViaKotlinReflection() throws Exception { + runTest("compiler/testData/codegen/boxModernJdk/testsWithJava17/javaRecordsViaKotlinReflection.kt"); + } + @Test @TestMetadata("javaSealedClass.kt") public void testJavaSealedClass() throws Exception { diff --git a/core/descriptors.runtime/src/org/jetbrains/kotlin/descriptors/runtime/structure/ReflectJavaClass.kt b/core/descriptors.runtime/src/org/jetbrains/kotlin/descriptors/runtime/structure/ReflectJavaClass.kt index 923ebc950f0..ebd606a89c6 100644 --- a/core/descriptors.runtime/src/org/jetbrains/kotlin/descriptors/runtime/structure/ReflectJavaClass.kt +++ b/core/descriptors.runtime/src/org/jetbrains/kotlin/descriptors/runtime/structure/ReflectJavaClass.kt @@ -97,9 +97,6 @@ class ReflectJavaClass( .map(::ReflectJavaConstructor) .toList() - override val recordComponents: Collection - get() = emptyList() - override fun hasDefaultConstructor() = false // any default constructor is returned by constructors override val lightClassOriginKind: LightClassOriginKind? @@ -118,9 +115,11 @@ class ReflectJavaClass( override val isEnum: Boolean get() = klass.isEnum - // TODO: Support reflect override val isRecord: Boolean - get() = false + get() = Java16SealedRecordLoader.loadIsRecord(klass) ?: false + + override val recordComponents: Collection + get() = (Java16SealedRecordLoader.loadGetRecordComponents(klass) ?: emptyArray()).map(::ReflectJavaRecordComponent) override val isSealed: Boolean get() = Java16SealedRecordLoader.loadIsSealed(klass) ?: false @@ -141,6 +140,8 @@ private object Java16SealedRecordLoader { class Cache( val isSealed: Method?, val getPermittedSubclasses: Method?, + val isRecord: Method?, + val getRecordComponents: Method? ) private var _cache: Cache? = null @@ -152,9 +153,11 @@ private object Java16SealedRecordLoader { Cache( clazz.getMethod("isSealed"), clazz.getMethod("getPermittedSubclasses"), + clazz.getMethod("isRecord"), + clazz.getMethod("getRecordComponents") ) } catch (e: NoSuchMethodException) { - Cache(null, null) + Cache(null, null, null, null) } } @@ -179,4 +182,17 @@ private object Java16SealedRecordLoader { @Suppress("UNCHECKED_CAST") return getPermittedSubclasses.invoke(clazz) as Array> } + + fun loadIsRecord(clazz: Class<*>): Boolean? { + val cache = initCache() + val isRecord = cache.isRecord ?: return null + return isRecord.invoke(clazz) as Boolean + } + + fun loadGetRecordComponents(clazz: Class<*>): Array? { + val cache = initCache() + val getRecordComponents = cache.getRecordComponents ?: return null + @Suppress("UNCHECKED_CAST") + return getRecordComponents.invoke(clazz) as Array? + } } diff --git a/core/descriptors.runtime/src/org/jetbrains/kotlin/descriptors/runtime/structure/ReflectJavaRecordComponent.kt b/core/descriptors.runtime/src/org/jetbrains/kotlin/descriptors/runtime/structure/ReflectJavaRecordComponent.kt new file mode 100644 index 00000000000..edf3e18bad7 --- /dev/null +++ b/core/descriptors.runtime/src/org/jetbrains/kotlin/descriptors/runtime/structure/ReflectJavaRecordComponent.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.descriptors.runtime.structure + +import org.jetbrains.kotlin.load.java.structure.JavaRecordComponent +import org.jetbrains.kotlin.load.java.structure.JavaType +import java.lang.reflect.Member +import java.lang.reflect.Method + +class ReflectJavaRecordComponent(val recordComponent: Any) : ReflectJavaMember(), JavaRecordComponent { + override val type: JavaType + get() = Java16RecordComponentsLoader.loadGetType(recordComponent)?.let { ReflectJavaClassifierType(it) } + ?: throw NoSuchMethodError("Can't find `getType` method") + override val isVararg: Boolean + get() = false + override val member: Member + get() = Java16RecordComponentsLoader.loadGetAccessor(recordComponent) + ?: throw NoSuchMethodError("Can't find `getAccessor` method") +} + +private object Java16RecordComponentsLoader { + class Cache( + val getType: Method?, + val getAccessor: Method?, + ) + + private var _cache: Cache? = null + + private fun buildCache(recordComponent: Any): Cache { + // Should be Class + val classOfComponent = recordComponent::class.java + + return try { + Cache( + classOfComponent.getMethod("getType"), + classOfComponent.getMethod("getAccessor"), + ) + } catch (e: NoSuchMethodException) { + Cache(null, null) + } + } + + private fun initCache(recordComponent: Any): Cache { + var cache = this._cache + if (cache == null) { + cache = buildCache(recordComponent) + this._cache = cache + } + return cache + + } + + fun loadGetType(recordComponent: Any): Class<*>? { + val cache = initCache(recordComponent) + val getType = cache.getType ?: return null + return getType.invoke(recordComponent) as Class<*> + } + + fun loadGetAccessor(recordComponent: Any): Method? { + val cache = initCache(recordComponent) + val getType = cache.getAccessor ?: return null + return getType.invoke(recordComponent) as Method + } +}