[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
This commit is contained in:
Svyatoslav Kuzmich
2023-02-28 10:16:13 +01:00
committed by Space Team
parent bb05c8528f
commit 1e91fe155b
15 changed files with 299 additions and 121 deletions
@@ -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 {
+15 -13
View File
@@ -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<DC>
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"
+5 -4
View File
@@ -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> = C(x).toJsHandle()
@JsExport
fun getX(c: C): Int = c.x
fun getX(c: JsHandle<C>): 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<Any> = ei.toJsHandle()
@JsExport
fun anyAsEI(any: Any): EI = any as EI
fun anyAsEI(any: JsHandle<Any>): EI = any.get() as EI
fun box(): String = "OK"
@@ -43,7 +43,8 @@ fun testExterRef() {
check(null2ExternRef() == null)
}
class DataRef
class DataRefImpl
typealias DataRef = JsHandle<DataRefImpl>
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"
}
@@ -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"
}
-11
View File
@@ -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"
}
@@ -9,7 +9,7 @@ fun funBlockBody(x: Int): Int {
}
fun <!IMPLICIT_NOTHING_RETURN_TYPE!>returnTypeNotSepcified<!>() = js("1")
val <!IMPLICIT_NOTHING_PROPERTY_TYPE!>valTypeNotSepcified<!> = js("1")
<!WRONG_JS_INTEROP_TYPE!>val <!IMPLICIT_NOTHING_PROPERTY_TYPE!>valTypeNotSepcified<!><!> = js("1")
val a = "1"
fun nonConst(): String = "1"
@@ -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(
<!WRONG_JS_INTEROP_TYPE!>any: Any<!>,
<!WRONG_JS_INTEROP_TYPE!>nany: Any?<!>,
<!WRONG_JS_INTEROP_TYPE!>unit: Unit<!>,
<!WRONG_JS_INTEROP_TYPE!>nunit: Unit?<!>,
<!WRONG_JS_INTEROP_TYPE!>nothing: Nothing<!>,
<!WRONG_JS_INTEROP_TYPE!>nnothing: Nothing?<!>,
<!WRONG_JS_INTEROP_TYPE!>charSequence: CharSequence<!>,
<!WRONG_JS_INTEROP_TYPE!>list: List<Int><!>,
<!WRONG_JS_INTEROP_TYPE!>array: Array<Int><!>,
<!WRONG_JS_INTEROP_TYPE!>intArray: IntArray<!>,
<!WRONG_JS_INTEROP_TYPE!>pair: Pair<Int, Int><!>,
<!WRONG_JS_INTEROP_TYPE!>number: Number<!>,
)
external fun <<!WRONG_JS_INTEROP_TYPE!>T<!>> supportedTypeParamtersUpperBounds(p: T): T where T : EI, T : Any
external fun <
<!WRONG_JS_INTEROP_TYPE!>T1<!>,
<!WRONG_JS_INTEROP_TYPE!>T2 : Number<!>,
<!WRONG_JS_INTEROP_TYPE!>T3: List<Int><!>,
> supportedTypeParamtersUpperBounds(
p1: T1,
p2: T2
): T3
<!WRONG_JS_INTEROP_TYPE!>fun jsCode1(<!WRONG_JS_INTEROP_TYPE!>x: Any<!>): Any<!> = js("x")
<!WRONG_JS_INTEROP_TYPE!>fun jsCode2(<!WRONG_JS_INTEROP_TYPE!>x: Any<!>): Any<!> {
js("return x;")
}
<!WRONG_JS_INTEROP_TYPE!>val jsProp: Any<!> = js("1")
<!WRONG_JS_INTEROP_TYPE!>@JsExport
fun exported(<!WRONG_JS_INTEROP_TYPE!>x: Any<!>): Any<!> = x
@@ -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 {
@@ -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 {
@@ -29,6 +29,7 @@ object WasmPlatformConfigurator : PlatformConfiguratorBase(
WasmExternalDeclarationChecker,
WasmImportAnnotationChecker,
WasmJsFunAnnotationChecker,
WasmJsInteropTypesChecker,
),
additionalCallCheckers = listOf(
JsModuleCallChecker,
@@ -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")
@@ -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<KtElement, KotlinType> NON_EXTERNAL_TYPE_EXTENDS_EXTERNAL_TYPE =
DiagnosticFactory1.create(ERROR, PositioningStrategies.DECLARATION_SIGNATURE_OR_DEFAULT);
DiagnosticFactory2<PsiElement, String, KotlinType>
WRONG_JS_INTEROP_TYPE = DiagnosticFactory2.create(ERROR, PositioningStrategies.DECLARATION_SIGNATURE_OR_DEFAULT);
DiagnosticFactory0<PsiElement> NESTED_WASM_IMPORT = DiagnosticFactory0.create(ERROR);
DiagnosticFactory0<PsiElement> WASM_IMPORT_ON_NON_EXTERNAL_DECLARATION = DiagnosticFactory0.create(ERROR);
DiagnosticFactory0<PsiElement> WASM_IMPORT_PARAMETER_DEFAULT_VALUE = DiagnosticFactory0.create(ERROR);
@@ -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
}
@@ -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