Use descriptors for reflection on class properties

- use ConcurrentHashMap as a cache of class loaders to module descriptors
- KClassImpl now has a lazy class descriptor and it manages property creation
  by looking (also lazily) for the property descriptor in the corresponding
  scope
- since deserialized descriptors have full information about where a JVM symbol
  is located and what signature it has, new tests will begin to pass where
  Kotlin model and Java reflection model differ, see classObjectVar.kt
This commit is contained in:
Alexander Udalov
2015-02-19 17:27:58 +03:00
parent ba0ea1515b
commit 64fdb18ad3
11 changed files with 409 additions and 73 deletions
@@ -0,0 +1,15 @@
class A {
default object B {
var state: String = "12345"
}
}
fun box(): String {
val p = A.B::state
if (p.name != "state") return "Fail state: ${p.name}"
if (p.get(A.B) != "12345") return "Fail value: ${p.get(A.B)}"
p.set(A.B, "OK")
return p[A.B]
}
@@ -741,6 +741,12 @@ public class BlackBoxWithStdlibCodegenTestGenerated extends AbstractBlackBoxCode
JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("compiler/testData/codegen/boxWithStdlib/callableReference/property"), Pattern.compile("^(.+)\\.kt$"), true);
}
@TestMetadata("classObjectVar.kt")
public void testClassObjectVar() throws Exception {
String fileName = JetTestUtils.navigationMetadata("compiler/testData/codegen/boxWithStdlib/callableReference/property/classObjectVar.kt");
doTestWithStdlib(fileName);
}
@TestMetadata("delegated.kt")
public void testDelegated() throws Exception {
String fileName = JetTestUtils.navigationMetadata("compiler/testData/codegen/boxWithStdlib/callableReference/property/delegated.kt");
@@ -21,7 +21,9 @@ import org.jetbrains.kotlin.descriptors.Visibility
import org.jetbrains.kotlin.load.java.JavaVisibilities
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.FqNameUnsafe
import org.jetbrains.kotlin.name.Name
import java.lang.reflect.Array
import java.lang.reflect.Modifier
private fun calculateVisibility(modifiers: Int): Visibility {
@@ -34,6 +36,9 @@ private fun calculateVisibility(modifiers: Int): Visibility {
}
}
public val Class<*>.classLoader: ClassLoader
get() = getClassLoader() ?: ClassLoader.getSystemClassLoader()
public fun Class<*>.isEnumClassOrSpecializedEnumEntryClass(): Boolean =
javaClass<Enum<*>>().isAssignableFrom(this)
@@ -41,4 +46,15 @@ public val Class<*>.fqName: FqName
get() = classId.asSingleFqName().toSafe()
public val Class<*>.classId: ClassId
get() = getDeclaringClass()?.classId?.createNestedClassId(Name.identifier(getSimpleName())) ?: ClassId.topLevel(FqName(getName()))
get() = when {
isPrimitive() -> throw IllegalArgumentException("Can't compute ClassId for primitive type: $this")
isArray() -> throw IllegalArgumentException("Can't compute ClassId for array type: $this")
getEnclosingMethod() != null, getEnclosingConstructor() != null, getSimpleName().isEmpty() -> {
val fqName = FqName(getName())
ClassId(fqName.parent(), FqNameUnsafe.topLevel(fqName.shortName()), /* local = */ true)
}
else -> getDeclaringClass()?.classId?.createNestedClassId(Name.identifier(getSimpleName())) ?: ClassId.topLevel(FqName(getName()))
}
public fun Class<*>.createArrayType(): Class<*> =
Array.newInstance(this, 0).javaClass
@@ -16,16 +16,20 @@
package kotlin.reflect
suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
public class IllegalPropertyAccessException(cause: IllegalAccessException) : Exception(cause.getMessage()) {
{
[suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")]
(this as java.lang.Throwable).initCause(cause)
}
}
suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
public class NoSuchPropertyException(cause: Exception) : Exception() {
public class NoSuchPropertyException(cause: Exception? = null) : Exception() {
{
(this as java.lang.Throwable).initCause(cause)
[suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")]
if (cause != null) {
(this as java.lang.Throwable).initCause(cause)
}
}
}
public class KotlinReflectionInternalError(message: String) : Error(message)
@@ -0,0 +1,67 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* 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 kotlin.reflect.jvm.internal
import org.jetbrains.kotlin.descriptors.PropertyDescriptor
import org.jetbrains.kotlin.resolve.DescriptorUtils
import org.jetbrains.kotlin.serialization.ProtoBuf
import org.jetbrains.kotlin.serialization.deserialization.NameResolver
import org.jetbrains.kotlin.serialization.deserialization.descriptors.DeserializedPropertyDescriptor
import org.jetbrains.kotlin.serialization.jvm.JvmProtoBuf
import java.lang.reflect.Field
import java.lang.reflect.Method
import kotlin.reflect.KotlinReflectionInternalError
abstract class DescriptorBasedProperty(computeDescriptor: () -> PropertyDescriptor) {
protected abstract val container: KCallableContainerImpl
data class PropertyProtoData(
val proto: ProtoBuf.Callable,
val nameResolver: NameResolver,
val signature: JvmProtoBuf.JvmPropertySignature
)
protected val descriptor: PropertyDescriptor by ReflectProperties.lazySoft { computeDescriptor() }
protected val protoData: PropertyProtoData by ReflectProperties.lazyWeak {
val property = DescriptorUtils.unwrapFakeOverride(descriptor) as? DeserializedPropertyDescriptor
?: throw KotlinReflectionInternalError("Member property resolved incorrectly: $descriptor")
val proto = property.proto
if (!proto.hasExtension(JvmProtoBuf.propertySignature)) {
throw KotlinReflectionInternalError("Member property lacks JVM signature: $descriptor")
}
PropertyProtoData(proto, property.nameResolver, proto.getExtension(JvmProtoBuf.propertySignature))
}
val field: Field? by ReflectProperties.lazySoft {
val proto = protoData
if (!proto.signature.hasField()) null
else container.findFieldBySignature(proto.signature.getField(), proto.nameResolver)
}
val getter: Method? by ReflectProperties.lazySoft {
val proto = protoData
if (!proto.signature.hasGetter()) null
else container.findMethodBySignature(proto.signature.getGetter(), proto.nameResolver)
}
val setter: Method? by ReflectProperties.lazySoft {
val proto = protoData
if (!proto.signature.hasSetter()) null
else container.findMethodBySignature(proto.signature.getSetter(), proto.nameResolver)
}
}
@@ -0,0 +1,40 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* 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 kotlin.reflect.jvm.internal
import org.jetbrains.kotlin.descriptors.PropertyDescriptor
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.resolve.scopes.JetScope
import kotlin.reflect.KotlinReflectionInternalError
abstract class KCallableContainerImpl {
abstract val jClass: Class<*>
abstract val scope: JetScope
protected fun findPropertyDescriptor(name: String): () -> PropertyDescriptor = {
val properties = scope.getProperties(Name.identifier(name))
if (properties.size() != 1) {
throw KotlinReflectionInternalError(
if (properties.isEmpty()) "Property $name not resolved in $this"
else "${properties.size()} properties with name $name resolved in $this"
)
}
properties.single() as PropertyDescriptor
}
}
@@ -16,8 +16,15 @@
package kotlin.reflect.jvm.internal
import kotlin.reflect.*
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.load.java.structure.reflect.classId
import org.jetbrains.kotlin.resolve.scopes.JetScope
import org.jetbrains.kotlin.serialization.deserialization.findClassAcrossModuleDependencies
import kotlin.jvm.internal.KotlinClass
import kotlin.reflect.KClass
import kotlin.reflect.KMemberProperty
import kotlin.reflect.KMutableMemberProperty
import kotlin.reflect.KotlinReflectionInternalError
enum class KClassOrigin {
BUILT_IN
@@ -25,10 +32,21 @@ enum class KClassOrigin {
FOREIGN
}
class KClassImpl<T>(val jClass: Class<T>) : KClass<T> {
class KClassImpl<T>(override val jClass: Class<T>) : KCallableContainerImpl(), KClass<T> {
// Don't use kotlin.properties.Delegates here because it's a Kotlin class which will invoke KClassImpl() in <clinit>,
// resulting in infinite recursion
val descriptor by ReflectProperties.lazySoft {(): ClassDescriptor ->
val moduleData = jClass.getOrCreateModule()
val found = moduleData.module.findClassAcrossModuleDependencies(jClass.classId)
if (found != null) return@lazySoft found
throw KotlinReflectionInternalError("Class not resolved: $jClass")
}
override val scope: JetScope get() = descriptor.getDefaultType().getMemberScope()
private val origin by ReflectProperties.lazy {(): KClassOrigin ->
if (jClass.isAnnotationPresent(javaClass<KotlinClass>())) {
KClassOrigin.KOTLIN
@@ -39,21 +57,27 @@ class KClassImpl<T>(val jClass: Class<T>) : KClass<T> {
}
}
fun memberProperty(name: String): KMemberProperty<T, *> =
if (origin === KClassOrigin.KOTLIN) {
KMemberPropertyImpl<T, Any>(name, this)
}
else {
KForeignMemberProperty<T, Any>(name, this)
}
fun memberProperty(name: String): KMemberProperty<T, *> {
val computeDescriptor = findPropertyDescriptor(name)
fun mutableMemberProperty(name: String): KMutableMemberProperty<T, *> =
if (origin === KClassOrigin.KOTLIN) {
KMutableMemberPropertyImpl<T, Any>(name, this)
}
else {
KMutableForeignMemberProperty<T, Any>(name, this)
}
if (origin === KClassOrigin.KOTLIN) {
return KMemberPropertyImpl<T, Any>(this, computeDescriptor)
}
else {
return KForeignMemberProperty<T, Any>(name, this)
}
}
fun mutableMemberProperty(name: String): KMutableMemberProperty<T, *> {
val computeDescriptor = findPropertyDescriptor(name)
if (origin === KClassOrigin.KOTLIN) {
return KMutableMemberPropertyImpl<T, Any>(this, computeDescriptor)
}
else {
return KMutableForeignMemberProperty<T, Any>(name, this)
}
}
override fun equals(other: Any?): Boolean =
other is KClassImpl<*> && jClass == other.jClass
@@ -16,39 +16,22 @@
package kotlin.reflect.jvm.internal
import java.lang.reflect.*
import kotlin.reflect.*
// TODO: properties of built-in classes
import org.jetbrains.kotlin.descriptors.PropertyDescriptor
import kotlin.reflect.IllegalPropertyAccessException
import kotlin.reflect.KMemberProperty
import kotlin.reflect.KMutableMemberProperty
open class KMemberPropertyImpl<T : Any, out R>(
override val name: String,
protected val owner: KClassImpl<T>
) : KMemberProperty<T, R>, KPropertyImpl<R> {
// TODO: extract, make lazy (weak?), use our descriptors knowledge
override val field: Field?
override val getter: Method?
{
try {
field = owner.jClass.getDeclaredField(name)
}
catch (e: NoSuchFieldException) {
field = null
}
try {
getter = owner.jClass.getMaybeDeclaredMethod(getterName(name))
}
catch (e: NoSuchMethodException) {
if (field == null) throw NoSuchPropertyException(e)
getter = null
}
}
override val container: KClassImpl<T>,
computeDescriptor: () -> PropertyDescriptor
) : DescriptorBasedProperty(computeDescriptor), KMemberProperty<T, R>, KPropertyImpl<R> {
override val name: String get() = descriptor.getName().asString()
override fun get(receiver: T): R {
try {
return (if (getter != null) getter!!(receiver) else field!!.get(receiver)) as R
val getter = getter
[suppress("UNCHECKED_CAST")]
return if (getter != null) getter(receiver) as R else field!!.get(receiver) as R
}
catch (e: IllegalAccessException) {
throw IllegalPropertyAccessException(e)
@@ -56,42 +39,27 @@ open class KMemberPropertyImpl<T : Any, out R>(
}
override fun equals(other: Any?): Boolean =
other is KMemberPropertyImpl<*, *> && name == other.name && owner == other.owner
other is KMemberPropertyImpl<*, *> && descriptor == other.descriptor
override fun hashCode(): Int =
name.hashCode() * 31 + owner.hashCode()
descriptor.hashCode()
// TODO: include visibility, return type
override fun toString(): String =
"val ${owner.jClass.getName()}.$name"
ReflectionObjectRenderer.renderProperty(descriptor)
}
class KMutableMemberPropertyImpl<T : Any, R>(
name: String,
owner: KClassImpl<T>
) : KMutableMemberProperty<T, R>, KMutablePropertyImpl<R>, KMemberPropertyImpl<T, R>(name, owner) {
override val setter: Method?
{
try {
val returnType = if (getter != null) getter.getReturnType() else field!!.getType()
setter = owner.jClass.getMaybeDeclaredMethod(setterName(name), returnType)
}
catch (e: NoSuchMethodException) {
if (field == null) throw NoSuchPropertyException(e)
setter = null
}
}
container: KClassImpl<T>,
computeDescriptor: () -> PropertyDescriptor
) : KMemberPropertyImpl<T, R>(container, computeDescriptor), KMutableMemberProperty<T, R>, KMutablePropertyImpl<R> {
override fun set(receiver: T, value: R) {
try {
if (setter != null) setter!!(receiver, value) else field!!.set(receiver, value)
val setter = setter
if (setter != null) setter(receiver, value) else field!!.set(receiver, value)
}
catch (e: IllegalAccessException) {
throw IllegalPropertyAccessException(e)
}
}
override fun toString(): String =
"var ${owner.jClass.getName()}.$name"
}
@@ -0,0 +1,51 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* 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 kotlin.reflect.jvm.internal
import org.jetbrains.kotlin.descriptors.PropertyDescriptor
import org.jetbrains.kotlin.descriptors.ReceiverParameterDescriptor
import org.jetbrains.kotlin.renderer.DescriptorRenderer
object ReflectionObjectRenderer {
private val renderer = DescriptorRenderer.FQ_NAMES_IN_TYPES
// TODO: include visibility, return type
fun renderProperty(descriptor: PropertyDescriptor): String {
fun StringBuilder.appendReceiverType(receiver: ReceiverParameterDescriptor?) {
if (receiver != null) {
append(renderer.renderType(receiver.getType()))
append(".")
}
}
return StringBuilder {
append(if (descriptor.isVar()) "var " else "val ")
val dispatchReceiver = descriptor.getDispatchReceiverParameter()
val extensionReceiver = descriptor.getExtensionReceiverParameter()
appendReceiverType(dispatchReceiver)
val addParentheses = dispatchReceiver != null && extensionReceiver != null
if (addParentheses) append("(")
appendReceiverType(extensionReceiver)
if (addParentheses) append(")")
append(renderer.renderName(descriptor.getName()))
}.toString()
}
}
@@ -0,0 +1,68 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* 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 kotlin.reflect.jvm.internal
import org.jetbrains.kotlin.load.java.structure.reflect.classLoader
import org.jetbrains.kotlin.load.kotlin.reflect.RuntimeModuleData
import java.lang.ref.WeakReference
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentMap
// TODO: collect nulls periodically
private val moduleByClassLoader: ConcurrentMap<WeakClassLoaderBox, WeakReference<RuntimeModuleData>> = ConcurrentHashMap()
private class WeakClassLoaderBox(classLoader: ClassLoader) {
val ref: WeakReference<ClassLoader> = WeakReference(classLoader)
// Identity hash code is saved because otherwise once the weak reference is GC'd we cannot compute it anymore
val identityHashCode: Int = System.identityHashCode(classLoader)
// Temporary strong reference to the class loader to ensure it won't get GC'd when we're inserting this box into the map
var temporaryStrongRef: ClassLoader? = classLoader
override fun equals(other: Any?) =
other is WeakClassLoaderBox && ref.get() === other.ref.get()
override fun hashCode() =
identityHashCode
}
private fun Class<*>.getOrCreateModule(): RuntimeModuleData {
val classLoader = this.classLoader
val key = WeakClassLoaderBox(classLoader)
val cached = moduleByClassLoader[key]
if (cached != null) {
cached.get()?.let { return it }
moduleByClassLoader.remove(key, cached)
}
val module = RuntimeModuleData.create(classLoader)
try {
while (true) {
val ref = moduleByClassLoader.putIfAbsent(key, WeakReference(module))
if (ref == null) return module
val result = ref.get()
if (result != null) return result
moduleByClassLoader.remove(key, ref)
}
}
finally {
key.temporaryStrongRef = null
}
}
@@ -16,7 +16,14 @@
package kotlin.reflect.jvm.internal
import org.jetbrains.kotlin.load.java.structure.reflect.classLoader
import org.jetbrains.kotlin.load.java.structure.reflect.createArrayType
import org.jetbrains.kotlin.serialization.deserialization.NameResolver
import org.jetbrains.kotlin.serialization.jvm.JvmProtoBuf
import org.jetbrains.kotlin.serialization.jvm.JvmProtoBuf.JvmType.PrimitiveType.*
import java.lang.reflect.Field
import java.lang.reflect.Method
import kotlin.reflect.KotlinReflectionInternalError
private fun String.capitalizeWithJavaBeanConvention(): String {
if (length() > 1 && Character.isUpperCase(this[1])) return this
@@ -36,3 +43,73 @@ private fun Class<*>.getMaybeDeclaredMethod(name: String, vararg parameterTypes:
return getDeclaredMethod(name, *parameterTypes)
}
}
// TODO: check resulting method's return type
private fun KCallableContainerImpl.findMethodBySignature(signature: JvmProtoBuf.JvmMethodSignature, nameResolver: NameResolver): Method? {
val name = nameResolver.getString(signature.getName())
val classLoader = jClass.classLoader
val parameterTypes = signature.getParameterTypeList().map { jvmType ->
loadJvmType(jvmType, nameResolver, classLoader)
}.copyToArray()
return try {
jClass.getMaybeDeclaredMethod(name, *parameterTypes)
}
catch (e: NoSuchMethodException) {
null
}
}
// TODO: check resulting field's type
private fun KCallableContainerImpl.findFieldBySignature(signature: JvmProtoBuf.JvmFieldSignature, nameResolver: NameResolver): Field? {
val name = nameResolver.getString(signature.getName())
val owner =
if (signature.getIsStaticInOuter())
jClass.getEnclosingClass()
?: throw KotlinReflectionInternalError("Inconsistent metadata for field $name in $jClass")
else jClass
return try {
owner.getDeclaredField(name)
}
catch (e: NoSuchFieldException) {
null
}
}
private val PRIMITIVE_TYPES = mapOf(
VOID to Void.TYPE,
BOOLEAN to java.lang.Boolean.TYPE,
CHAR to java.lang.Character.TYPE,
BYTE to java.lang.Byte.TYPE,
SHORT to java.lang.Short.TYPE,
INT to java.lang.Integer.TYPE,
FLOAT to java.lang.Float.TYPE,
LONG to java.lang.Long.TYPE,
DOUBLE to java.lang.Double.TYPE
)
private fun loadJvmType(
type: JvmProtoBuf.JvmType,
nameResolver: NameResolver,
classLoader: ClassLoader,
arrayDimension: Int = type.getArrayDimension()
): Class<*> {
if (arrayDimension > 0) {
// TODO: test multi-dimensional arrays
return loadJvmType(type, nameResolver, classLoader, arrayDimension - 1).createArrayType()
}
if (type.hasPrimitiveType()) {
return PRIMITIVE_TYPES[type.getPrimitiveType()]
?: throw KotlinReflectionInternalError("Unknown primitive type: ${type.getPrimitiveType()}")
}
if (type.hasClassFqName()) {
val fqName = nameResolver.getFqName(type.getClassFqName())
return classLoader.loadClass(fqName.asString())
}
throw KotlinReflectionInternalError("Inconsistent metadata for JVM type")
}