From 1e91fe155b83996b66bbf18a841c208391a0c22c Mon Sep 17 00:00:00 2001 From: Svyatoslav Kuzmich Date: Tue, 28 Feb 2023 10:16:13 +0100 Subject: [PATCH] [Wasm] Restrict types allowed in JS interop - Prohibit Any, Array and other unsupported non-external types in JS interop context - Add K1 diagnostic - Update BE testdata ^KT-57136 Fixed --- .../codegen/boxWasmJsInterop/defaultValues.kt | 2 +- .../codegen/boxWasmJsInterop/functionTypes.kt | 28 ++-- .../codegen/boxWasmJsInterop/jsExport.kt | 9 +- .../boxWasmJsInterop/jsToKotlinAdapters.kt | 22 +-- .../boxWasmJsInterop/kotlinToJsAdapters.kt | 56 ------- .../codegen/boxWasmJsInterop/types.kt | 11 -- .../diagnostics/wasmTests/jsInterop/jsCode.kt | 2 +- .../diagnostics/wasmTests/jsInterop/types.kt | 104 ++++++++++++ ...irJsCodegenWasmJsInteropTestGenerated.java | 6 - ...IrCodegenWasmJsInteropJsTestGenerated.java | 6 - .../wasm/resolve/WasmPlatformConfigurator.kt | 1 + .../diagnostics/DefaultErrorMessagesWasm.kt | 7 + .../wasm/resolve/diagnostics/ErrorsWasm.java | 8 +- .../diagnostics/WasmJsInteropTypesChecker.kt | 152 ++++++++++++++++++ .../DiagnosticsWasmTestGenerated.java | 6 + 15 files changed, 299 insertions(+), 121 deletions(-) create mode 100644 compiler/testData/diagnostics/wasmTests/jsInterop/types.kt create mode 100644 wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/diagnostics/WasmJsInteropTypesChecker.kt diff --git a/compiler/testData/codegen/boxWasmJsInterop/defaultValues.kt b/compiler/testData/codegen/boxWasmJsInterop/defaultValues.kt index 0e2826a13f4..b4c477c46d2 100644 --- a/compiler/testData/codegen/boxWasmJsInterop/defaultValues.kt +++ b/compiler/testData/codegen/boxWasmJsInterop/defaultValues.kt @@ -53,7 +53,7 @@ external class C { val x1: Int val x2: Int fun foo(x3: Int = definedExternally, x4: Int = definedExternally): String - fun bar(x5: C = definedExternally, x6: Any = definedExternally) : String + fun bar(x5: C = definedExternally, x6: C = definedExternally) : String } open external class Writable: WritableStream { diff --git a/compiler/testData/codegen/boxWasmJsInterop/functionTypes.kt b/compiler/testData/codegen/boxWasmJsInterop/functionTypes.kt index abb045dc521..a8e7567dc42 100644 --- a/compiler/testData/codegen/boxWasmJsInterop/functionTypes.kt +++ b/compiler/testData/codegen/boxWasmJsInterop/functionTypes.kt @@ -88,7 +88,7 @@ function createJsLambda() { // FILE: externals.kt -external fun createJsLambda(): (Boolean, Byte, Short, Char, Int, Long, Float, Double, String, EI, DC, (DC) -> Int) -> Int +external fun createJsLambda(): (Boolean, Byte, Short, Char, Int, Long, Float, Double, String, EI, JSDC, (JSDC) -> Int) -> Int external fun apply7(f: (String) -> ((String) -> ((String) -> ((String) -> ((String) -> ((String) -> ((String) -> String))))))): String @@ -98,10 +98,11 @@ external fun is123Array(x: EI): Boolean external fun create123Array(): EI data class DC(val x: Int, val y: Int) +typealias JSDC = JsHandle external fun extenalWithLambda( - x: (Boolean, Byte, Short, Char, Int, Long, Float, Double, String, EI, DC) -> Unit, - dc: DC, + x: (Boolean, Byte, Short, Char, Int, Long, Float, Double, String, EI, JSDC) -> Unit, + dc: JSDC, ) external fun externalWithLambdas2( @@ -115,8 +116,8 @@ external fun externalWithLambdas2( double: () -> Double, string: () -> String, ei: () -> EI, - dc: () -> DC, - dcGetY: (DC) -> Int, + dc: () -> JSDC, + dcGetY: (JSDC) -> Int, ): Int @JsExport @@ -165,7 +166,8 @@ fun box(): String { double: Double, string: String, ei: EI, - dc: DC -> + jsdc: JSDC -> + val dc = jsdc.get() test(bool == true) test(byte == 1.toByte()) test(short == 2.toShort()) @@ -177,7 +179,7 @@ fun box(): String { test(string == "S") test(is123Array(ei)) test(dc.x == 100 && dc.y == 200) - }, DC(100, 200)) + }, DC(100, 200).toJsHandle()) if (extenalWithLambdasCount != 11) return "Fail 1" @@ -193,8 +195,8 @@ fun box(): String { double = { 600.5 }, string = { "700" }, ei = { create123Array() }, - dc = { DC(800, 800) }, - dcGetY = { it.y } + dc = { DC(800, 800).toJsHandle() }, + dcGetY = { it.get().y } ) if (externalWithLambdas2Count != 11) return "Fail externalWithLambdas2" @@ -210,8 +212,8 @@ fun box(): String { { 600.5 }, { "700" }, { create123Array() }, - { DC(800, 800) }, - { it.y } + { DC(800, 800).toJsHandle() }, + { it.get().y } ) if (externalWithLambdas2RefCount != 11) return "Fail externalWithLambdas2" @@ -228,8 +230,8 @@ fun box(): String { 600.5, "700", create123Array(), - DC(800, 800), - { it.y } + DC(800, 800).toJsHandle(), + { it.get().y } ) if (jsLambdaCount != 11) return "Fail 3" diff --git a/compiler/testData/codegen/boxWasmJsInterop/jsExport.kt b/compiler/testData/codegen/boxWasmJsInterop/jsExport.kt index b59eaa1c501..f8b8b8b75ca 100644 --- a/compiler/testData/codegen/boxWasmJsInterop/jsExport.kt +++ b/compiler/testData/codegen/boxWasmJsInterop/jsExport.kt @@ -1,13 +1,14 @@ +// TARGET_BACKEND: WASM // MODULE: main // FILE: externals.kt class C(val x: Int) @JsExport -fun makeC(x: Int): C = C(x) +fun makeC(x: Int): JsHandle = C(x).toJsHandle() @JsExport -fun getX(c: C): Int = c.x +fun getX(c: JsHandle): Int = c.get().x @JsExport fun getString(s: String): String = "Test string $s"; @@ -18,10 +19,10 @@ fun isEven(x: Int): Boolean = x % 2 == 0 external interface EI @JsExport -fun eiAsAny(ei: EI): Any = ei +fun eiAsAny(ei: EI): JsHandle = ei.toJsHandle() @JsExport -fun anyAsEI(any: Any): EI = any as EI +fun anyAsEI(any: JsHandle): EI = any.get() as EI fun box(): String = "OK" diff --git a/compiler/testData/codegen/boxWasmJsInterop/jsToKotlinAdapters.kt b/compiler/testData/codegen/boxWasmJsInterop/jsToKotlinAdapters.kt index dc60f7aab8c..0882180c66d 100644 --- a/compiler/testData/codegen/boxWasmJsInterop/jsToKotlinAdapters.kt +++ b/compiler/testData/codegen/boxWasmJsInterop/jsToKotlinAdapters.kt @@ -43,7 +43,8 @@ fun testExterRef() { check(null2ExternRef() == null) } -class DataRef +class DataRefImpl +typealias DataRef = JsHandle fun notNullDataRef(x: DataRef): DataRef = js("x") @@ -54,7 +55,7 @@ fun nullDataRef(x: DataRef): DataRef? = js("x") fun null2DataRef(x: DataRef): DataRef? = js("null") fun testDataRef() { - val dataRef = DataRef() + val dataRef = DataRefImpl().toJsHandle() check(notNullDataRef(dataRef) == dataRef) checkNPE { notNull2DataRef(dataRef) } check (nullDataRef(dataRef) == dataRef) @@ -121,22 +122,6 @@ fun testFloat() { check(null2Float() == null) } - -fun notNullNumber(): Number = js("123.5") - -fun notNull2Number(): Number = js("null") - -fun nullNumber(): Number? = js("123.5") - -fun null2Number(): Number? = js("null") - -fun testNumber() { - check(notNullNumber() == 123.5) - check(notNull2Number() == 0.0) - check(nullNumber() == 123.5) - check(null2Number() == null) -} - fun box(): String { testString() testExterRef() @@ -145,6 +130,5 @@ fun box(): String { testBoolean() testShort() testFloat() - testNumber() return "OK" } diff --git a/compiler/testData/codegen/boxWasmJsInterop/kotlinToJsAdapters.kt b/compiler/testData/codegen/boxWasmJsInterop/kotlinToJsAdapters.kt index fba075656cb..dfdd3bf4bbe 100644 --- a/compiler/testData/codegen/boxWasmJsInterop/kotlinToJsAdapters.kt +++ b/compiler/testData/codegen/boxWasmJsInterop/kotlinToJsAdapters.kt @@ -40,27 +40,6 @@ fun testExterRef() { null2ExternRef(null) } -class DataRef - -fun notNullDataRef(x: DataRef) { - js("if (x === null) throw 'error'") -} - -fun nullDataRef(x: DataRef?) { - js("if (x === null) throw 'error'") -} - -fun null2DataRef(x: DataRef?) { - js("if (x !== null) throw 'error'") -} - -fun testDataRef() { - val dataRef = DataRef() - notNullDataRef(dataRef) - nullDataRef(dataRef) - null2DataRef(null) -} - fun notNullInt(x: Int) { js("if (x !== 123) throw 'error'") } @@ -133,47 +112,12 @@ fun testFloat() { null2Float(null) } -fun notNullNumber(x: Number) { - js("if (x !== 123.5) throw 'error'") -} - -fun nullNumber(x: Number?) { - js("if (x !== 123.5) throw 'error'") -} - -fun null2Number(x: Number?) { - js("if (x !== null) throw 'error'") -} - -fun byte2Number(x: Number) { - js("if (x !== 123) throw 'error'") -} - -fun notNullByte2Number(x: Number?) { - js("if (x !== 123) throw 'error'") -} - -fun nullByte2Number(x: Number?) { - js("if (x !== null) throw 'error'") -} - -fun testNumber() { - notNullNumber(123.5) - nullNumber(123.5) - null2Number(null) - byte2Number(123) - notNullByte2Number(123) - nullByte2Number(null) -} - fun box(): String { testString() testExterRef() - testDataRef() testInt() testBoolean() testShort() testFloat() - testNumber() return "OK" } \ No newline at end of file diff --git a/compiler/testData/codegen/boxWasmJsInterop/types.kt b/compiler/testData/codegen/boxWasmJsInterop/types.kt index 8ed3f297a4c..5eac17e388f 100644 --- a/compiler/testData/codegen/boxWasmJsInterop/types.kt +++ b/compiler/testData/codegen/boxWasmJsInterop/types.kt @@ -72,10 +72,8 @@ external fun getFalseBoolean(): Boolean external interface EI -external fun createJsObjectAsAny(): Any external fun createJsObjectAsExternalInterface(): EI external fun getObjectValueEI(x: EI): String -external fun getObjectValueAny(x: Any): String fun box(): String { // Strings @@ -98,15 +96,6 @@ fun box(): String { val objAsEI: EI = createJsObjectAsExternalInterface() if (getObjectValueEI(objAsEI) != "object created by createJsObjectAsExternalInterface") return "Fail createJsObjectAsExternalInterface + getObjectValueEI" - if (getObjectValueAny(objAsEI) != "object created by createJsObjectAsExternalInterface") - return "Fail createJsObjectAsExternalInterface + getObjectValueAny" - - // Any - val objAsAny: Any = createJsObjectAsAny() - if (getObjectValueAny(objAsAny) != "object created by createJsObjectAsAny") - return "Fail createJsObjectAsAny + getObjectValueAny" - if (getObjectValueEI(objAsAny as EI) != "object created by createJsObjectAsAny") - return "Fail createJsObjectAsAny + getObjectValueEI" return "OK" } \ No newline at end of file diff --git a/compiler/testData/diagnostics/wasmTests/jsInterop/jsCode.kt b/compiler/testData/diagnostics/wasmTests/jsInterop/jsCode.kt index 0182dd8e274..353f5925c95 100644 --- a/compiler/testData/diagnostics/wasmTests/jsInterop/jsCode.kt +++ b/compiler/testData/diagnostics/wasmTests/jsInterop/jsCode.kt @@ -9,7 +9,7 @@ fun funBlockBody(x: Int): Int { } fun returnTypeNotSepcified() = js("1") -val valTypeNotSepcified = js("1") +val valTypeNotSepcified = js("1") val a = "1" fun nonConst(): String = "1" diff --git a/compiler/testData/diagnostics/wasmTests/jsInterop/types.kt b/compiler/testData/diagnostics/wasmTests/jsInterop/types.kt new file mode 100644 index 00000000000..c66b2bb1337 --- /dev/null +++ b/compiler/testData/diagnostics/wasmTests/jsInterop/types.kt @@ -0,0 +1,104 @@ +// !OPT_IN: kotlin.js.ExperimentalJsExport + +external interface EI +external open class EC +external object EO + +external fun supportedTypes( + boolean: Boolean, + + byte: Byte, + short: Short, + int: Int, + long: Long, + + float: Float, + double: Double, + + char: Char, + string: String, + + ei: EI, + ec: EC, + eo: EO, + + f1: (Boolean, Byte, Short, Int, Long, Float, Double, Char, String, EI, EC, EO) -> Unit, + + f2: (Boolean) -> ((Int) -> ((Float) -> ((String) -> EI))), + + f3: ((((Boolean) -> Int) -> Float) -> String) -> EI, +): Unit + +external fun supportedNullableTypes( + boolean: Boolean?, + + byte: Byte?, + short: Short?, + int: Int?, + long: Long?, + + float: Float?, + double: Double?, + + char: Char?, + string: String?, + + ei: EI?, + ec: EC?, + eo: EO?, + + f1: ((Boolean?, Byte?, Short?, Int?, Long?, Float?, Double?, Char?, String?, EI?, EC?, EO?) -> Unit)?, + + f2: ((Boolean?) -> ((Int?) -> ((Float?) -> ((String?) -> EI)?)?)?)?, + + f3: ((((((((Boolean?) -> Int?)?) -> Float?)?) -> String?)?) -> EI?)?, +): Unit + +external fun supportedReturnTypeUnit(): Unit +external fun supportedReturnTypeNothing(): Nothing +external fun supportedReturnTypeBoolean(): Boolean +external fun supportedReturnTypeNullableInt(): Int? +external fun supportedReturnTypeEI(): EI +external fun supportedReturnTypeNullableEC(): EC? + +external fun < + T1 : EI, + T2 : EC?, + T3 : T1? + > supportedTypeParamtersUpperBounds(p1: T1, p2: T2): T3 + + +external fun wrongExternalTypes( + any: Any, + nany: Any?, + unit: Unit, + nunit: Unit?, + nothing: Nothing, + nnothing: Nothing?, + charSequence: CharSequence, + list: List, + array: Array, + intArray: IntArray, + pair: Pair, + number: Number, +) + +external fun <T> supportedTypeParamtersUpperBounds(p: T): T where T : EI, T : Any + +external fun < + T1, + T2 : Number, + T3: List, + > supportedTypeParamtersUpperBounds( + p1: T1, + p2: T2 +): T3 + +fun jsCode1(x: Any): Any = js("x") +fun jsCode2(x: Any): Any { + js("return x;") +} +val jsProp: Any = js("1") + +@JsExport +fun exported(x: Any): Any = x diff --git a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/fir/FirJsCodegenWasmJsInteropTestGenerated.java b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/fir/FirJsCodegenWasmJsInteropTestGenerated.java index c5c3bbf24b7..fd22cc887f1 100644 --- a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/fir/FirJsCodegenWasmJsInteropTestGenerated.java +++ b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/fir/FirJsCodegenWasmJsInteropTestGenerated.java @@ -55,12 +55,6 @@ public class FirJsCodegenWasmJsInteropTestGenerated extends AbstractFirJsCodegen runTest("compiler/testData/codegen/boxWasmJsInterop/jsCode.kt"); } - @Test - @TestMetadata("jsExport.kt") - public void testJsExport() throws Exception { - runTest("compiler/testData/codegen/boxWasmJsInterop/jsExport.kt"); - } - @Test @TestMetadata("jsModule.kt") public void testJsModule() throws Exception { diff --git a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrCodegenWasmJsInteropJsTestGenerated.java b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrCodegenWasmJsInteropJsTestGenerated.java index b4e0acac89f..966547a7851 100644 --- a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrCodegenWasmJsInteropJsTestGenerated.java +++ b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrCodegenWasmJsInteropJsTestGenerated.java @@ -55,12 +55,6 @@ public class IrCodegenWasmJsInteropJsTestGenerated extends AbstractIrCodegenWasm runTest("compiler/testData/codegen/boxWasmJsInterop/jsCode.kt"); } - @Test - @TestMetadata("jsExport.kt") - public void testJsExport() throws Exception { - runTest("compiler/testData/codegen/boxWasmJsInterop/jsExport.kt"); - } - @Test @TestMetadata("jsModule.kt") public void testJsModule() throws Exception { 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 a28b6a9eb9c..4eea7c1c7fe 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 @@ -29,6 +29,7 @@ object WasmPlatformConfigurator : PlatformConfiguratorBase( WasmExternalDeclarationChecker, WasmImportAnnotationChecker, WasmJsFunAnnotationChecker, + WasmJsInteropTypesChecker, ), additionalCallCheckers = listOf( JsModuleCallChecker, 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 1240c40ce47..79a25d2b9e2 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 @@ -18,6 +18,13 @@ private val DIAGNOSTIC_FACTORY_TO_RENDERER by lazy { Renderers.RENDER_TYPE ) + put( + ErrorsWasm.WRONG_JS_INTEROP_TYPE, "Type {1} cannot be used in {0}. " + + "Only external, primitive, string and function types are supported in Kotlin/Wasm JS interop.", + CommonRenderers.STRING, + Renderers.RENDER_TYPE + ) + put(ErrorsWasm.NESTED_WASM_IMPORT, "Only top-level functions can be imported with @WasmImport") put(ErrorsWasm.WASM_IMPORT_ON_NON_EXTERNAL_DECLARATION, "Functions annotated with @WasmImport must be external") put(ErrorsWasm.WASM_IMPORT_PARAMETER_DEFAULT_VALUE, "Default parameter values are not supported with @WasmImport") 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 c12eaf25f53..01153e41631 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 @@ -6,10 +6,7 @@ package org.jetbrains.kotlin.wasm.resolve.diagnostics; import com.intellij.psi.PsiElement; -import org.jetbrains.kotlin.diagnostics.DiagnosticFactory0; -import org.jetbrains.kotlin.diagnostics.DiagnosticFactory1; -import org.jetbrains.kotlin.diagnostics.Errors; -import org.jetbrains.kotlin.diagnostics.PositioningStrategies; +import org.jetbrains.kotlin.diagnostics.*; import org.jetbrains.kotlin.psi.KtElement; import org.jetbrains.kotlin.types.KotlinType; @@ -19,6 +16,9 @@ public interface ErrorsWasm { DiagnosticFactory1 NON_EXTERNAL_TYPE_EXTENDS_EXTERNAL_TYPE = DiagnosticFactory1.create(ERROR, PositioningStrategies.DECLARATION_SIGNATURE_OR_DEFAULT); + DiagnosticFactory2 + WRONG_JS_INTEROP_TYPE = DiagnosticFactory2.create(ERROR, PositioningStrategies.DECLARATION_SIGNATURE_OR_DEFAULT); + DiagnosticFactory0 NESTED_WASM_IMPORT = DiagnosticFactory0.create(ERROR); DiagnosticFactory0 WASM_IMPORT_ON_NON_EXTERNAL_DECLARATION = DiagnosticFactory0.create(ERROR); DiagnosticFactory0 WASM_IMPORT_PARAMETER_DEFAULT_VALUE = DiagnosticFactory0.create(ERROR); diff --git a/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/diagnostics/WasmJsInteropTypesChecker.kt b/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/diagnostics/WasmJsInteropTypesChecker.kt new file mode 100644 index 00000000000..81db8dbcc9c --- /dev/null +++ b/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/diagnostics/WasmJsInteropTypesChecker.kt @@ -0,0 +1,152 @@ +/* + * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.wasm.resolve.diagnostics + +import com.intellij.psi.PsiElement +import org.jetbrains.kotlin.builtins.KotlinBuiltIns +import org.jetbrains.kotlin.builtins.isFunctionType +import org.jetbrains.kotlin.descriptors.* +import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi +import org.jetbrains.kotlin.js.translate.utils.AnnotationsUtils +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.psi.KtDeclaration +import org.jetbrains.kotlin.resolve.checkers.DeclarationChecker +import org.jetbrains.kotlin.resolve.checkers.DeclarationCheckerContext +import org.jetbrains.kotlin.resolve.descriptorUtil.isEffectivelyExternal +import org.jetbrains.kotlin.types.KotlinType +import org.jetbrains.kotlin.types.typeUtil.isNothing +import org.jetbrains.kotlin.types.typeUtil.isTypeParameter +import org.jetbrains.kotlin.types.typeUtil.isUnit +import org.jetbrains.kotlin.types.typeUtil.makeNotNullable +import org.jetbrains.kotlin.wasm.util.hasValidJsCodeBody + +// TODO: Implement in K2: KT-56849 +object WasmJsInteropTypesChecker : DeclarationChecker { + override fun check(declaration: KtDeclaration, descriptor: DeclarationDescriptor, context: DeclarationCheckerContext) { + if (descriptor !is MemberDescriptor) + return + + val trace = context.trace + val bindingContext = trace.bindingContext + + fun isExternalJsInteropDeclaration() = + descriptor.isEffectivelyExternal() && + !descriptor.annotations.hasAnnotation(FqName("kotlin.wasm.WasmImport")) + + fun isJsCodeDeclaration() = + (descriptor is FunctionDescriptor && descriptor.hasValidJsCodeBody(bindingContext) || + descriptor is PropertyDescriptor && descriptor.hasValidJsCodeBody(bindingContext)) + + fun isJsExportDeclaration() = + AnnotationsUtils.isExportedObject(descriptor, bindingContext) + + if ( + !isExternalJsInteropDeclaration() && + !isJsCodeDeclaration() && + !isJsExportDeclaration() + ) { + return + } + + fun KotlinType.checkJsInteropType( + typePositionDescription: String, + reportOn: PsiElement, + isInFunctionReturnPosition: Boolean = false, + ) { + if (!isTypeSupportedInJsInterop(this, isInFunctionReturnPosition)) { + trace.report(ErrorsWasm.WRONG_JS_INTEROP_TYPE.on(reportOn, typePositionDescription, this)) + } + } + + fun TypeParameterDescriptor.checkJsInteropTypeParameter() { + for (upperBound in this.upperBounds) { + if (!isTypeSupportedInJsInterop(upperBound, isInFunctionReturnPosition = false)) { + val reportOn = this.findPsi() ?: declaration + trace.report(ErrorsWasm.WRONG_JS_INTEROP_TYPE.on(reportOn, "type parameter upper bound", upperBound)) + } + } + } + + when (descriptor) { + is ClassDescriptor -> { + for (typeParameter in descriptor.declaredTypeParameters) { + typeParameter.checkJsInteropTypeParameter() + } + } + is PropertyDescriptor -> { + for (typeParameter in descriptor.typeParameters) { + typeParameter.checkJsInteropTypeParameter() + } + descriptor.type.checkJsInteropType("external property", declaration) + } + is FunctionDescriptor -> { + for (typeParameter in descriptor.typeParameters) { + typeParameter.checkJsInteropTypeParameter() + } + for (parameter in descriptor.valueParameters) { + val typeToCheck = parameter.varargElementType ?: parameter.type + typeToCheck.checkJsInteropType( + "external function parameter", + reportOn = parameter.findPsi() ?: declaration + ) + } + descriptor.returnType?.checkJsInteropType( + "external function return", + reportOn = declaration, + isInFunctionReturnPosition = true + ) + } + } + } +} + +private fun isTypeSupportedInJsInterop( + type: KotlinType, + isInFunctionReturnPosition: Boolean, +): Boolean { + if (type.isUnit() || type.isNothing()) { + return isInFunctionReturnPosition + } + + val nonNullable = type.makeNotNullable() + if ( + KotlinBuiltIns.isPrimitiveType(nonNullable) || + KotlinBuiltIns.isString(nonNullable) + ) { + return true + } + + // Interop type parameters upper bounds should are checked + // on declaration site separately + if (nonNullable.isTypeParameter()) { + return true + } + + val classifierDescriptor = nonNullable.constructor.declarationDescriptor + if (classifierDescriptor is MemberDescriptor && classifierDescriptor.isEffectivelyExternal()) { + return true + } + + if (type.isFunctionType) { + val arguments = type.arguments + for (i in 0 until arguments.lastIndex) { + val isArgumentSupported = isTypeSupportedInJsInterop( + arguments[i].type, + isInFunctionReturnPosition = false, + ) + + if (!isArgumentSupported) { + return false + } + } + + return isTypeSupportedInJsInterop( + arguments.last().type, // Function type result type + isInFunctionReturnPosition = true, + ) + } + return false +} 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 c81ff4edb63..9952677de28 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 @@ -68,6 +68,12 @@ public class DiagnosticsWasmTestGenerated extends AbstractDiagnosticsWasmTest { public void testJsFun() throws Exception { runTest("compiler/testData/diagnostics/wasmTests/jsInterop/jsFun.kt"); } + + @Test + @TestMetadata("types.kt") + public void testTypes() throws Exception { + runTest("compiler/testData/diagnostics/wasmTests/jsInterop/types.kt"); + } } @Nested