JS/RTTI: if it's known that T <: X & Y, where T is non-reified type variable, for each a is T check that a is X && a is Y
This commit is contained in:
+2
-1
@@ -61,5 +61,6 @@ enum class TypeCheck {
|
||||
TYPEOF,
|
||||
INSTANCEOF,
|
||||
OR_NULL,
|
||||
IS_ANY
|
||||
IS_ANY,
|
||||
AND_PREDICATE
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.<JsExpression>emptyList(), TypeCheck.IS_ANY);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -423,13 +430,19 @@ public final class Namer {
|
||||
@NotNull String functionName,
|
||||
@Nullable JsExpression argument,
|
||||
@NotNull TypeCheck metadata
|
||||
) {
|
||||
List<JsExpression> arguments = argument != null ? Collections.singletonList(argument) : Collections.<JsExpression>emptyList();
|
||||
return invokeFunctionAndSetTypeCheckMetadata(functionName, arguments, metadata);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private JsExpression invokeFunctionAndSetTypeCheckMetadata(
|
||||
@NotNull String functionName,
|
||||
@NotNull List<JsExpression> 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;
|
||||
|
||||
+7
-2
@@ -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);
|
||||
|
||||
@@ -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<JsName>().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<JsNode>) {
|
||||
val test = x.testExpression
|
||||
|
||||
}
|
||||
|
||||
override fun visit(x: JsInvocation, ctx: JsContext<JsNode>): 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<JsExpression>, 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() }
|
||||
}
|
||||
|
||||
Vendored
+59
@@ -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 <T> 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>(b))
|
||||
assertEquals("B", test<C>(b))
|
||||
|
||||
assertEquals("C", test<B>(c))
|
||||
assertEquals("C", test<C>(c))
|
||||
|
||||
assertEquals("error", test<B>(d))
|
||||
assertEquals("error", test<C>(d))
|
||||
assertEquals("error", test<B>(e))
|
||||
assertEquals("error", test<C>(e))
|
||||
assertEquals("error", test<B>(f))
|
||||
assertEquals("error", test<C>(f))
|
||||
|
||||
return "OK"
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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) {
|
||||
};
|
||||
|
||||
+1
-1
@@ -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 <reified T> Any?.isTypeOfOrNull() = this is T?
|
||||
|
||||
Reference in New Issue
Block a user