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 417020b1e9d..578c5a82c2c 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 @@ -61,5 +61,6 @@ enum class TypeCheck { TYPEOF, INSTANCEOF, OR_NULL, - IS_ANY + IS_ANY, + AND_PREDICATE } 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 be58b03b2df..77920944d5d 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 @@ -59,6 +59,12 @@ public class CastTestGenerated extends AbstractCastTest { doTest(fileName); } + @TestMetadata("castToGenericTypeWithMultipleUpperBounds.kt") + public void testCastToGenericTypeWithMultipleUpperBounds() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/expression/cast/cases/castToGenericTypeWithMultipleUpperBounds.kt"); + doTest(fileName); + } + @TestMetadata("castToGenericTypeWithUpperBound.kt") public void testCastToGenericTypeWithUpperBound() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/expression/cast/cases/castToGenericTypeWithUpperBound.kt"); 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 bef3f02cbc1..866ddcaa568 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 @@ -56,23 +56,25 @@ public class DirectiveTestUtils { private static final DirectiveHandler FUNCTION_CALLED_IN_SCOPE = new DirectiveHandler("CHECK_CALLED_IN_SCOPE") { @Override void processEntry(@NotNull JsNode ast, @NotNull ArgumentsHelper arguments) throws Exception { - checkCalledInScope(ast, arguments.getNamedArgument("function"), arguments.getNamedArgument("scope")); + // Be more restrictive, check qualified match by default + checkCalledInScope(ast, arguments.getNamedArgument("function"), arguments.getNamedArgument("scope"), + parseBooleanArgument(arguments, "qualified", true)); } }; private static final DirectiveHandler FUNCTION_NOT_CALLED_IN_SCOPE = new DirectiveHandler("CHECK_NOT_CALLED_IN_SCOPE") { @Override void processEntry(@NotNull JsNode ast, @NotNull ArgumentsHelper arguments) throws Exception { - checkNotCalledInScope(ast, arguments.getNamedArgument("function"), arguments.getNamedArgument("scope")); + // Be more restrictive, check unqualified match by default + checkNotCalledInScope(ast, arguments.getNamedArgument("function"), arguments.getNamedArgument("scope"), + parseBooleanArgument(arguments, "qualified", false)); } }; - private static final DirectiveHandler METHOD_NOT_CALLED_IN_SCOPE = new DirectiveHandler("CHECK_METHOD_NOT_CALLED_IN_SCOPE") { - @Override - void processEntry(@NotNull JsNode ast, @NotNull ArgumentsHelper arguments) throws Exception { - checkMethodNotCalledInScope(ast, arguments.getNamedArgument("function"), arguments.getNamedArgument("scope")); - } - }; + private static boolean parseBooleanArgument(@NotNull ArgumentsHelper arguments, @NotNull String name, boolean defaultValue) { + String value = arguments.findNamedArgument(name); + return value != null ? Boolean.parseBoolean(value) : defaultValue; + } private static final DirectiveHandler FUNCTIONS_HAVE_SAME_LINES = new DirectiveHandler("CHECK_FUNCTIONS_HAVE_SAME_LINES") { @Override @@ -196,7 +198,6 @@ public class DirectiveTestUtils { FUNCTION_NOT_CALLED, FUNCTION_CALLED_IN_SCOPE, FUNCTION_NOT_CALLED_IN_SCOPE, - METHOD_NOT_CALLED_IN_SCOPE, FUNCTIONS_HAVE_SAME_LINES, COUNT_LABELS, COUNT_VARS, @@ -234,50 +235,38 @@ public class DirectiveTestUtils { public static void checkCalledInScope( @NotNull JsNode node, @NotNull String functionName, - @NotNull String scopeFunctionName + @NotNull String scopeFunctionName, + boolean checkQualifier ) throws Exception { String errorMessage = functionName + " is not called inside " + scopeFunctionName; - assertFalse(errorMessage, isCalledInScope(node, functionName, scopeFunctionName)); + assertFalse(errorMessage, isCalledInScope(node, functionName, scopeFunctionName, checkQualifier)); } public static void checkNotCalledInScope( @NotNull JsNode node, @NotNull String functionName, - @NotNull String scopeFunctionName + @NotNull String scopeFunctionName, + boolean checkQualifier ) throws Exception { String errorMessage = functionName + " is called inside " + scopeFunctionName; - assertTrue(errorMessage, isCalledInScope(node, functionName, scopeFunctionName)); - } - - private static void checkMethodNotCalledInScope( - @NotNull JsNode node, - @NotNull String functionName, - @NotNull String scopeFunctionName - ) throws Exception { - String errorMessage = functionName + " is called inside " + scopeFunctionName; - assertTrue(errorMessage, isMethodCalledInScope(node, functionName, scopeFunctionName)); + assertTrue(errorMessage, isCalledInScope(node, functionName, scopeFunctionName, checkQualifier)); } private static boolean isCalledInScope( @NotNull JsNode node, @NotNull String functionName, - @NotNull String scopeFunctionName + @NotNull String scopeFunctionName, + boolean checkQualifier ) throws Exception { JsNode scope = AstSearchUtil.getFunction(node, scopeFunctionName); CallCounter counter = CallCounter.countCalls(scope); - return counter.getQualifiedCallsCount(functionName) == 0; - } - - private static boolean isMethodCalledInScope( - @NotNull JsNode node, - @NotNull String functionName, - @NotNull String scopeFunctionName - ) throws Exception { - JsNode scope = AstSearchUtil.getFunction(node, scopeFunctionName); - - CallCounter counter = CallCounter.countCalls(scope); - return counter.getUnqualifiedCallsCount(functionName) == 0; + if (checkQualifier) { + return counter.getQualifiedCallsCount(functionName) == 0; + } + else { + return counter.getUnqualifiedCallsCount(functionName) == 0; + } } private abstract static class DirectiveHandler { 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 bf4bbdf899a..8750c1e8846 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 @@ -34,6 +34,8 @@ import org.jetbrains.kotlin.name.FqNameUnsafe; import org.jetbrains.kotlin.resolve.DescriptorUtils; import java.util.Arrays; +import java.util.Collections; +import java.util.List; import static com.google.dart.compiler.backend.js.ast.JsScopesKt.JsObjectScope; import static org.jetbrains.kotlin.js.translate.utils.JsAstUtils.fqnWithoutSideEffects; @@ -403,9 +405,14 @@ public final class Namer { return invokeFunctionAndSetTypeCheckMetadata("orNull", callable, TypeCheck.OR_NULL); } + @NotNull + public JsExpression andPredicate(@NotNull JsExpression a, @NotNull JsExpression b) { + return invokeFunctionAndSetTypeCheckMetadata("andPredicate", Arrays.asList(a, b), TypeCheck.AND_PREDICATE); + } + @NotNull public JsExpression isAny() { - return invokeFunctionAndSetTypeCheckMetadata("isAny", null, TypeCheck.IS_ANY); + return invokeFunctionAndSetTypeCheckMetadata("isAny", Collections.emptyList(), TypeCheck.IS_ANY); } @NotNull @@ -423,13 +430,19 @@ public final class Namer { @NotNull String functionName, @Nullable JsExpression argument, @NotNull TypeCheck metadata + ) { + List arguments = argument != null ? Collections.singletonList(argument) : Collections.emptyList(); + return invokeFunctionAndSetTypeCheckMetadata(functionName, arguments, metadata); + } + + @NotNull + private JsExpression invokeFunctionAndSetTypeCheckMetadata( + @NotNull String functionName, + @NotNull List arguments, + @NotNull TypeCheck metadata ) { JsInvocation invocation = new JsInvocation(kotlin(functionName)); - - if (argument != null) { - invocation.getArguments().add(argument); - } - + invocation.getArguments().addAll(arguments); MetadataProperties.setTypeCheck(invocation, metadata); MetadataProperties.setSideEffects(invocation, false); return invocation; 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 d8ef5692998..bff340275fa 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 @@ -41,7 +41,6 @@ import org.jetbrains.kotlin.psi.KtTypeReference; import org.jetbrains.kotlin.resolve.DescriptorUtils; import org.jetbrains.kotlin.types.DynamicTypesKt; import org.jetbrains.kotlin.types.KotlinType; -import org.jetbrains.kotlin.types.TypeIntersector; import org.jetbrains.kotlin.types.typeUtil.TypeUtilsKt; import static org.jetbrains.kotlin.builtins.FunctionTypesKt.isFunctionTypeOrSubtype; @@ -157,7 +156,13 @@ public final class PatternTranslator extends AbstractTranslator { return getIsTypeCheckCallableForReifiedType(typeParameterDescriptor); } - return doGetIsTypeCheckCallable(TypeIntersector.getUpperBoundsAsType(typeParameterDescriptor)); + JsExpression result = null; + for (KotlinType upperBound : typeParameterDescriptor.getUpperBounds()) { + JsExpression next = doGetIsTypeCheckCallable(upperBound); + result = result != null ? namer().andPredicate(result, next) : next; + } + assert result != null : "KotlinType is expected to return at least one upper bound: " + type; + return result; } JsNameRef typeName = getClassNameReference(type); 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 61b6355666b..fcc85e75688 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 @@ -35,7 +35,7 @@ private class TypeCheckRewritingVisitor(private val context: TranslationContext) override fun visit(x: JsFunction, ctx: JsContext<*>): Boolean { scopes.push(x.scope) - localVars.push(IdentitySet()) + localVars.push(IdentitySet().apply { this += x.parameters.map { it.name } }) return super.visit(x, ctx) } @@ -50,19 +50,14 @@ private class TypeCheckRewritingVisitor(private val context: TranslationContext) super.endVisit(x, ctx) } - override fun endVisit(x: JsConditional, ctx: JsContext) { - val test = x.testExpression - - } - override fun visit(x: JsInvocation, ctx: JsContext): Boolean { // callee(calleeArgument)(argument) val callee = x.qualifier as? JsInvocation - val calleeArgument = callee?.arguments?.firstOrNull() + val calleeArguments = callee?.arguments val argument = x.arguments.firstOrNull() - if (callee != null && argument != null) { - val replacement = getReplacement(callee, calleeArgument, argument) + if (callee != null && argument != null && calleeArguments != null) { + val replacement = getReplacement(callee, calleeArguments, argument) if (replacement != null) { ctx.replaceMe(accept(replacement)) @@ -73,64 +68,100 @@ private class TypeCheckRewritingVisitor(private val context: TranslationContext) return true } - private fun getReplacement(callee: JsInvocation, calleeArgument: JsExpression?, argument: JsExpression): JsExpression? { - if (calleeArgument == null) { - // `Kotlin.isAny()(argument)` -> `argument != null` - if (callee.typeCheck == TypeCheck.IS_ANY) { - return TranslationUtils.isNotNullCheck(argument) + private fun getReplacement(callee: JsInvocation, calleeArguments: List, argument: JsExpression): JsExpression? { + val typeCheck = callee.typeCheck + return when (typeCheck) { + TypeCheck.IS_ANY -> { + // `Kotlin.isAny()(argument)` -> `argument != null` + if (calleeArguments.isEmpty()) TranslationUtils.isNotNullCheck(argument) else null } - return null - } - - // `Kotlin.isTypeOf(calleeArgument)(argument)` -> `typeOf argument === calleeArgument` - if (callee.typeCheck == TypeCheck.TYPEOF) { - return typeOfIs(argument, calleeArgument as JsStringLiteral) - } - - // `Kotlin.isInstanceOf(calleeArgument)(argument)` -> `argument instanceof calleeArgument` - if (callee.typeCheck == TypeCheck.INSTANCEOF) { - return context.namer().isInstanceOf(argument, calleeArgument) - } - - // `Kotlin.orNull(calleeArgument)(argument)` -> `(tmp = argument) == null || calleeArgument(tmp)` - if (callee.typeCheck == TypeCheck.OR_NULL) { - if (calleeArgument is JsInvocation && calleeArgument.typeCheck == TypeCheck.OR_NULL) { - return JsInvocation(calleeArgument, argument) + TypeCheck.TYPEOF -> { + // `Kotlin.isTypeOf(calleeArgument)(argument)` -> `typeOf argument === calleeArgument` + if (calleeArguments.size == 1) typeOfIs(argument, calleeArguments[0] as JsStringLiteral) else null } - var nullCheckTarget = argument - var nextCheckTarget = argument - - if (argument.isAssignmentToLocalVar) { - // `Kotlin.orNull(Kotlin.isInstance(SomeType))(localVar=someExpr)` -> `(localVar=someExpr) != null || Kotlin.isInstance(SomeType)(localVar)` - val localVar = (argument as JsBinaryOperation).getArg1() - nextCheckTarget = localVar - } - else if (!argument.isLocalVar) { - val currentScope = scopes.peek() - val tmp = currentScope.declareTemporary() - val statementContext = lastStatementLevelContext - statementContext.addPrevious(newVar(tmp, null)) - nullCheckTarget = assignment(tmp.makeRef(), argument) - nextCheckTarget = tmp.makeRef() + TypeCheck.INSTANCEOF -> { + // `Kotlin.isInstanceOf(calleeArgument)(argument)` -> `argument instanceof calleeArgument` + if (calleeArguments.size == 1) context.namer().isInstanceOf(argument, calleeArguments[0]) else null } - val isNull = TranslationUtils.isNullCheck(nullCheckTarget) - return or(isNull, JsInvocation(calleeArgument, nextCheckTarget)) + TypeCheck.OR_NULL -> { + // `Kotlin.orNull(calleeArgument)(argument)` -> `(tmp = argument) == null || calleeArgument(tmp)` + if (calleeArguments.size == 1) getReplacementForOrNull(argument, calleeArguments[0]) else null + } + + TypeCheck.AND_PREDICATE -> { + // `Kotlin.andPredicate(p1, p2)(argument)` -> `p1(tmp = argument) && p2(tmp)` + if (calleeArguments.size == 2) { + getReplacementForAndPredicate(argument, calleeArguments[0], calleeArguments[1]) + } + else { + null + } + } + + null -> null } - - return null } - private val JsExpression.isLocalVar: Boolean - get() { - if (localVars.empty() || this !is JsNameRef) return false - - val name = this.getName() - return name != null && localVars.peek().contains(name) + private fun getReplacementForOrNull(argument: JsExpression, calleeArgument: JsExpression): JsExpression { + if (calleeArgument is JsInvocation && calleeArgument.typeCheck == TypeCheck.OR_NULL) { + return JsInvocation(calleeArgument, argument) } + var nullCheckTarget = argument + var nextCheckTarget = argument + + if (argument.isAssignmentToLocalVar) { + // `Kotlin.orNull(Kotlin.isInstance(SomeType))(localVar=someExpr)` -> + // `(localVar=someExpr) != null || Kotlin.isInstance(SomeType)(localVar)` + val localVar = (argument as JsBinaryOperation).arg1 + nextCheckTarget = localVar + } + else if (argument.needsAlias) { + val currentScope = scopes.peek() + val tmp = currentScope.declareTemporary() + val statementContext = lastStatementLevelContext + statementContext.addPrevious(newVar(tmp, null)) + nullCheckTarget = assignment(tmp.makeRef(), argument) + nextCheckTarget = tmp.makeRef() + } + + val isNull = TranslationUtils.isNullCheck(nullCheckTarget) + return or(isNull, JsInvocation(calleeArgument, nextCheckTarget)) + } + + private fun getReplacementForAndPredicate(argument: JsExpression, p1: JsExpression, p2: JsExpression): JsExpression { + val (arg1, arg2) = if (!argument.needsAlias) { + Pair(argument, argument) + } + else if (argument.isAssignmentToLocalVar) { + Pair(argument, JsAstUtils.decomposeAssignment(argument)!!.first) + } + else { + val currentScope = scopes.peek() + val tmp = currentScope.declareTemporary() + val statementContext = lastStatementLevelContext + statementContext.addPrevious(newVar(tmp, null)) + Pair(assignment(tmp.makeRef(), argument), tmp.makeRef()) + } + + val first = accept(JsInvocation(p1, arg1) as JsExpression) + val second = accept(JsInvocation(p2, arg2) as JsExpression) + return JsAstUtils.and(first, second) + } + + private val JsExpression.needsAlias: Boolean + get() = when (this) { + is JsStringLiteral -> false + else -> !isLocalVar + } + + private val JsExpression.isLocalVar: Boolean + get() = localVars.isNotEmpty() && this is JsNameRef && name.let { it != null && it in localVars.peek() } + private val JsExpression.isAssignmentToLocalVar: Boolean - get() = this is JsBinaryOperation && getOperator() == JsBinaryOperator.ASG + get() = localVars.isNotEmpty() && + JsAstUtils.decomposeAssignmentToVariable(this).let { it != null && it.first in localVars.peek() } } diff --git a/js/js.translator/testData/expression/cast/cases/castToGenericTypeWithMultipleUpperBounds.kt b/js/js.translator/testData/expression/cast/cases/castToGenericTypeWithMultipleUpperBounds.kt new file mode 100644 index 00000000000..2c6f02250a9 --- /dev/null +++ b/js/js.translator/testData/expression/cast/cases/castToGenericTypeWithMultipleUpperBounds.kt @@ -0,0 +1,59 @@ +package foo + +open class A() + +interface X + +interface Y + +class B() : A(), X, Y { + override fun toString() = "B" +} + +class C() : A(), X, Y { + override fun toString() = "C" +} + +class D() : A() { + override fun toString() = "D" +} + +class E() : X { + override fun toString() = "E" +} + +class F() : A(), Y { + override fun toString() = "E" +} + +fun test(a: Any): String where T : A, T : X, T : Y { + return (try { + a as T + } + catch (e: Exception) { + "error" + }).toString() +} + +fun box(): String { + val b = B() + val c = C() + val d = D() + val e = E() + val f = F() + + assertEquals("B", test(b)) + assertEquals("B", test(b)) + + assertEquals("C", test(c)) + assertEquals("C", test(c)) + + assertEquals("error", test(d)) + assertEquals("error", test(d)) + assertEquals("error", test(e)) + assertEquals("error", test(e)) + assertEquals("error", test(f)) + assertEquals("error", test(f)) + + return "OK" +} \ No newline at end of file diff --git a/js/js.translator/testData/expression/cast/cases/noRuntimeTypeCheck.kt b/js/js.translator/testData/expression/cast/cases/noRuntimeTypeCheck.kt index 35d9a7160e2..d9b31bab488 100644 --- a/js/js.translator/testData/expression/cast/cases/noRuntimeTypeCheck.kt +++ b/js/js.translator/testData/expression/cast/cases/noRuntimeTypeCheck.kt @@ -1,6 +1,6 @@ package foo -// CHECK_METHOD_NOT_CALLED_IN_SCOPE: scope=box function=isType +// CHECK_NOT_CALLED_IN_SCOPE: scope=box function=isType open class A() diff --git a/js/js.translator/testData/kotlin_lib_ecma5.js b/js/js.translator/testData/kotlin_lib_ecma5.js index 436a5abdad5..43cc8c68b73 100644 --- a/js/js.translator/testData/kotlin_lib_ecma5.js +++ b/js/js.translator/testData/kotlin_lib_ecma5.js @@ -560,6 +560,12 @@ var Kotlin = {}; return object != null; } }; + + Kotlin.andPredicate = function (a, b) { + return function (object) { + return a(object) && b(object); + } + }; Kotlin.kotlinModuleMetadata = function (abiVersion, moduleName, data) { }; diff --git a/js/js.translator/testData/reified/cases/isTNullable.kt b/js/js.translator/testData/reified/cases/isTNullable.kt index 79874c80b96..e492ff6a258 100644 --- a/js/js.translator/testData/reified/cases/isTNullable.kt +++ b/js/js.translator/testData/reified/cases/isTNullable.kt @@ -1,7 +1,7 @@ package foo // CHECK_NOT_CALLED: isTypeOfOrNull -// CHECK_NULLS_COUNT: function=box count=8 +// CHECK_NULLS_COUNT: function=box count=10 inline fun Any?.isTypeOfOrNull() = this is T?