diff --git a/js/js.dart-ast/src/com/google/dart/compiler/backend/js/ast/metadata/metadataProperties.kt b/js/js.dart-ast/src/com/google/dart/compiler/backend/js/ast/metadata/metadataProperties.kt index d323dfa2576..4427204fce2 100644 --- a/js/js.dart-ast/src/com/google/dart/compiler/backend/js/ast/metadata/metadataProperties.kt +++ b/js/js.dart-ast/src/com/google/dart/compiler/backend/js/ast/metadata/metadataProperties.kt @@ -36,6 +36,8 @@ var JsFunction.isLocal: Boolean by MetadataProperty(default = false) var JsParameter.hasDefaultValue: Boolean by MetadataProperty(default = false) +var JsConditional.isUnsafeCast: Boolean by MetadataProperty(default = false) + var JsInvocation.typeCheck: TypeCheck? by MetadataProperty(default = null) /** diff --git a/js/js.libraries/src/core/javalang.kt b/js/js.libraries/src/core/javalang.kt index ada48ae15d3..b4547a93b34 100644 --- a/js/js.libraries/src/core/javalang.kt +++ b/js/js.libraries/src/core/javalang.kt @@ -27,6 +27,9 @@ public class NumberFormatException(message: String? = null) : RuntimeException(m @library public class NullPointerException(message: String? = null) : RuntimeException(message) {} +library +public class ClassCastException(message: String? = null) : RuntimeException(message) {} + @library public class AssertionError(message: String? = null) : Error(message) {} diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/CastTestGenerated.java b/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/CastTestGenerated.java index a8960e737ee..4ec43632889 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/CastTestGenerated.java +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/CastTestGenerated.java @@ -41,15 +41,27 @@ public class CastTestGenerated extends AbstractCastTest { doTest(fileName); } - @TestMetadata("castToNotNullType.kt") - public void testCastToNotNullType() throws Exception { - String fileName = JetTestUtils.navigationMetadata("js/js.translator/testData/expression/cast/cases/castToNotNullType.kt"); + @TestMetadata("castToNotNull.kt") + public void testCastToNotNull() throws Exception { + String fileName = JetTestUtils.navigationMetadata("js/js.translator/testData/expression/cast/cases/castToNotNull.kt"); doTest(fileName); } - @TestMetadata("castToNullableType.kt") - public void testCastToNullableType() throws Exception { - String fileName = JetTestUtils.navigationMetadata("js/js.translator/testData/expression/cast/cases/castToNullableType.kt"); + @TestMetadata("castToNullable.kt") + public void testCastToNullable() throws Exception { + String fileName = JetTestUtils.navigationMetadata("js/js.translator/testData/expression/cast/cases/castToNullable.kt"); + doTest(fileName); + } + + @TestMetadata("reifiedToNotNull.kt") + public void testReifiedToNotNull() throws Exception { + String fileName = JetTestUtils.navigationMetadata("js/js.translator/testData/expression/cast/cases/reifiedToNotNull.kt"); + doTest(fileName); + } + + @TestMetadata("reifiedToNullable.kt") + public void testReifiedToNullable() throws Exception { + String fileName = JetTestUtils.navigationMetadata("js/js.translator/testData/expression/cast/cases/reifiedToNullable.kt"); doTest(fileName); } diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/utils/DirectiveTestUtils.java b/js/js.tests/test/org/jetbrains/kotlin/js/test/utils/DirectiveTestUtils.java index 189273c5485..352e0296433 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/utils/DirectiveTestUtils.java +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/utils/DirectiveTestUtils.java @@ -148,6 +148,22 @@ public class DirectiveTestUtils { private static final DirectiveHandler COUNT_NULLS = new CountNodesDirective("CHECK_NULLS_COUNT", JsNullLiteral.class); + private static final DirectiveHandler NOT_REFERENCED = new DirectiveHandler("CHECK_NOT_REFERENCED") { + @Override + void processEntry(@NotNull JsNode ast, @NotNull ArgumentsHelper arguments) throws Exception { + final String reference = arguments.getPositionalArgument(0); + + JsVisitor visitor = new RecursiveJsVisitor() { + @Override + public void visitNameRef(@NotNull JsNameRef nameRef) { + assertNotEquals(reference, nameRef.toString()); + } + }; + + visitor.accept(ast); + } + }; + private static final DirectiveHandler HAS_INLINE_METADATA = new DirectiveHandler("CHECK_HAS_INLINE_METADATA") { @Override void processEntry(@NotNull JsNode ast, @NotNull ArgumentsHelper arguments) throws Exception { @@ -178,6 +194,7 @@ public class DirectiveTestUtils { COUNT_VARS, COUNT_BREAKS, COUNT_NULLS, + NOT_REFERENCED, HAS_INLINE_METADATA, HAS_NO_INLINE_METADATA ); 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 f8404177b29..cb5590502e0 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 @@ -92,6 +92,7 @@ public final class Namer { public static final String ANOTHER_THIS_PARAMETER_NAME = "$this"; private static final String THROW_NPE_FUN_NAME = "throwNPE"; + private static final String THROW_CLASS_CAST_EXCEPTION_FUN_NAME = "throwCCE"; private static final String PROTOTYPE_NAME = "prototype"; private static final String CAPTURED_VAR_FIELD = "v"; @@ -363,6 +364,11 @@ public final class Namer { return new JsNameRef(THROW_NPE_FUN_NAME, kotlinObject()); } + @NotNull + public JsExpression throwClassCastExceptionFunRef() { + return new JsNameRef(THROW_CLASS_CAST_EXCEPTION_FUN_NAME, kotlinObject()); + } + @NotNull public static JsNameRef kotlin(@NotNull JsName name) { return fqnWithoutSideEffects(name, kotlinObject()); 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 8425bcdbff6..94c1109d3cd 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 @@ -36,8 +36,6 @@ 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.TranslationUtils; -import org.jetbrains.kotlin.lexer.KtTokens; import org.jetbrains.kotlin.psi.*; import org.jetbrains.kotlin.psi.psiUtil.PsiUtilsKt; import org.jetbrains.kotlin.resolve.BindingContext; @@ -392,25 +390,20 @@ public final class ExpressionVisitor extends TranslatorVisitor { @Override @NotNull - public JsNode visitBinaryWithTypeRHSExpression(@NotNull KtBinaryExpressionWithTypeRHS expression, - @NotNull TranslationContext context) { - JsExpression jsExpression = Translation.translateAsExpression(expression.getLeft(), context); + public JsNode visitBinaryWithTypeRHSExpression( + @NotNull KtBinaryExpressionWithTypeRHS expression, + @NotNull TranslationContext context + ) { + JsExpression jsExpression; - if (expression.getOperationReference().getReferencedNameElementType() != KtTokens.AS_KEYWORD) - return jsExpression.source(expression); - - KtTypeReference right = expression.getRight(); - assert right != null; - - KotlinType rightType = BindingContextUtils.getNotNull(context.bindingContext(), BindingContext.TYPE, right); - KotlinType leftType = BindingContextUtils.getTypeNotNull(context.bindingContext(), expression.getLeft()); - if (TypeUtils.isNullableType(rightType) || !TypeUtils.isNullableType(leftType)) { - return jsExpression.source(expression); + if (PatternTranslator.isUnsafeCast(expression)) { + jsExpression = PatternTranslator.newInstance(context).translateUnsafeCast(expression); + } + else { + jsExpression = Translation.translateAsExpression(expression.getLeft(), context); } - // KT-2670 - // we actually do not care for types in js - return TranslationUtils.sure(jsExpression, context).source(expression); + return jsExpression.source(expression); } private static String getReferencedName(KtSimpleNameExpression expression) { diff --git a/js/js.translator/src/org/jetbrains/kotlin/js/translate/expression/PatternTranslator.java b/js/js.translator/src/org/jetbrains/kotlin/js/translate/expression/PatternTranslator.java index 7f75bb8584d..ece9e967ff0 100644 --- a/js/js.translator/src/org/jetbrains/kotlin/js/translate/expression/PatternTranslator.java +++ b/js/js.translator/src/org/jetbrains/kotlin/js/translate/expression/PatternTranslator.java @@ -16,20 +16,25 @@ package org.jetbrains.kotlin.js.translate.expression; +import com.google.dart.compiler.backend.js.ast.JsConditional; import com.google.dart.compiler.backend.js.ast.JsExpression; import com.google.dart.compiler.backend.js.ast.JsInvocation; import com.google.dart.compiler.backend.js.ast.JsNameRef; +import com.google.dart.compiler.backend.js.ast.metadata.MetadataProperties; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.kotlin.descriptors.*; -import org.jetbrains.kotlin.js.patterns.NamePredicate; import org.jetbrains.kotlin.js.descriptorUtils.DescriptorUtilsKt; +import org.jetbrains.kotlin.js.patterns.NamePredicate; import org.jetbrains.kotlin.js.translate.context.Namer; +import org.jetbrains.kotlin.js.translate.context.TemporaryVariable; import org.jetbrains.kotlin.js.translate.context.TranslationContext; import org.jetbrains.kotlin.js.translate.general.AbstractTranslator; import org.jetbrains.kotlin.js.translate.general.Translation; import org.jetbrains.kotlin.js.translate.utils.BindingUtils; +import org.jetbrains.kotlin.lexer.KtTokens; import org.jetbrains.kotlin.name.Name; +import org.jetbrains.kotlin.psi.KtBinaryExpressionWithTypeRHS; import org.jetbrains.kotlin.psi.KtExpression; import org.jetbrains.kotlin.psi.KtIsExpression; import org.jetbrains.kotlin.psi.KtTypeReference; @@ -50,6 +55,29 @@ public final class PatternTranslator extends AbstractTranslator { super(context); } + public static boolean isUnsafeCast(@NotNull KtBinaryExpressionWithTypeRHS expression) { + return expression.getOperationReference().getReferencedNameElementType() == KtTokens.AS_KEYWORD; + } + + @NotNull + public JsExpression translateUnsafeCast(@NotNull KtBinaryExpressionWithTypeRHS expression) { + assert isUnsafeCast(expression): "Expected unsafe cast expression, got " + expression; + KtExpression left = expression.getLeft(); + JsExpression expressionToCast = Translation.translateAsExpression(left, context()); + TemporaryVariable temporary = context().declareTemporary(expressionToCast); + + KtTypeReference typeReference = expression.getRight(); + assert typeReference != null: "Unsafe cast must have type reference"; + JsExpression isCheck = translateIsCheck(temporary.assignmentExpression(), typeReference); + + Namer namer = context().namer(); + JsExpression throwCCEFunRef = namer.throwClassCastExceptionFunRef(); + JsExpression throwCCE = new JsInvocation(throwCCEFunRef); + JsConditional conditional = new JsConditional(isCheck, temporary.reference(), throwCCE); + MetadataProperties.setUnsafeCast(conditional, true); + return conditional; + } + @NotNull public JsExpression translateIsExpression(@NotNull KtIsExpression expression) { JsExpression left = Translation.translateAsExpression(expression.getLeftHandSide(), context()); diff --git a/js/js.translator/src/org/jetbrains/kotlin/js/translate/utils/expandIsCalls.kt b/js/js.translator/src/org/jetbrains/kotlin/js/translate/utils/expandIsCalls.kt index f8205c51244..598019e4a95 100644 --- a/js/js.translator/src/org/jetbrains/kotlin/js/translate/utils/expandIsCalls.kt +++ b/js/js.translator/src/org/jetbrains/kotlin/js/translate/utils/expandIsCalls.kt @@ -18,6 +18,7 @@ package org.jetbrains.kotlin.js.translate.utils import com.google.dart.compiler.backend.js.ast.* import com.google.dart.compiler.backend.js.ast.metadata.TypeCheck +import com.google.dart.compiler.backend.js.ast.metadata.isUnsafeCast import com.google.dart.compiler.backend.js.ast.metadata.typeCheck import org.jetbrains.kotlin.js.inline.util.IdentitySet import org.jetbrains.kotlin.js.translate.context.TranslationContext @@ -50,6 +51,17 @@ private class TypeCheckRewritingVisitor(private val context: TranslationContext) super.endVisit(x, ctx) } + override fun endVisit(x: JsConditional, ctx: JsContext) { + val test = x.testExpression + + if (x.isUnsafeCast && + test is JsBinaryOperation && + test.operator == JsBinaryOperator.ASG + ) { + ctx.replaceMe(test.arg2) + } + } + override fun visit(x: JsInvocation, ctx: JsContext): Boolean { // callee(calleeArgument)(argument) val callee = x.qualifier as? JsInvocation diff --git a/js/js.translator/testData/expression/cast/_commonFiles/asserts.kt b/js/js.translator/testData/expression/cast/_commonFiles/asserts.kt new file mode 100644 index 00000000000..cf42464c974 --- /dev/null +++ b/js/js.translator/testData/expression/cast/_commonFiles/asserts.kt @@ -0,0 +1,16 @@ +package kotlin + +// CHECK_NOT_REFERENCED: Kotlin.isInstanceOf +// CHECK_NOT_REFERENCED: Kotlin.isTypeOf +// CHECK_NOT_REFERENCED: Kotlin.orNull + +fun failsClassCast(message: String, fn: ()->Unit) { + try { + fn() + } + catch (e: ClassCastException) { + return + } + + throw Exception("Expected ClassCastException to be thrown: message=$message") +} \ No newline at end of file diff --git a/js/js.translator/testData/expression/cast/cases/castToGenericType.kt b/js/js.translator/testData/expression/cast/cases/castToGenericType.kt index 171621bf5aa..9a2d568be8a 100644 --- a/js/js.translator/testData/expression/cast/cases/castToGenericType.kt +++ b/js/js.translator/testData/expression/cast/cases/castToGenericType.kt @@ -43,17 +43,6 @@ fun test(f: () -> Unit) { } } -fun fails(f: () -> Unit) { - try { - f() - } - catch(e: Exception) { - return - } - - throw Exception("Expected an exception to be thrown from $f") -} - fun box(): String { val a = A("OK") @@ -69,6 +58,7 @@ fun box(): String { test { castNullableToNotNullT(a) } fails { castNullableToNotNullT(null) } + failsClassCast("castNullableToNotNullT(null)") { castNullableToNotNullT(null) } return "OK" } diff --git a/js/js.translator/testData/expression/cast/cases/castToNotNull.kt b/js/js.translator/testData/expression/cast/cases/castToNotNull.kt new file mode 100644 index 00000000000..9b89ea3f4ee --- /dev/null +++ b/js/js.translator/testData/expression/cast/cases/castToNotNull.kt @@ -0,0 +1,18 @@ +package foo + +trait A + +class AImpl: A + +fun test(x: Any?): A = x as A + +fun box(): String { + var a: A = AImpl() + assertEquals(a, test(a), "a = AImpl()") + a = object : A {} + assertEquals(a, test(a), "a = object : A{}") + failsClassCast("test(null)") { test(null) } + failsClassCast("test(object{})") { test(object{}) } + + return "OK" +} \ No newline at end of file diff --git a/js/js.translator/testData/expression/cast/cases/castToNotNullType.kt b/js/js.translator/testData/expression/cast/cases/castToNotNullType.kt deleted file mode 100644 index 54be7f57779..00000000000 --- a/js/js.translator/testData/expression/cast/cases/castToNotNullType.kt +++ /dev/null @@ -1,26 +0,0 @@ -package foo - -class A - -fun checkCastNullableToNotNull(): Boolean { - val a = null - try { - val s = a as A - } - catch (e: Exception) { - return true - } - return false -} - -fun checkCastNotNullToNotNull(): Boolean { - val a = A() - var s = a as A - return s == a -} - -fun box(): String { - if (!checkCastNullableToNotNull()) return "Failed when try cast Nullable to NotNull" - if (!checkCastNotNullToNotNull()) return "Failed when try cast NotNull to NotNull" - return "OK" -} diff --git a/js/js.translator/testData/expression/cast/cases/castToNullable.kt b/js/js.translator/testData/expression/cast/cases/castToNullable.kt new file mode 100644 index 00000000000..00658e716ac --- /dev/null +++ b/js/js.translator/testData/expression/cast/cases/castToNullable.kt @@ -0,0 +1,18 @@ +package foo + +interface A + +class AImpl : A {} + +fun test(x: Any?): A? = x as A? + +fun box(): String { + var a: A? = AImpl() + assertEquals(a, test(a), "a = AImpl()") + a = object : A {} + assertEquals(a, test(a), "a = object : A{}") + assertEquals(null, test(null), "test(null)") + failsClassCast("test(object{})") { test(object{}) } + + return "OK" +} \ No newline at end of file diff --git a/js/js.translator/testData/expression/cast/cases/castToNullableType.kt b/js/js.translator/testData/expression/cast/cases/castToNullableType.kt deleted file mode 100644 index 839f1e18a3e..00000000000 --- a/js/js.translator/testData/expression/cast/cases/castToNullableType.kt +++ /dev/null @@ -1,21 +0,0 @@ -package foo - -class A - -fun Any.cast() = this!! as T - -fun box(): String { - val a = null - val s = a as A? - if (s != null) return "Failed when try cast Nullable with null value to Nullable" - - val b: A? = A() - val n = b as A? - if (n != b) return "Failed when try cast Nullable with not null value to Nullable" - - val c = A() - val m = c as A? - if (m != c) return "Failed when try cast NotNull to Nullable" - - return "OK" -} diff --git a/js/js.translator/testData/expression/cast/cases/reifiedToNotNull.kt b/js/js.translator/testData/expression/cast/cases/reifiedToNotNull.kt new file mode 100644 index 00000000000..01c41b85866 --- /dev/null +++ b/js/js.translator/testData/expression/cast/cases/reifiedToNotNull.kt @@ -0,0 +1,21 @@ +package foo + +// CHECK_NOT_CALLED: test + +trait A + +class AImpl: A + +inline +fun test(x: Any?): T = x as T + +fun box(): String { + var a: A = AImpl() + assertEquals(a, test(a), "a = AImpl()") + a = object : A {} + assertEquals(a, test(a), "a = object : A{}") + failsClassCast("test(null)") { test(null) } + failsClassCast("test(object{})") { test(object{}) } + + return "OK" +} \ No newline at end of file diff --git a/js/js.translator/testData/expression/cast/cases/reifiedToNullable.kt b/js/js.translator/testData/expression/cast/cases/reifiedToNullable.kt new file mode 100644 index 00000000000..f4e2d7d44b5 --- /dev/null +++ b/js/js.translator/testData/expression/cast/cases/reifiedToNullable.kt @@ -0,0 +1,19 @@ +package foo + +interface A + +class AImpl : A {} + +inline +fun test(x: Any?): T = x as T + +fun box(): String { + var a: A? = AImpl() + assertEquals(a, test(a), "a = AImpl()") + a = object : A {} + assertEquals(a, test(a), "a = object : A{}") + assertEquals(null, test(null), "test(null)") + failsClassCast("test(object{})") { test(object{}) } + + return "OK" +} \ No newline at end of file diff --git a/js/js.translator/testData/kotlin_lib.js b/js/js.translator/testData/kotlin_lib.js index eb01ad02b4b..c8db5a02cef 100644 --- a/js/js.translator/testData/kotlin_lib.js +++ b/js/js.translator/testData/kotlin_lib.js @@ -199,6 +199,7 @@ Kotlin.IllegalStateException = createClassNowWithMessage(Kotlin.RuntimeException); Kotlin.UnsupportedOperationException = createClassNowWithMessage(Kotlin.RuntimeException); Kotlin.IndexOutOfBoundsException = createClassNowWithMessage(Kotlin.RuntimeException); + Kotlin.ClassCastException = createClassNowWithMessage(Kotlin.RuntimeException); Kotlin.IOException = createClassNowWithMessage(Kotlin.Exception); Kotlin.AssertionError = createClassNowWithMessage(Kotlin.Error); @@ -206,6 +207,10 @@ throw new Kotlin.NullPointerException(message); }; + Kotlin.throwCCE = function () { + throw new Kotlin.ClassCastException("Illegal cast"); + }; + function throwAbstractFunctionInvocationError(funName) { return function () { var message;