From 7a04999e4aaaec931d01f6007c5ec3e92926fd40 Mon Sep 17 00:00:00 2001 From: Svyatoslav Kuzmich Date: Wed, 22 Feb 2023 18:09:26 +0100 Subject: [PATCH] [Wasm] Add K1 compiler diagnostics for js(code) calls (KT-56955) This function is handled as intrinsics and supported in limited context --- .../diagnostics/wasmTests/jsInterop/jsCode.kt | 83 ++++++++++++++++ .../wasm/resolve/WasmPlatformConfigurator.kt | 7 +- .../diagnostics/DefaultErrorMessagesWasm.kt | 16 +++ .../wasm/resolve/diagnostics/ErrorsWasm.java | 4 + .../diagnostics/WasmJsCodeCallsChecker.kt | 97 +++++++++++++++++++ .../jetbrains/kotlin/wasm/util/jsCodeUtils.kt | 11 ++- .../DiagnosticsWasmTestGenerated.java | 6 ++ 7 files changed, 217 insertions(+), 7 deletions(-) create mode 100644 compiler/testData/diagnostics/wasmTests/jsInterop/jsCode.kt create mode 100644 wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/diagnostics/WasmJsCodeCallsChecker.kt diff --git a/compiler/testData/diagnostics/wasmTests/jsInterop/jsCode.kt b/compiler/testData/diagnostics/wasmTests/jsInterop/jsCode.kt new file mode 100644 index 00000000000..77b23b358ee --- /dev/null +++ b/compiler/testData/diagnostics/wasmTests/jsInterop/jsCode.kt @@ -0,0 +1,83 @@ +val prop: Int = + js("1") + +fun funExprBody(x: Int): Int = + js("x") + +fun funBlockBody(x: Int): Int { + js("return x;") +} + +fun returnTypeNotSepcified() = js("1") +val valTypeNotSepcified = js("1") + +val a = "1" +fun nonConst(): String = "1" + +val p0: Int = js(a) +val p1: Int = js(("1")) +val p2: Int = js("$a") +val p3: Int = js("${1}") +val p4: Int = js("${a}${a}") +val p5: Int = js(a + a) +val p6: Int = js("1" + "1") +val p7: Int = js(nonConst()) + +fun foo0(b: Boolean): Int = + if (b) js("1") else js("2") + +fun foo1(): Int { + println() + js("return x;") +} + +fun foo11() { + fun local1(): Int = js("1") + fun local2(): Int { + js("return 1;") + } + fun local3(): Int { + println() + js("return 1;") + } +} + +class C { + fun memberFun1(): Int = js("1") + fun memberFun2(): Int { + js("return 1;") + } + + constructor() { + js("1;") + } + + init { + js("1") + } + + val memberProperty: Int = js("1") +} + +fun withDefault(x: Int = js("1")) { + println(x) +} + +suspend fun suspendFun(): Int = js("1") + +inline fun inlineFun(f: () -> Int): Int = js("f()") + +fun Int.extensionFun(): Int = js("1") + +var propertyWithAccessors: Int + get(): Int = js("1") + set(value: Int) { + js("console.log(value);") + } + + +fun invalidNames( + `a b`: Int, + `1b`: Int, + `ab$`: Int +): Int = js("1") diff --git a/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/WasmPlatformConfigurator.kt b/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/WasmPlatformConfigurator.kt index c811b1967e5..a28b6a9eb9c 100644 --- a/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/WasmPlatformConfigurator.kt +++ b/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/WasmPlatformConfigurator.kt @@ -15,10 +15,7 @@ import org.jetbrains.kotlin.resolve.PlatformConfiguratorBase import org.jetbrains.kotlin.resolve.calls.checkers.LateinitIntrinsicApplicabilityChecker import org.jetbrains.kotlin.resolve.checkers.ExpectedActualDeclarationChecker import org.jetbrains.kotlin.wasm.analyze.WasmDiagnosticSuppressor -import org.jetbrains.kotlin.wasm.resolve.diagnostics.WasmExternalDeclarationChecker -import org.jetbrains.kotlin.wasm.resolve.diagnostics.WasmExternalInheritanceChecker -import org.jetbrains.kotlin.wasm.resolve.diagnostics.WasmImportAnnotationChecker -import org.jetbrains.kotlin.wasm.resolve.diagnostics.WasmJsFunAnnotationChecker +import org.jetbrains.kotlin.wasm.resolve.diagnostics.* // TODO: Review the list of used K/JS checkers. // Refactor useful checkers into common module. @@ -41,7 +38,7 @@ object WasmPlatformConfigurator : PlatformConfiguratorBase( ) { override fun configureModuleComponents(container: StorageComponentContainer) { container.useInstance(NameSuggestion()) - container.useImpl() + container.useImpl() container.useImpl() container.useImpl() container.useInstance(JsModuleClassLiteralChecker) diff --git a/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/diagnostics/DefaultErrorMessagesWasm.kt b/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/diagnostics/DefaultErrorMessagesWasm.kt index 9520d1a300e..1240c40ce47 100644 --- a/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/diagnostics/DefaultErrorMessagesWasm.kt +++ b/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/diagnostics/DefaultErrorMessagesWasm.kt @@ -5,6 +5,7 @@ package org.jetbrains.kotlin.wasm.resolve.diagnostics +import org.jetbrains.kotlin.diagnostics.rendering.CommonRenderers import org.jetbrains.kotlin.diagnostics.rendering.DefaultErrorMessages import org.jetbrains.kotlin.diagnostics.rendering.DiagnosticFactoryToRendererMap import org.jetbrains.kotlin.diagnostics.rendering.Renderers @@ -25,6 +26,21 @@ private val DIAGNOSTIC_FACTORY_TO_RENDERER by lazy { put(ErrorsWasm.WASM_IMPORT_UNSUPPORTED_RETURN_TYPE, "Unsupported @WasmImport return type {0}", Renderers.RENDER_TYPE) put(ErrorsWasm.WRONG_JS_FUN_TARGET, "Only top-level external functions can be implemented using @JsFun") + + put( + ErrorsWasm.JSCODE_WRONG_CONTEXT, + "Calls to js(code) should be a single expression inside a top-level function body or a property initializer in Kotlin/Wasm" + ) + put( + ErrorsWasm.JSCODE_UNSUPPORTED_FUNCTION_KIND, + "Calls to js(code) are not supported in {0} in Kotlin/Wasm", + CommonRenderers.STRING + ) + put( + ErrorsWasm.JSCODE_INVALID_PARAMETER_NAME, + "Parameters passed to js(code) should have a valid JavaScript name" + ) + } } diff --git a/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/diagnostics/ErrorsWasm.java b/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/diagnostics/ErrorsWasm.java index b5ca65acd57..c12eaf25f53 100644 --- a/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/diagnostics/ErrorsWasm.java +++ b/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/diagnostics/ErrorsWasm.java @@ -28,6 +28,10 @@ public interface ErrorsWasm { DiagnosticFactory0 WRONG_JS_FUN_TARGET = DiagnosticFactory0.create(ERROR); + DiagnosticFactory0 JSCODE_WRONG_CONTEXT = DiagnosticFactory0.create(ERROR); + DiagnosticFactory1 JSCODE_UNSUPPORTED_FUNCTION_KIND = DiagnosticFactory1.create(ERROR); + DiagnosticFactory0 JSCODE_INVALID_PARAMETER_NAME = DiagnosticFactory0.create(ERROR); + @SuppressWarnings("UnusedDeclaration") Object _initializer = new Object() { { diff --git a/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/diagnostics/WasmJsCodeCallsChecker.kt b/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/diagnostics/WasmJsCodeCallsChecker.kt new file mode 100644 index 00000000000..25d2c86676f --- /dev/null +++ b/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/diagnostics/WasmJsCodeCallsChecker.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2010-2015 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.wasm.resolve.diagnostics + +import com.intellij.psi.PsiElement +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.descriptors.PropertyDescriptor +import org.jetbrains.kotlin.descriptors.isTopLevelInPackage +import org.jetbrains.kotlin.js.common.isValidES5Identifier +import org.jetbrains.kotlin.js.resolve.diagnostics.ErrorsJs +import org.jetbrains.kotlin.js.resolve.diagnostics.JsCallChecker.Companion.extractStringValue +import org.jetbrains.kotlin.js.resolve.diagnostics.JsCallChecker.Companion.isJsCall +import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi +import org.jetbrains.kotlin.psi.KtCallExpression +import org.jetbrains.kotlin.resolve.TemporaryBindingTrace +import org.jetbrains.kotlin.resolve.calls.checkers.CallChecker +import org.jetbrains.kotlin.resolve.calls.checkers.CallCheckerContext +import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall +import org.jetbrains.kotlin.resolve.constants.evaluate.ConstantExpressionEvaluator +import org.jetbrains.kotlin.types.TypeUtils +import org.jetbrains.kotlin.wasm.util.hasValidJsCodeBody + +class WasmJsCallChecker( + private val constantExpressionEvaluator: ConstantExpressionEvaluator +) : CallChecker { + + override fun check(resolvedCall: ResolvedCall<*>, reportOn: PsiElement, context: CallCheckerContext) { + if (context.isAnnotationContext || !resolvedCall.isJsCall()) return + + val containingDeclaration = context.scope.ownerDescriptor + if ( + !(containingDeclaration is FunctionDescriptor || containingDeclaration is PropertyDescriptor) || + !containingDeclaration.isTopLevelInPackage() + ) { + context.trace.report(ErrorsWasm.JSCODE_WRONG_CONTEXT.on(reportOn)) + } else { + when (containingDeclaration) { + is FunctionDescriptor -> { + if (!containingDeclaration.hasValidJsCodeBody(context.trace.bindingContext)) { + context.trace.report(ErrorsWasm.JSCODE_WRONG_CONTEXT.on(reportOn)) + } else { + if (containingDeclaration.isSuspend) { + context.trace.report(ErrorsWasm.JSCODE_UNSUPPORTED_FUNCTION_KIND.on(reportOn, "suspend function")) + } + if (containingDeclaration.isInline) { + context.trace.report(ErrorsWasm.JSCODE_UNSUPPORTED_FUNCTION_KIND.on(reportOn, "inline function")) + } + if (containingDeclaration.extensionReceiverParameter != null) { + context.trace.report(ErrorsWasm.JSCODE_UNSUPPORTED_FUNCTION_KIND.on(reportOn, "function with extension receiver")) + } + for (parameter in containingDeclaration.valueParameters) { + if (parameter.name.identifierOrNullIfSpecial?.isValidES5Identifier() != true) { + context.trace.report(ErrorsWasm.JSCODE_INVALID_PARAMETER_NAME.on(parameter.findPsi() ?: reportOn)) + } + } + } + } + is PropertyDescriptor -> { + if (!containingDeclaration.hasValidJsCodeBody(context.trace.bindingContext)) { + context.trace.report(ErrorsWasm.JSCODE_WRONG_CONTEXT.on(reportOn)) + } + } + } + } + + val expression = resolvedCall.call.callElement + if (expression !is KtCallExpression) return + + val arguments = expression.valueArgumentList?.arguments + val argument = arguments?.firstOrNull()?.getArgumentExpression() ?: return + + val trace = TemporaryBindingTrace.create(context.trace, "WasmJsCallChecker") + val evaluationResult = constantExpressionEvaluator.evaluateExpression(argument, trace, TypeUtils.NO_EXPECTED_TYPE) + val code = extractStringValue(evaluationResult) + + if (code == null) { + context.trace.report(ErrorsJs.JSCODE_ARGUMENT_SHOULD_BE_CONSTANT.on(argument)) + return + } + + trace.commit() + } +} \ No newline at end of file diff --git a/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/util/jsCodeUtils.kt b/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/util/jsCodeUtils.kt index 31089c01cba..d6a5fc4eb74 100644 --- a/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/util/jsCodeUtils.kt +++ b/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/util/jsCodeUtils.kt @@ -6,15 +6,22 @@ package org.jetbrains.kotlin.wasm.util import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.descriptors.PropertyDescriptor import org.jetbrains.kotlin.js.resolve.diagnostics.JsCallChecker.Companion.isJsCall import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.resolve.BindingContext import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall import org.jetbrains.kotlin.resolve.source.getPsi +fun PropertyDescriptor.hasValidJsCodeBody(bindingContext: BindingContext): Boolean { + val property = source.getPsi() as? KtProperty ?: return false + val initializer = property.initializer ?: return false + return initializer.isJsCall(bindingContext) +} + fun FunctionDescriptor.hasValidJsCodeBody(bindingContext: BindingContext): Boolean { - val psi = source.getPsi() as? KtNamedFunction ?: return false - return psi.hasValidJsCodeBody(bindingContext) + val function = source.getPsi() as? KtNamedFunction ?: return false + return function.hasValidJsCodeBody(bindingContext) } private fun KtDeclarationWithBody.hasValidJsCodeBody(bindingContext: BindingContext): Boolean { diff --git a/wasm/wasm.tests/tests-gen/org/jetbrains/kotlin/wasm/test/diagnostics/DiagnosticsWasmTestGenerated.java b/wasm/wasm.tests/tests-gen/org/jetbrains/kotlin/wasm/test/diagnostics/DiagnosticsWasmTestGenerated.java index 14cfd284daa..c81ff4edb63 100644 --- a/wasm/wasm.tests/tests-gen/org/jetbrains/kotlin/wasm/test/diagnostics/DiagnosticsWasmTestGenerated.java +++ b/wasm/wasm.tests/tests-gen/org/jetbrains/kotlin/wasm/test/diagnostics/DiagnosticsWasmTestGenerated.java @@ -51,6 +51,12 @@ public class DiagnosticsWasmTestGenerated extends AbstractDiagnosticsWasmTest { runTest("compiler/testData/diagnostics/wasmTests/jsInterop/inheritance.kt"); } + @Test + @TestMetadata("jsCode.kt") + public void testJsCode() throws Exception { + runTest("compiler/testData/diagnostics/wasmTests/jsInterop/jsCode.kt"); + } + @Test @TestMetadata("jsExport.kt") public void testJsExport() throws Exception {