From 3c520a3ce3ef84fe6dfc603d46ac00e91ca71d4b Mon Sep 17 00:00:00 2001 From: Zalim Bashorov Date: Tue, 4 Oct 2016 17:39:37 +0300 Subject: [PATCH] JS backend: basic support for class literals. Added: * the ability to get KClass using class literals (`::class`); * the ability to get KClass from JsClass and vice versa; * the ability to get simpleName. #KT-13345 Fixed --- .idea/dictionaries/bashor.xml | 1 + .../reflection.kt => reflect/JsClass.kt} | 11 +++ js/js.libraries/src/reflect/KClassImpl.kt | 81 +++++++++++++++++++ js/js.libraries/src/reflect/reflection.kt | 44 ++++++++++ .../js/test/semantics/BoxJsTestGenerated.java | 24 ++++++ .../kotlin/js/translate/context/Namer.java | 2 + .../expression/ExpressionVisitor.java | 32 ++++++-- .../testData/box/reflection/_common.kt | 15 ++++ .../testData/box/reflection/jsClass.kt | 11 +-- .../box/reflection/jsClassOnReifiedType.kt | 8 +- .../testData/box/reflection/kClass.kt | 14 ++++ .../box/reflection/kClassOnReifiedType.kt | 19 +++++ .../box/reflection/kClassSimpleName.kt | 40 +++++++++ .../box/reflection/kClassToAndFromJsClass.kt | 29 +++++++ 14 files changed, 310 insertions(+), 21 deletions(-) rename js/js.libraries/src/{core/reflection.kt => reflect/JsClass.kt} (64%) create mode 100644 js/js.libraries/src/reflect/KClassImpl.kt create mode 100644 js/js.libraries/src/reflect/reflection.kt create mode 100644 js/js.translator/testData/box/reflection/kClass.kt create mode 100644 js/js.translator/testData/box/reflection/kClassOnReifiedType.kt create mode 100644 js/js.translator/testData/box/reflection/kClassSimpleName.kt create mode 100644 js/js.translator/testData/box/reflection/kClassToAndFromJsClass.kt diff --git a/.idea/dictionaries/bashor.xml b/.idea/dictionaries/bashor.xml index fa362eaa919..b3a119e13fe 100644 --- a/.idea/dictionaries/bashor.xml +++ b/.idea/dictionaries/bashor.xml @@ -3,6 +3,7 @@ ctor interner + kclass lookups unescape diff --git a/js/js.libraries/src/core/reflection.kt b/js/js.libraries/src/reflect/JsClass.kt similarity index 64% rename from js/js.libraries/src/core/reflection.kt rename to js/js.libraries/src/reflect/JsClass.kt index f9d3cc1cc4b..a373ab74ecd 100644 --- a/js/js.libraries/src/core/reflection.kt +++ b/js/js.libraries/src/reflect/JsClass.kt @@ -16,13 +16,24 @@ package kotlin.js +import kotlin.reflect.KClass +import kotlin.reflect.js.internal.KClassImpl + @native interface JsClass { val name: String } @native +@Deprecated("Use class literal and extension property `js` instead.", replaceWith = ReplaceWith("T::class.js")) fun jsClass(): JsClass = noImpl +@Deprecated("Use class literal and extension property `js` instead.", replaceWith = ReplaceWith("this::class.js")) val T.jsClass: JsClass get() = js("Object").getPrototypeOf(this).constructor + +val KClass.js: JsClass + get() = (this as KClassImpl).jClass + +val JsClass.kotlin: KClass + get() = KClassImpl(this) diff --git a/js/js.libraries/src/reflect/KClassImpl.kt b/js/js.libraries/src/reflect/KClassImpl.kt new file mode 100644 index 00000000000..b85773887a1 --- /dev/null +++ b/js/js.libraries/src/reflect/KClassImpl.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2010-2016 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.js.internal + +import kotlin.reflect.* + +internal class KClassImpl( + internal val jClass: JsClass +) : KClass { + + private val metadata = jClass.asDynamic().`$metadata$` + // TODO: use FQN + private val hashCode = simpleName?.hashCode() ?: 0 + + override val simpleName: String? + get() = metadata?.simpleName + + override val annotations: List + get() = TODO() + override val constructors: Collection> + get() = TODO() + override val isAbstract: Boolean + get() = TODO() + override val isCompanion: Boolean + get() = TODO() + override val isData: Boolean + get() = TODO() + override val isFinal: Boolean + get() = TODO() + override val isInner: Boolean + get() = TODO() + override val isOpen: Boolean + get() = TODO() + override val isSealed: Boolean + get() = TODO() + override val members: Collection> + get() = TODO() + override val nestedClasses: Collection> + get() = TODO() + override val objectInstance: T? + get() = TODO() + override val qualifiedName: String? + get() = TODO() + override val supertypes: List + get() = TODO() + override val typeParameters: List + get() = TODO() + override val visibility: KVisibility? + get() = TODO() + + override fun equals(other: Any?): Boolean { + return other is KClassImpl<*> && jClass == other.jClass + } + + override fun hashCode(): Int { + return hashCode + } + + override fun isInstance(value: Any?): Boolean { + TODO() + } + + override fun toString(): String { + // TODO: use FQN + return "class $simpleName" + } +} diff --git a/js/js.libraries/src/reflect/reflection.kt b/js/js.libraries/src/reflect/reflection.kt new file mode 100644 index 00000000000..e5ed63f3c04 --- /dev/null +++ b/js/js.libraries/src/reflect/reflection.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2010-2016 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. + */ + +// a package is omitted to get declarations directly under the module + +import kotlin.reflect.KClass +import kotlin.reflect.js.internal.KClassImpl + +@JsName("getKClass") +private fun getKClass(jClass: JsClass): KClass = getOrCreateKClass(jClass) +@JsName("getKClassFromExpression") +private fun getKClassFromExpression(e: T): KClass = getOrCreateKClass(e.jsClass) + +private fun getOrCreateKClass(jClass: JsClass): KClass { + val metadata = jClass.asDynamic().`$metadata$` + + return if (metadata != null) { + if (metadata.`$kClass$` == null) { + val kClass = KClassImpl(jClass) + metadata.`$kClass$` = kClass + kClass + } + else { + metadata.`$kClass$` + } + } + else { + KClassImpl(jClass) + } +} + diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/BoxJsTestGenerated.java b/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/BoxJsTestGenerated.java index f63b6963971..13096e0a2f7 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/BoxJsTestGenerated.java +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/BoxJsTestGenerated.java @@ -6252,6 +6252,30 @@ public class BoxJsTestGenerated extends AbstractBoxJsTest { String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/box/reflection/jsClassSimpleName.kt"); doTest(fileName); } + + @TestMetadata("kClass.kt") + public void testKClass() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/box/reflection/kClass.kt"); + doTest(fileName); + } + + @TestMetadata("kClassOnReifiedType.kt") + public void testKClassOnReifiedType() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/box/reflection/kClassOnReifiedType.kt"); + doTest(fileName); + } + + @TestMetadata("kClassSimpleName.kt") + public void testKClassSimpleName() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/box/reflection/kClassSimpleName.kt"); + doTest(fileName); + } + + @TestMetadata("kClassToAndFromJsClass.kt") + public void testKClassToAndFromJsClass() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/box/reflection/kClassToAndFromJsClass.kt"); + doTest(fileName); + } } @TestMetadata("js/js.translator/testData/box/regression") diff --git a/js/js.translator/src/org/jetbrains/kotlin/js/translate/context/Namer.java b/js/js.translator/src/org/jetbrains/kotlin/js/translate/context/Namer.java index 99683a0e7f1..ceb3443dbb4 100644 --- a/js/js.translator/src/org/jetbrains/kotlin/js/translate/context/Namer.java +++ b/js/js.translator/src/org/jetbrains/kotlin/js/translate/context/Namer.java @@ -67,6 +67,8 @@ public final class Namer { public static final String IS_CHAR = "isChar"; public static final String IS_NUMBER = "isNumber"; public static final String IS_CHAR_SEQUENCE = "isCharSequence"; + public static final String GET_KCLASS = "getKClass"; + public static final String GET_KCLASS_FROM_EXPRESSION = "getKClassFromExpression"; public static final String CALLEE_NAME = "$fun"; diff --git a/js/js.translator/src/org/jetbrains/kotlin/js/translate/expression/ExpressionVisitor.java b/js/js.translator/src/org/jetbrains/kotlin/js/translate/expression/ExpressionVisitor.java index 7c2948d1039..b417706f78e 100644 --- a/js/js.translator/src/org/jetbrains/kotlin/js/translate/expression/ExpressionVisitor.java +++ b/js/js.translator/src/org/jetbrains/kotlin/js/translate/expression/ExpressionVisitor.java @@ -29,14 +29,15 @@ import org.jetbrains.kotlin.descriptors.annotations.KotlinRetention; import org.jetbrains.kotlin.js.translate.context.Namer; import org.jetbrains.kotlin.js.translate.context.TranslationContext; import org.jetbrains.kotlin.js.translate.declaration.ClassTranslator; -import org.jetbrains.kotlin.js.translate.general.Translation; import org.jetbrains.kotlin.js.translate.declaration.PropertyTranslatorKt; +import org.jetbrains.kotlin.js.translate.general.Translation; import org.jetbrains.kotlin.js.translate.general.TranslatorVisitor; import org.jetbrains.kotlin.js.translate.operation.BinaryOperationTranslator; import org.jetbrains.kotlin.js.translate.operation.UnaryOperationTranslator; import org.jetbrains.kotlin.js.translate.reference.*; import org.jetbrains.kotlin.js.translate.utils.BindingUtils; import org.jetbrains.kotlin.js.translate.utils.JsAstUtils; +import org.jetbrains.kotlin.js.translate.utils.UtilsKt; import org.jetbrains.kotlin.psi.*; import org.jetbrains.kotlin.psi.psiUtil.PsiUtilsKt; import org.jetbrains.kotlin.resolve.BindingContext; @@ -44,17 +45,16 @@ import org.jetbrains.kotlin.resolve.BindingContextUtils; import org.jetbrains.kotlin.resolve.bindingContextUtil.BindingContextUtilsKt; import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall; import org.jetbrains.kotlin.resolve.constants.CompileTimeConstant; -import org.jetbrains.kotlin.resolve.constants.ConstantValue; -import org.jetbrains.kotlin.resolve.constants.NullValue; import org.jetbrains.kotlin.resolve.constants.evaluate.ConstantExpressionEvaluator; import org.jetbrains.kotlin.resolve.descriptorUtil.DescriptorUtilsKt; import org.jetbrains.kotlin.resolve.inline.InlineUtil; -import org.jetbrains.kotlin.types.KotlinType; -import org.jetbrains.kotlin.types.TypeUtils; +import org.jetbrains.kotlin.types.expressions.DoubleColonLHS; import java.util.ArrayList; import java.util.List; +import static org.jetbrains.kotlin.js.translate.context.Namer.GET_KCLASS; +import static org.jetbrains.kotlin.js.translate.context.Namer.GET_KCLASS_FROM_EXPRESSION; import static org.jetbrains.kotlin.js.translate.context.Namer.getCapturedVarAccessor; import static org.jetbrains.kotlin.js.translate.general.Translation.translateAsExpression; import static org.jetbrains.kotlin.js.translate.utils.BindingUtils.*; @@ -63,8 +63,7 @@ import static org.jetbrains.kotlin.js.translate.utils.JsAstUtils.convertToStatem import static org.jetbrains.kotlin.js.translate.utils.JsAstUtils.newVar; import static org.jetbrains.kotlin.js.translate.utils.JsDescriptorUtils.getReceiverParameterForDeclaration; import static org.jetbrains.kotlin.js.translate.utils.TranslationUtils.translateInitializerForProperty; -import static org.jetbrains.kotlin.resolve.BindingContext.DECLARATION_TO_DESCRIPTOR; -import static org.jetbrains.kotlin.resolve.BindingContext.LABEL_TARGET; +import static org.jetbrains.kotlin.resolve.BindingContext.*; import static org.jetbrains.kotlin.resolve.BindingContextUtils.isVarCapturedInClosure; import static org.jetbrains.kotlin.resolve.calls.callUtil.CallUtilKt.getResolvedCallWithAssert; import static org.jetbrains.kotlin.types.expressions.ExpressionTypingUtils.isFunctionExpression; @@ -226,6 +225,25 @@ public final class ExpressionVisitor extends TranslatorVisitor { return CallableReferenceTranslator.INSTANCE.translate(expression, context); } + @Override + public JsNode visitClassLiteralExpression( + @NotNull KtClassLiteralExpression expression, TranslationContext context + ) { + KtExpression receiverExpression = expression.getReceiverExpression(); + assert receiverExpression != null : "Class literal expression should have a left-hand side"; + + DoubleColonLHS lhs = context.bindingContext().get(DOUBLE_COLON_LHS, receiverExpression); + assert lhs != null : "Class literal expression should have LHS resolved"; + + if (lhs instanceof DoubleColonLHS.Expression && !((DoubleColonLHS.Expression) lhs).isObject()) { + JsExpression receiver = translateAsExpression(receiverExpression, context); + return new JsInvocation(context.namer().kotlin(GET_KCLASS_FROM_EXPRESSION), receiver); + } + + return new JsInvocation(context.namer().kotlin(GET_KCLASS), UtilsKt.getReferenceToJsClass(lhs.getType(), context)); + } + + @Override @NotNull public JsNode visitCallExpression( diff --git a/js/js.translator/testData/box/reflection/_common.kt b/js/js.translator/testData/box/reflection/_common.kt index 129cfe42758..76885c9bba9 100644 --- a/js/js.translator/testData/box/reflection/_common.kt +++ b/js/js.translator/testData/box/reflection/_common.kt @@ -20,3 +20,18 @@ enum class E { @JsName("Q") class R + +fun check(x: Any, y: Any, shouldBeEqual: Boolean = true, shouldBeSame: Boolean = true) { + assertNotEquals(null, x) + assertNotEquals(null, y) + if (shouldBeEqual) { + assertEquals(x, y) + + if (shouldBeSame && x !== y) { + fail("Expected same instances, got expected = '$x', actual = '$y'") + } + } + else { + assertNotEquals(x, y) + } +} diff --git a/js/js.translator/testData/box/reflection/jsClass.kt b/js/js.translator/testData/box/reflection/jsClass.kt index 0097f0a7992..73431b882a8 100644 --- a/js/js.translator/testData/box/reflection/jsClass.kt +++ b/js/js.translator/testData/box/reflection/jsClass.kt @@ -1,21 +1,12 @@ package foo -fun check(x: JsClass<*>, y: JsClass<*>, shouldBeEquals: Boolean = true) { - assertNotEquals(null, x) - assertNotEquals(null, y) - if (shouldBeEquals) - assertEquals(x, y) - else - assertNotEquals(x, y) -} - fun box(): String { check(jsClass(), A().jsClass) check(jsClass(), B().jsClass) check(jsClass(), O.jsClass) assertNotEquals(null, jsClass()) check(jsClass(), E.X.jsClass) - check(jsClass(), E.Y.jsClass, shouldBeEquals = false) + check(jsClass(), E.Y.jsClass, shouldBeEqual = false) // TODO uncomment after KT-13338 is fixed // check(jsClass(), E.Z.jsClass) diff --git a/js/js.translator/testData/box/reflection/jsClassOnReifiedType.kt b/js/js.translator/testData/box/reflection/jsClassOnReifiedType.kt index a5675dc484f..d58c4ce10f5 100644 --- a/js/js.translator/testData/box/reflection/jsClassOnReifiedType.kt +++ b/js/js.translator/testData/box/reflection/jsClassOnReifiedType.kt @@ -6,10 +6,10 @@ inline fun foo(): JsClass { } fun box(): String { - assertEquals(jsClass(), foo()) - assertEquals(jsClass(), foo()) - assertEquals(jsClass(), foo()) - assertEquals(jsClass(), foo()) + check(jsClass(), foo()) + check(jsClass(), foo()) + check(jsClass(), foo()) + check(jsClass(), foo()) return "OK" } diff --git a/js/js.translator/testData/box/reflection/kClass.kt b/js/js.translator/testData/box/reflection/kClass.kt new file mode 100644 index 00000000000..80c91bb2116 --- /dev/null +++ b/js/js.translator/testData/box/reflection/kClass.kt @@ -0,0 +1,14 @@ +package foo + +fun box(): String { + check(A::class, A()::class) + check(B::class, B()::class) + check(O::class, (O)::class) + assertNotEquals(null, I::class) + check(E::class, E.X::class) + check(E::class, E.Y::class, shouldBeEqual = false) +// TODO uncomment after KT-13338 is fixed +// check(E::class, E.Z::class) + + return "OK" +} diff --git a/js/js.translator/testData/box/reflection/kClassOnReifiedType.kt b/js/js.translator/testData/box/reflection/kClassOnReifiedType.kt new file mode 100644 index 00000000000..fd1c6ba3f63 --- /dev/null +++ b/js/js.translator/testData/box/reflection/kClassOnReifiedType.kt @@ -0,0 +1,19 @@ +package foo + +import kotlin.reflect.KClass + +inline fun foo(b: Boolean = false): KClass { + if (b) { + val T = 1 + } + return T::class +} + +fun box(): String { + check(A::class, foo()) + check(B::class, foo()) + check(O::class, foo()) + check(E::class, foo()) + + return "OK" +} diff --git a/js/js.translator/testData/box/reflection/kClassSimpleName.kt b/js/js.translator/testData/box/reflection/kClassSimpleName.kt new file mode 100644 index 00000000000..3d2047acef1 --- /dev/null +++ b/js/js.translator/testData/box/reflection/kClassSimpleName.kt @@ -0,0 +1,40 @@ +package foo + +class undefined + +class Outer { + class Nested + + inner class Inner +} + +fun testWithInstance() { + assertEquals("A", A()::class.simpleName) + assertEquals("B", B()::class.simpleName) + assertEquals("O", (O)::class.simpleName) + assertEquals("E", E.X::class.simpleName) + assertEquals("Y", E.Y::class.simpleName) +// TODO uncomment after KT-13338 is fixed +// assertEquals("E", E.Z::class.simpleName) + assertEquals("undefined", undefined()::class.simpleName) + assertEquals("Nested", Outer.Nested()::class.simpleName) + assertEquals("Inner", Outer().Inner()::class.simpleName) +} + +fun testWithClassReference() { + assertEquals("A", A::class.simpleName) + assertEquals("B", B::class.simpleName) + assertEquals("O", O::class.simpleName) + assertEquals("I", I::class.simpleName) + assertEquals("E", E::class.simpleName) + assertEquals("undefined", foo.undefined::class.simpleName) + assertEquals("Nested", Outer.Nested::class.simpleName) + assertEquals("Inner", Outer.Inner::class.simpleName) +} + +fun box(): String { + testWithInstance() + testWithClassReference() + + return "OK" +} diff --git a/js/js.translator/testData/box/reflection/kClassToAndFromJsClass.kt b/js/js.translator/testData/box/reflection/kClassToAndFromJsClass.kt new file mode 100644 index 00000000000..8fd0e9d7bbb --- /dev/null +++ b/js/js.translator/testData/box/reflection/kClassToAndFromJsClass.kt @@ -0,0 +1,29 @@ +package foo + +import kotlin.reflect.KClass + +fun check(k: KClass<*>, j: JsClass<*>) { + assertNotEquals(null, k) + assertNotEquals(null, j) + + assertEquals(k.js, j) + assertEquals(k, j.kotlin) + assertEquals(j, j.kotlin.js) + assertEquals(k, k.js.kotlin) +} + +fun jsClassbyName(name: String) = js("JS_TESTS").foo[name] + +fun box(): String { + check(A::class, jsClassbyName("A")) + check(B::class, jsClassbyName("B")) + check(O::class, jsClassbyName("O").constructor) + check(I::class, jsClassbyName("I")) + check(E::class, jsClassbyName("E")) + check(E.X::class, jsClassbyName("E").X.constructor) + check(E.Y::class, jsClassbyName("E").Y.constructor) +// TODO uncomment after KT-13338 is fixed +// check(E.Z::class, jsClassbyName("E").Z.constructor) + + return "OK" +}