From acf7fcaebf0fa7b991cd58734bf189da27978ac1 Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Wed, 28 Sep 2016 19:32:25 +0300 Subject: [PATCH] JS: prohibit is checks against native interfaces. Warn about casts to native interfaces. Fix #KT-14037, fix #KT-14038 --- .../calls/checkers/RttiExpressionChecker.kt | 40 ++++++++++++++++++ .../BasicExpressionTypingVisitor.java | 17 ++++++-- .../ExpressionTypingComponents.java | 7 ++++ .../PatternMatchingTypingVisitor.kt | 32 +++++++++++++- .../native/rtti/castToNativeInterface.kt | 5 +++ .../native/rtti/castToNativeInterface.txt | 9 ++++ .../native/rtti/checkForNativeInterface.kt | 5 +++ .../native/rtti/checkForNativeInterface.txt | 9 ++++ .../native/rtti/whenIsNativeInterface.kt | 9 ++++ .../native/rtti/whenIsNativeInterface.txt | 15 +++++++ .../DiagnosticsTestWithJsStdLibGenerated.java | 27 ++++++++++++ .../js/resolve/JsPlatformConfigurator.kt | 2 + .../diagnostics/DefaultErrorMessagesJs.kt | 5 ++- .../js/resolve/diagnostics/ErrorsJs.java | 2 + .../diagnostics/JsNativeRttiChecker.kt | 42 +++++++++++++++++++ .../js/translate/utils/AnnotationsUtils.java | 4 ++ .../js/test/semantics/BoxJsTestGenerated.java | 18 ++++++++ .../expression/PatternTranslator.java | 21 ++++++++-- .../box/native/castToNativeClassChecked.kt | 29 +++++++++++++ .../box/native/castToNativeInterface.kt | 20 +++++++++ ...castToTypeParamBoundedByNativeInterface.kt | 32 ++++++++++++++ 21 files changed, 341 insertions(+), 9 deletions(-) create mode 100644 compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/checkers/RttiExpressionChecker.kt create mode 100644 compiler/testData/diagnostics/testsWithJsStdLib/native/rtti/castToNativeInterface.kt create mode 100644 compiler/testData/diagnostics/testsWithJsStdLib/native/rtti/castToNativeInterface.txt create mode 100644 compiler/testData/diagnostics/testsWithJsStdLib/native/rtti/checkForNativeInterface.kt create mode 100644 compiler/testData/diagnostics/testsWithJsStdLib/native/rtti/checkForNativeInterface.txt create mode 100644 compiler/testData/diagnostics/testsWithJsStdLib/native/rtti/whenIsNativeInterface.kt create mode 100644 compiler/testData/diagnostics/testsWithJsStdLib/native/rtti/whenIsNativeInterface.txt create mode 100644 js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/JsNativeRttiChecker.kt create mode 100644 js/js.translator/testData/box/native/castToNativeClassChecked.kt create mode 100644 js/js.translator/testData/box/native/castToNativeInterface.kt create mode 100644 js/js.translator/testData/box/native/castToTypeParamBoundedByNativeInterface.kt diff --git a/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/checkers/RttiExpressionChecker.kt b/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/checkers/RttiExpressionChecker.kt new file mode 100644 index 00000000000..6809dd0176e --- /dev/null +++ b/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/checkers/RttiExpressionChecker.kt @@ -0,0 +1,40 @@ +/* + * 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 org.jetbrains.kotlin.resolve.calls.checkers + +import com.intellij.psi.PsiElement +import org.jetbrains.kotlin.psi.KtExpression +import org.jetbrains.kotlin.resolve.BindingTrace +import org.jetbrains.kotlin.types.KotlinType + +interface RttiExpressionChecker { + fun check(rttiInformation: RttiExpressionInformation, reportOn: PsiElement, trace: BindingTrace) +} + +enum class RttiOperation { + IS, + NOT_IS, + AS, + SAFE_AS +} + +class RttiExpressionInformation( + val subject: KtExpression, + val sourceType: KotlinType?, + val targetType: KotlinType?, + val operation: RttiOperation +) \ No newline at end of file diff --git a/compiler/frontend/src/org/jetbrains/kotlin/types/expressions/BasicExpressionTypingVisitor.java b/compiler/frontend/src/org/jetbrains/kotlin/types/expressions/BasicExpressionTypingVisitor.java index 4759d7f08d4..49c7344f158 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/types/expressions/BasicExpressionTypingVisitor.java +++ b/compiler/frontend/src/org/jetbrains/kotlin/types/expressions/BasicExpressionTypingVisitor.java @@ -42,8 +42,7 @@ import org.jetbrains.kotlin.resolve.bindingContextUtil.BindingContextUtilsKt; import org.jetbrains.kotlin.resolve.calls.ArgumentTypeResolver; import org.jetbrains.kotlin.resolve.calls.CallExpressionResolver; import org.jetbrains.kotlin.resolve.calls.callUtil.CallUtilKt; -import org.jetbrains.kotlin.resolve.calls.checkers.CallChecker; -import org.jetbrains.kotlin.resolve.calls.checkers.CallCheckerContext; +import org.jetbrains.kotlin.resolve.calls.checkers.*; import org.jetbrains.kotlin.resolve.calls.model.DataFlowInfoForArgumentsImpl; import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall; import org.jetbrains.kotlin.resolve.calls.model.ResolvedCallImpl; @@ -315,7 +314,19 @@ public class BasicExpressionTypingVisitor extends ExpressionTypingVisitor { } KotlinType result = operationType == AS_SAFE ? TypeUtils.makeNullable(targetType) : targetType; - return components.dataFlowAnalyzer.checkType(typeInfo.replaceType(result), expression, context); + KotlinTypeInfo resultTypeInfo = components.dataFlowAnalyzer.checkType(typeInfo.replaceType(result), expression, context); + + RttiExpressionInformation rttiInformation = new RttiExpressionInformation( + expression.getLeft(), + subjectType, + result, + operationType == AS_SAFE ? RttiOperation.SAFE_AS : RttiOperation.AS + ); + for (RttiExpressionChecker checker : components.rttiExpressionCheckers) { + checker.check(rttiInformation, expression, context.trace); + } + + return resultTypeInfo; } private void checkBinaryWithTypeRHS( diff --git a/compiler/frontend/src/org/jetbrains/kotlin/types/expressions/ExpressionTypingComponents.java b/compiler/frontend/src/org/jetbrains/kotlin/types/expressions/ExpressionTypingComponents.java index 80097130348..5e64c3f5289 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/types/expressions/ExpressionTypingComponents.java +++ b/compiler/frontend/src/org/jetbrains/kotlin/types/expressions/ExpressionTypingComponents.java @@ -26,6 +26,7 @@ import org.jetbrains.kotlin.resolve.*; import org.jetbrains.kotlin.resolve.calls.CallExpressionResolver; import org.jetbrains.kotlin.resolve.calls.CallResolver; import org.jetbrains.kotlin.resolve.calls.checkers.CallChecker; +import org.jetbrains.kotlin.resolve.calls.checkers.RttiExpressionChecker; import org.jetbrains.kotlin.resolve.constants.evaluate.ConstantExpressionEvaluator; import javax.inject.Inject; @@ -58,6 +59,7 @@ public class ExpressionTypingComponents { /*package*/ LookupTracker lookupTracker; /*package*/ OverloadChecker overloadChecker; /*package*/ LanguageVersionSettings languageVersionSettings; + /*package*/ Iterable rttiExpressionCheckers; @Inject public void setGlobalContext(@NotNull GlobalContext globalContext) { @@ -193,4 +195,9 @@ public class ExpressionTypingComponents { public void setLanguageVersionSettings(@NotNull LanguageVersionSettings languageVersionSettings) { this.languageVersionSettings = languageVersionSettings; } + + @Inject + public void setRttiExpressionCheckers(@NotNull Iterable rttiExpressionCheckers) { + this.rttiExpressionCheckers = rttiExpressionCheckers; + } } diff --git a/compiler/frontend/src/org/jetbrains/kotlin/types/expressions/PatternMatchingTypingVisitor.kt b/compiler/frontend/src/org/jetbrains/kotlin/types/expressions/PatternMatchingTypingVisitor.kt index 7f215eb5ca1..214037b92af 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/types/expressions/PatternMatchingTypingVisitor.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/types/expressions/PatternMatchingTypingVisitor.kt @@ -26,6 +26,8 @@ import org.jetbrains.kotlin.diagnostics.Errors import org.jetbrains.kotlin.diagnostics.Errors.* import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.resolve.* +import org.jetbrains.kotlin.resolve.calls.checkers.RttiExpressionInformation +import org.jetbrains.kotlin.resolve.calls.checkers.RttiOperation import org.jetbrains.kotlin.resolve.calls.context.ContextDependency.INDEPENDENT import org.jetbrains.kotlin.resolve.calls.smartcasts.DataFlowInfo import org.jetbrains.kotlin.resolve.calls.smartcasts.DataFlowValue @@ -53,7 +55,23 @@ class PatternMatchingTypingVisitor internal constructor(facade: ExpressionTyping val newDataFlowInfo = conditionInfo.and(typeInfo.dataFlowInfo) context.trace.record(BindingContext.DATAFLOW_INFO_AFTER_CONDITION, expression, newDataFlowInfo) } - return components.dataFlowAnalyzer.checkType(typeInfo.replaceType(components.builtIns.booleanType), expression, contextWithExpectedType) + + val resultTypeInfo = components.dataFlowAnalyzer.checkType(typeInfo.replaceType(components.builtIns.booleanType), expression, contextWithExpectedType) + + if (typeReference != null) { + val rhsType = context.trace[BindingContext.TYPE, typeReference] + val rttiInformation = RttiExpressionInformation( + subject = leftHandSide, + sourceType = knownType, + targetType = rhsType, + operation = RttiOperation.IS + ) + components.rttiExpressionCheckers.forEach { + it.check(rttiInformation, expression, context.trace) + } + } + + return resultTypeInfo } override fun visitWhenExpression(expression: KtWhenExpression, context: ExpressionTypingContext) = @@ -314,6 +332,18 @@ class PatternMatchingTypingVisitor internal constructor(facade: ExpressionTyping else { newDataFlowInfo = result } + val rhsType = context.trace[BindingContext.TYPE, typeReference] + if (subjectExpression != null) { + val rttiInformation = RttiExpressionInformation( + subject = subjectExpression, + sourceType = subjectType, + targetType = rhsType, + operation = if (condition.isNegated) RttiOperation.NOT_IS else RttiOperation.IS + ) + components.rttiExpressionCheckers.forEach { + it.check(rttiInformation, condition, context.trace) + } + } } } diff --git a/compiler/testData/diagnostics/testsWithJsStdLib/native/rtti/castToNativeInterface.kt b/compiler/testData/diagnostics/testsWithJsStdLib/native/rtti/castToNativeInterface.kt new file mode 100644 index 00000000000..0a8a4cdc333 --- /dev/null +++ b/compiler/testData/diagnostics/testsWithJsStdLib/native/rtti/castToNativeInterface.kt @@ -0,0 +1,5 @@ +@native interface I + +fun box(a: Any, b: Any): Pair { + return Pair(a as I, b as? I) +} \ No newline at end of file diff --git a/compiler/testData/diagnostics/testsWithJsStdLib/native/rtti/castToNativeInterface.txt b/compiler/testData/diagnostics/testsWithJsStdLib/native/rtti/castToNativeInterface.txt new file mode 100644 index 00000000000..af836f5c2b3 --- /dev/null +++ b/compiler/testData/diagnostics/testsWithJsStdLib/native/rtti/castToNativeInterface.txt @@ -0,0 +1,9 @@ +package + +public fun box(/*0*/ a: kotlin.Any, /*1*/ b: kotlin.Any): kotlin.Pair + +@kotlin.js.native() public interface I { + public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int + public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String +} diff --git a/compiler/testData/diagnostics/testsWithJsStdLib/native/rtti/checkForNativeInterface.kt b/compiler/testData/diagnostics/testsWithJsStdLib/native/rtti/checkForNativeInterface.kt new file mode 100644 index 00000000000..83ed4ff9702 --- /dev/null +++ b/compiler/testData/diagnostics/testsWithJsStdLib/native/rtti/checkForNativeInterface.kt @@ -0,0 +1,5 @@ +@native interface I + +fun box(a: Any, b: Any): Boolean { + return a is I && b !is I +} \ No newline at end of file diff --git a/compiler/testData/diagnostics/testsWithJsStdLib/native/rtti/checkForNativeInterface.txt b/compiler/testData/diagnostics/testsWithJsStdLib/native/rtti/checkForNativeInterface.txt new file mode 100644 index 00000000000..dcf110c32d2 --- /dev/null +++ b/compiler/testData/diagnostics/testsWithJsStdLib/native/rtti/checkForNativeInterface.txt @@ -0,0 +1,9 @@ +package + +public fun box(/*0*/ a: kotlin.Any, /*1*/ b: kotlin.Any): kotlin.Boolean + +@kotlin.js.native() public interface I { + public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int + public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String +} diff --git a/compiler/testData/diagnostics/testsWithJsStdLib/native/rtti/whenIsNativeInterface.kt b/compiler/testData/diagnostics/testsWithJsStdLib/native/rtti/whenIsNativeInterface.kt new file mode 100644 index 00000000000..ca580321fbc --- /dev/null +++ b/compiler/testData/diagnostics/testsWithJsStdLib/native/rtti/whenIsNativeInterface.kt @@ -0,0 +1,9 @@ +@native interface I + +@native interface J + +fun box(a: Any) = when (a) { + is I -> 0 + !is J -> 1 + else -> 2 +} \ No newline at end of file diff --git a/compiler/testData/diagnostics/testsWithJsStdLib/native/rtti/whenIsNativeInterface.txt b/compiler/testData/diagnostics/testsWithJsStdLib/native/rtti/whenIsNativeInterface.txt new file mode 100644 index 00000000000..2790ab5f5a2 --- /dev/null +++ b/compiler/testData/diagnostics/testsWithJsStdLib/native/rtti/whenIsNativeInterface.txt @@ -0,0 +1,15 @@ +package + +public fun box(/*0*/ a: kotlin.Any): kotlin.Int + +@kotlin.js.native() public interface I { + public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int + public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String +} + +@kotlin.js.native() public interface J { + public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int + public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String +} diff --git a/compiler/tests/org/jetbrains/kotlin/checkers/DiagnosticsTestWithJsStdLibGenerated.java b/compiler/tests/org/jetbrains/kotlin/checkers/DiagnosticsTestWithJsStdLibGenerated.java index 8d8ed5a58f6..89b4c126f72 100644 --- a/compiler/tests/org/jetbrains/kotlin/checkers/DiagnosticsTestWithJsStdLibGenerated.java +++ b/compiler/tests/org/jetbrains/kotlin/checkers/DiagnosticsTestWithJsStdLibGenerated.java @@ -719,6 +719,33 @@ public class DiagnosticsTestWithJsStdLibGenerated extends AbstractDiagnosticsTes } } + @TestMetadata("compiler/testData/diagnostics/testsWithJsStdLib/native/rtti") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class Rtti extends AbstractDiagnosticsTestWithJsStdLib { + public void testAllFilesPresentInRtti() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("compiler/testData/diagnostics/testsWithJsStdLib/native/rtti"), Pattern.compile("^(.+)\\.kt$"), true); + } + + @TestMetadata("castToNativeInterface.kt") + public void testCastToNativeInterface() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/testsWithJsStdLib/native/rtti/castToNativeInterface.kt"); + doTest(fileName); + } + + @TestMetadata("checkForNativeInterface.kt") + public void testCheckForNativeInterface() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/testsWithJsStdLib/native/rtti/checkForNativeInterface.kt"); + doTest(fileName); + } + + @TestMetadata("whenIsNativeInterface.kt") + public void testWhenIsNativeInterface() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/testsWithJsStdLib/native/rtti/whenIsNativeInterface.kt"); + doTest(fileName); + } + } + @TestMetadata("compiler/testData/diagnostics/testsWithJsStdLib/native/unusedParam") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class) diff --git a/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/JsPlatformConfigurator.kt b/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/JsPlatformConfigurator.kt index 19544a72a03..ce349fa8586 100644 --- a/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/JsPlatformConfigurator.kt +++ b/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/JsPlatformConfigurator.kt @@ -22,6 +22,7 @@ import org.jetbrains.kotlin.container.useInstance import org.jetbrains.kotlin.js.resolve.diagnostics.JsCallChecker import org.jetbrains.kotlin.js.resolve.diagnostics.JsNameChecker import org.jetbrains.kotlin.js.resolve.diagnostics.JsNameClashChecker +import org.jetbrains.kotlin.js.resolve.diagnostics.JsNativeRttiChecker import org.jetbrains.kotlin.js.resolve.diagnostics.NativeInnerClassChecker import org.jetbrains.kotlin.platform.PlatformToKotlinClassMap import org.jetbrains.kotlin.resolve.IdentifierChecker @@ -51,5 +52,6 @@ object JsPlatformConfigurator : PlatformConfigurator( container.useInstance(JsTypeSpecificityComparator) container.useInstance(JsNameClashChecker()) container.useImpl() + container.useImpl() } } diff --git a/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/DefaultErrorMessagesJs.kt b/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/DefaultErrorMessagesJs.kt index 8812d201acb..7e0debb6b3d 100644 --- a/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/DefaultErrorMessagesJs.kt +++ b/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/DefaultErrorMessagesJs.kt @@ -19,12 +19,13 @@ package org.jetbrains.kotlin.js.resolve.diagnostics import org.jetbrains.kotlin.diagnostics.rendering.DefaultErrorMessages import org.jetbrains.kotlin.diagnostics.rendering.DiagnosticFactoryToRendererMap import org.jetbrains.kotlin.diagnostics.rendering.Renderers +import org.jetbrains.kotlin.diagnostics.rendering.Renderers.RENDER_TYPE private val DIAGNOSTIC_FACTORY_TO_RENDERER by lazy { with(DiagnosticFactoryToRendererMap("JS")) { put(ErrorsJs.NATIVE_ANNOTATIONS_ALLOWED_ONLY_ON_MEMBER_OR_EXTENSION_FUN, - "Annotation ''{0}'' is allowed only on member functions of declaration annotated as ''kotlin.js.native'' or on toplevel extension functions", Renderers.RENDER_TYPE) + "Annotation ''{0}'' is allowed only on member functions of declaration annotated as ''kotlin.js.native'' or on toplevel extension functions", RENDER_TYPE) put(ErrorsJs.NATIVE_INDEXER_KEY_SHOULD_BE_STRING_OR_NUMBER, "Native {0}''s first parameter type should be ''kotlin.String'' or subtype of ''kotlin.Number''", Renderers.STRING) put(ErrorsJs.NATIVE_INDEXER_CAN_NOT_HAVE_DEFAULT_ARGUMENTS, "Native {0}''s parameter can not have default value", Renderers.STRING) put(ErrorsJs.NATIVE_GETTER_RETURN_TYPE_SHOULD_BE_NULLABLE, "Native getter's return type should be nullable") @@ -47,6 +48,8 @@ private val DIAGNOSTIC_FACTORY_TO_RENDERER by lazy { put(ErrorsJs.JS_NAME_PROHIBITED_FOR_OVERRIDE, "@JsName is prohibited for overridden members") put(ErrorsJs.JS_NAME_PROHIBITED_FOR_EXTENSION_PROPERTY, "@JsName is prohibited for extension properties") put(ErrorsJs.JS_NAME_PROHIBITED_FOR_NAMED_NATIVE, "@JsName is prohibited for @native declaration with explicit name") + put(ErrorsJs.CANNOT_CHECK_FOR_NATIVE_INTERFACE, "Cannot check for native interface: {0}", RENDER_TYPE) + put(ErrorsJs.UNCHECKED_CAST_TO_NATIVE_INTERFACE, "Unchecked cast to native interface: {0} to {1}", RENDER_TYPE, RENDER_TYPE) this } diff --git a/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/ErrorsJs.java b/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/ErrorsJs.java index da59dbf44a9..46fdac52eb7 100644 --- a/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/ErrorsJs.java +++ b/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/ErrorsJs.java @@ -52,6 +52,8 @@ public interface ErrorsJs { DiagnosticFactory0 JS_NAME_PROHIBITED_FOR_OVERRIDE = DiagnosticFactory0.create(ERROR); DiagnosticFactory0 JS_NAME_PROHIBITED_FOR_EXTENSION_PROPERTY = DiagnosticFactory0.create(ERROR); DiagnosticFactory0 JS_NAME_PROHIBITED_FOR_NAMED_NATIVE = DiagnosticFactory0.create(ERROR); + DiagnosticFactory1 CANNOT_CHECK_FOR_NATIVE_INTERFACE = DiagnosticFactory1.create(ERROR); + DiagnosticFactory2 UNCHECKED_CAST_TO_NATIVE_INTERFACE = DiagnosticFactory2.create(WARNING); @SuppressWarnings("UnusedDeclaration") Object _initializer = new Object() { diff --git a/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/JsNativeRttiChecker.kt b/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/JsNativeRttiChecker.kt new file mode 100644 index 00000000000..37e3a2cad4d --- /dev/null +++ b/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/JsNativeRttiChecker.kt @@ -0,0 +1,42 @@ +/* + * 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 org.jetbrains.kotlin.js.resolve.diagnostics + +import com.intellij.psi.PsiElement +import org.jetbrains.kotlin.js.translate.utils.AnnotationsUtils +import org.jetbrains.kotlin.resolve.BindingTrace +import org.jetbrains.kotlin.resolve.DescriptorUtils +import org.jetbrains.kotlin.resolve.calls.checkers.RttiExpressionChecker +import org.jetbrains.kotlin.resolve.calls.checkers.RttiExpressionInformation +import org.jetbrains.kotlin.resolve.calls.checkers.RttiOperation + +class JsNativeRttiChecker : RttiExpressionChecker { + override fun check(rttiInformation: RttiExpressionInformation, reportOn: PsiElement, trace: BindingTrace) { + val sourceType = rttiInformation.sourceType + val targetType = rttiInformation.targetType + val targetDescriptor = targetType?.constructor?.declarationDescriptor + if (sourceType != null && targetDescriptor != null && AnnotationsUtils.isNativeInterface(targetDescriptor)) { + when (rttiInformation.operation) { + RttiOperation.IS, + RttiOperation.NOT_IS -> trace.report(ErrorsJs.CANNOT_CHECK_FOR_NATIVE_INTERFACE.on(reportOn, targetType)) + + RttiOperation.AS, + RttiOperation.SAFE_AS -> trace.report(ErrorsJs.UNCHECKED_CAST_TO_NATIVE_INTERFACE.on(reportOn, sourceType, targetType)) + } + } + } +} diff --git a/js/js.frontend/src/org/jetbrains/kotlin/js/translate/utils/AnnotationsUtils.java b/js/js.frontend/src/org/jetbrains/kotlin/js/translate/utils/AnnotationsUtils.java index 6605011f511..76bd54befe5 100644 --- a/js/js.frontend/src/org/jetbrains/kotlin/js/translate/utils/AnnotationsUtils.java +++ b/js/js.frontend/src/org/jetbrains/kotlin/js/translate/utils/AnnotationsUtils.java @@ -112,6 +112,10 @@ public final class AnnotationsUtils { return false; } + public static boolean isNativeInterface(@NotNull DeclarationDescriptor descriptor) { + return isNativeObject(descriptor) && DescriptorUtils.isInterface(descriptor); + } + public static boolean isLibraryObject(@NotNull DeclarationDescriptor descriptor) { return hasAnnotationOrInsideAnnotatedClass(descriptor, PredefinedAnnotation.LIBRARY); } 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 da592c2d163..d12ea576cb8 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 @@ -5492,6 +5492,24 @@ public class BoxJsTestGenerated extends AbstractBoxJsTest { KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("js/js.translator/testData/box/native"), Pattern.compile("^([^_](.+))\\.kt$"), TargetBackend.ANY, true); } + @TestMetadata("castToNativeClassChecked.kt") + public void testCastToNativeClassChecked() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/box/native/castToNativeClassChecked.kt"); + doTest(fileName); + } + + @TestMetadata("castToNativeInterface.kt") + public void testCastToNativeInterface() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/box/native/castToNativeInterface.kt"); + doTest(fileName); + } + + @TestMetadata("castToTypeParamBoundedByNativeInterface.kt") + public void testCastToTypeParamBoundedByNativeInterface() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/box/native/castToTypeParamBoundedByNativeInterface.kt"); + doTest(fileName); + } + @TestMetadata("class.kt") public void testClass() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/box/native/class.kt"); 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 02644f17671..b42e20e20c4 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 @@ -32,6 +32,7 @@ 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.intrinsic.functions.factories.TopLevelFIF; +import org.jetbrains.kotlin.js.translate.utils.AnnotationsUtils; import org.jetbrains.kotlin.js.translate.reference.ReferenceTranslator; import org.jetbrains.kotlin.js.translate.utils.BindingUtils; import org.jetbrains.kotlin.js.translate.utils.TranslationUtils; @@ -124,6 +125,10 @@ public final class PatternTranslator extends AbstractTranslator { if (sourceType != null && !DynamicTypesKt.isDynamic(sourceType) && TypeUtilsKt.isSubtypeOf(sourceType, targetType)) return null; JsExpression checkFunReference = doGetIsTypeCheckCallable(targetType); + if (checkFunReference == null) { + return null; + } + boolean isReifiedType = isReifiedTypeParameter(targetType); if (!isReifiedType && isNullableType(targetType) || isReifiedType && findChildByType(targetTypeReference, KtNodeTypes.NULLABLE_TYPE) != null @@ -137,6 +142,9 @@ public final class PatternTranslator extends AbstractTranslator { @NotNull public JsExpression getIsTypeCheckCallable(@NotNull KotlinType type) { JsExpression callable = doGetIsTypeCheckCallable(type); + assert callable != null : "This method should be called only to translate reified type parameters. " + + "`callable` should never be null for reified type parameters. " + + "Actual type: " + type; // If the type is reified, rely on the corresponding is type callable. // Otherwise make sure that passing null yields true @@ -147,8 +155,13 @@ public final class PatternTranslator extends AbstractTranslator { return callable; } - @NotNull + @Nullable private JsExpression doGetIsTypeCheckCallable(@NotNull KotlinType type) { + ClassifierDescriptor targetDescriptor = type.getConstructor().getDeclarationDescriptor(); + if (targetDescriptor != null && AnnotationsUtils.isNativeInterface(targetDescriptor)) { + return null; + } + JsExpression builtinCheck = getIsTypeCheckCallableForBuiltin(type); if (builtinCheck != null) return builtinCheck; @@ -164,13 +177,13 @@ public final class PatternTranslator extends AbstractTranslator { JsExpression result = null; for (KotlinType upperBound : typeParameterDescriptor.getUpperBounds()) { JsExpression next = doGetIsTypeCheckCallable(upperBound); - result = result != null ? namer().andPredicate(result, next) : next; + if (next != null) { + result = result != null ? namer().andPredicate(result, next) : next; + } } - assert result != null : "KotlinType is expected to return at least one upper bound: " + type; return result; } - ClassDescriptor referencedClass = DescriptorUtils.getClassDescriptorForType(type); JsExpression typeName = ReferenceTranslator.translateAsTypeReference(referencedClass, context()); return namer().isInstanceOf(typeName); diff --git a/js/js.translator/testData/box/native/castToNativeClassChecked.kt b/js/js.translator/testData/box/native/castToNativeClassChecked.kt new file mode 100644 index 00000000000..523800df8b1 --- /dev/null +++ b/js/js.translator/testData/box/native/castToNativeClassChecked.kt @@ -0,0 +1,29 @@ +// FILE: castToNativeClassChecked.kt +@native abstract class S() { + abstract fun foo(): String +} + +@native class A(x: String) { + fun foo(): String = noImpl +} + +fun createObject(): Any = A("fail: CCE not thrown") + +fun box(): String { + try { + return (createObject() as S).foo() + } + catch (e: ClassCastException) { + return "OK" + } +} + +// FILE: castToNativeClassChecked.js +function S() { +} +function A(x) { + this.x = x; +} +A.prototype.foo = function() { + return this.x; +} \ No newline at end of file diff --git a/js/js.translator/testData/box/native/castToNativeInterface.kt b/js/js.translator/testData/box/native/castToNativeInterface.kt new file mode 100644 index 00000000000..54380dd876d --- /dev/null +++ b/js/js.translator/testData/box/native/castToNativeInterface.kt @@ -0,0 +1,20 @@ +// FILE: castToNativeInterface.kt +@native interface I { + fun foo(): String +} + +@native class A(x: String) : I { + override fun foo(): String = noImpl +} + +fun createObject(): Any = A("OK") + +fun box() = (createObject() as I).foo() + +// FILE: castToNativeInterface.js +function A(x) { + this.x = x; +} +A.prototype.foo = function() { + return this.x; +} \ No newline at end of file diff --git a/js/js.translator/testData/box/native/castToTypeParamBoundedByNativeInterface.kt b/js/js.translator/testData/box/native/castToTypeParamBoundedByNativeInterface.kt new file mode 100644 index 00000000000..a754e77f681 --- /dev/null +++ b/js/js.translator/testData/box/native/castToTypeParamBoundedByNativeInterface.kt @@ -0,0 +1,32 @@ +// FILE: castToTypeParamBoundedByNativeInterface.kt +@native interface I { + fun foo(): String +} + +interface J { + fun bar(): String +} + +@native abstract class B() : I + +@native class A(x: String) : B() { + override fun foo(): String = noImpl +} + +fun createObject(): Any = A("OK") + +fun castToI(o: Any): T where T : I, T : B = o as T + +fun box() = castToI(createObject()).foo() + +// FILE: castToTypeParamBoundedByNativeInterface.js +function B() { +} + +function A(x) { + this.x = x; +} +A.prototype = Object.create(B.prototype); +A.prototype.foo = function() { + return this.x; +} \ No newline at end of file