From 1c534e5fd5e5c001ac586bbac9e7e3e20569ddcd Mon Sep 17 00:00:00 2001 From: Srujan Gaddam Date: Wed, 26 Jun 2024 23:06:36 +0000 Subject: [PATCH] [dart2wasm/ddc/dart2js] Lower Function.toJS and JSExportedDartFunction.toDart Lowers these extension methods to some helper functions instead of allowInterop to improve performance and get consistent semantics. Specifically: - Function.toJS no longer gives you the same JSFunction when calling toJS on the same function multiple times in the JS compilers. - Adds fast calling syntax for functions with 0-5 args in the JS compilers. - Allows additional args to be passed to converted functions that are omitted in the JS compilers. - The static type now determines the number of args that can be passed to the JS function in the JS compilers. - Fixes an issue in dart2wasm where if too few arguments are passed, the call may succeed due to conversion of undefined to null. - Adds throws when a user tries to wrap a JS function that returned from Function.toJS in the JS compilers. Closes https://github.com/dart-lang/sdk/issues/55515 Addresses https://github.com/dart-lang/sdk/issues/48186 CoreLibraryReviewExempt: Changes to docs only in API surface. Change-Id: I41d864dc5e02b597d9f1c16c88e3c04872f28225 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/368065 Reviewed-by: Nicholas Shahan Reviewed-by: Stephen Adams Commit-Queue: Srujan Gaddam Reviewed-by: Martin Kustermann --- CHANGELOG.md | 8 + .../transformations/js_util_optimizer.dart | 61 +++ .../lib/js/callback_specializer.dart | 104 ++-- pkg/dart2wasm/lib/js/util.dart | 14 + ...unctiontojs.dart.strong.transformed.expect | 30 +- .../functiontojs.dart.weak.transformed.expect | 30 +- ...unctiontojs.dart.strong.transformed.expect | 30 +- .../patch/js_allow_interop_patch.dart | 252 ++++++++++ .../private/ddc_runtime/operations.dart | 20 + .../lib/js_allow_interop_patch.dart | 186 ++++++- .../js_shared/lib/js_interop_patch.dart | 13 +- sdk/lib/js_interop/js_interop.dart | 12 +- .../js_function_arity_test.dart | 453 ++++++++++++++++++ .../js/static_interop_test/js_types_test.dart | 9 +- tests/lib/lib.status | 1 + 15 files changed, 1114 insertions(+), 109 deletions(-) create mode 100644 tests/lib/js/static_interop_test/js_function_arity_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fed68e8a39..859e1ad4d72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,11 +64,19 @@ `ExternalDartReferenceToObject` and `ObjectToExternalDartReference` are now extensions on `T` and `ExternalDartReference`, respectively, where `T extends Object?`. See [#55342][] and [#55536][] for more details. +- Fixes some consistency issues with `Function.toJS` across all compilers. + Specifically, calling `Function.toJS` on the same function gives you a new JS + function (see issue [#55515][]), the max number of args that are passed to the + JS function is determined by the static type of the Dart function, and extra + args are dropped when passed to the JS function in all compilers (see + [#48186][]). [#55508]: https://github.com/dart-lang/sdk/issues/55508 [#55267]: https://github.com/dart-lang/sdk/issues/55267 [#55342]: https://github.com/dart-lang/sdk/issues/55342 [#55536]: https://github.com/dart-lang/sdk/issues/55536 +[#55515]: https://github.com/dart-lang/sdk/issues/55515 +[#48186]: https://github.com/dart-lang/sdk/issues/48186 ### Tools diff --git a/pkg/_js_interop_checks/lib/src/transformations/js_util_optimizer.dart b/pkg/_js_interop_checks/lib/src/transformations/js_util_optimizer.dart index b61e03c4636..4e172d764b0 100644 --- a/pkg/_js_interop_checks/lib/src/transformations/js_util_optimizer.dart +++ b/pkg/_js_interop_checks/lib/src/transformations/js_util_optimizer.dart @@ -41,9 +41,14 @@ class JsUtilOptimizer extends Transformer { final List _callConstructorUncheckedTargets; final CloneVisitorNotMembers _cloner = CloneVisitorWithMembers(); final Map _externalInvocationBuilders = {}; + final Procedure _functionToJSTarget; + final List _functionToJSTargets; + final Procedure _functionToJSNTarget; final Procedure _getPropertyTarget; final Procedure _getPropertyTrustTypeTarget; final Procedure _globalContextTarget; + final Procedure _jsExportedDartFunctionToDartTarget; + final Procedure _jsFunctionToDart; final InterfaceType _objectType; final Procedure _setPropertyTarget; final Procedure _setPropertyUncheckedTarget; @@ -88,12 +93,25 @@ class JsUtilOptimizer extends Transformer { 5, (i) => _coreTypes.index.getTopLevelProcedure( 'dart:js_util', '_callConstructorUnchecked$i')), + _functionToJSTarget = _coreTypes.index.getTopLevelProcedure( + 'dart:js_interop', 'FunctionToJSExportedDartFunction|get#toJS'), + _functionToJSTargets = List.generate( + 6, + (i) => _coreTypes.index + .getTopLevelProcedure('dart:js_util', '_functionToJS$i')), + _functionToJSNTarget = _coreTypes.index + .getTopLevelProcedure('dart:js_util', '_functionToJSN'), _getPropertyTarget = _coreTypes.index .getTopLevelProcedure('dart:js_util', 'getProperty'), _getPropertyTrustTypeTarget = _coreTypes.index .getTopLevelProcedure('dart:js_util', '_getPropertyTrustType'), _globalContextTarget = _coreTypes.index.getTopLevelProcedure( 'dart:_js_helper', 'get:staticInteropGlobalContext'), + _jsExportedDartFunctionToDartTarget = _coreTypes.index + .getTopLevelProcedure('dart:js_interop', + 'JSExportedDartFunctionToFunction|get#toDart'), + _jsFunctionToDart = _coreTypes.index + .getTopLevelProcedure('dart:js_util', '_jsFunctionToDart'), _objectType = hierarchy.coreTypes.objectNonNullableRawType, _setPropertyTarget = _coreTypes.index .getTopLevelProcedure('dart:js_util', 'setProperty'), @@ -480,6 +498,10 @@ class JsUtilOptimizer extends Transformer { invocation = _lowerCallConstructor(node); // TODO(srujzs): Delete the `isPatchedMember` check once // https://github.com/dart-lang/sdk/issues/53367 is resolved. + } else if (target == _functionToJSTarget) { + invocation = _lowerFunctionToJS(node); + } else if (target == _jsExportedDartFunctionToDartTarget) { + invocation = _lowerJSExportedDartFunctionToDart(node); } else if (target.isExternal && !JsInteropChecks.isPatchedMember(target)) { final builder = _externalInvocationBuilders.putIfAbsent( target, () => _getExternalInvocationBuilder(target)); @@ -662,6 +684,45 @@ class JsUtilOptimizer extends Transformer { ..parent = nodeParent; } + /// For the given `dart:js_interop` `Function.toJS` invocation [node], returns + /// an invocation of `_functionToJSX` with the given `Function` argument, + /// where X is the number of the positional arguments. + /// + /// If the number of the positional arguments is larger than 5, returns an + /// invocation of `_functionToJSN` instead. + StaticInvocation _lowerFunctionToJS(StaticInvocation node) { + // JS interop checks assert that the static type is available, and that + // there are no named arguments or type arguments. + final function = node.arguments.positional.single; + final functionType = + function.getStaticType(_staticTypeContext) as FunctionType; + final argumentsLength = functionType.positionalParameters.length; + Procedure target; + Arguments arguments; + if (argumentsLength < _functionToJSTargets.length) { + target = _functionToJSTargets[argumentsLength]; + arguments = Arguments([function]); + } else { + target = _functionToJSNTarget; + arguments = Arguments([function, IntLiteral(argumentsLength)]); + } + return StaticInvocation( + target, arguments..fileOffset = node.arguments.fileOffset) + ..fileOffset = node.fileOffset + ..parent = node.parent; + } + + /// For the given `dart:js_interop` `JSExportedDartFunction.toDart` invocation + /// [node], returns an invocation of `_jsFunctionToDart` with the given + /// `JSExportedDartFunction` argument. + StaticInvocation _lowerJSExportedDartFunctionToDart(StaticInvocation node) => + StaticInvocation( + _jsFunctionToDart, + Arguments([node.arguments.positional[0]]) + ..fileOffset = node.arguments.fileOffset) + ..fileOffset = node.fileOffset + ..parent = node.parent; + /// Returns whether the given [node] is guaranteed to be allowed to interop /// with JS. /// diff --git a/pkg/dart2wasm/lib/js/callback_specializer.dart b/pkg/dart2wasm/lib/js/callback_specializer.dart index d4d1c5244d5..427aa0fa264 100644 --- a/pkg/dart2wasm/lib/js/callback_specializer.dart +++ b/pkg/dart2wasm/lib/js/callback_specializer.dart @@ -17,9 +17,6 @@ class CallbackSpecializer { CallbackSpecializer( this._staticTypeContext, this._util, this._methodCollector); - bool _needsArgumentsLength(FunctionType type) => - type.requiredParameterCount < type.positionalParameters.length; - Statement _generateDispatchCase( FunctionType function, VariableDeclaration callbackVariable, @@ -54,12 +51,11 @@ class CallbackSpecializer { /// Creates a callback trampoline for the given [function]. /// /// This callback trampoline expects a Dart callback as its first argument, - /// then an integer value(double type) indicating the position of the last - /// defined argument(only for callbacks that take optional parameters), - /// followed by all of the arguments to the Dart callback as JS objects. The - /// trampoline will `dartifyRaw` or box all incoming JS objects and then cast - /// them to their appropriate types, dispatch, and then `jsifyRaw` or box any - /// returned value. + /// then an integer value(double type) indicating the number of arguments + /// passed, followed by all of the arguments to the Dart callback as JS + /// objects. The trampoline will `dartifyRaw` or box all incoming JS objects + /// and then cast them to their appropriate types, dispatch, and then + /// `jsifyRaw` or box any returned value. /// /// Returns a [String] function name representing the name of the wrapping /// function. @@ -69,20 +65,18 @@ class CallbackSpecializer { // arguments will be JS objects. The generated wrapper will cast each // argument to the correct type. The first argument to this function will // be the Dart callback, which will be cast to the supplied [FunctionType] - // before being invoked. If the callback takes optional parameters then, the - // second argument will be a `double` indicating the last defined argument. + // before being invoked. The second argument will be a `double` indicating + // the number of arguments passed. int parameterId = 1; final callbackVariable = VariableDeclaration('callback', type: _util.nonNullableObjectType, isSynthesized: true); - VariableDeclaration? argumentsLength; - if (_needsArgumentsLength(function)) { - argumentsLength = VariableDeclaration('argumentsLength', - type: _util.coreTypes.doubleNonNullableRawType, isSynthesized: true); - } + final argumentsLength = VariableDeclaration('argumentsLength', + type: _util.coreTypes.doubleNonNullableRawType, isSynthesized: true); // Initialize variable declarations. List positionalParameters = []; - for (int j = 0; j < function.positionalParameters.length; j++) { + final positionalParametersLength = function.positionalParameters.length; + for (int j = 0; j < positionalParametersLength; j++) { positionalParameters.add(VariableDeclaration('x${parameterId++}', type: _util.nullableWasmExternRefType, isSynthesized: true)); } @@ -91,26 +85,42 @@ class CallbackSpecializer { // find the last defined argument in JS, that is the last argument which was // explicitly passed by the user, and then we dispatch to a Dart function // with the right number of arguments. - // - // First we handle cases where some or all arguments are undefined. - // TODO(joshualitt): Consider using a switch instead. List dispatchCases = []; - for (int i = function.requiredParameterCount + 1; - i <= function.positionalParameters.length; - i++) { + // If more arguments were passed than there are parameters, ignore the extra + // arguments. + dispatchCases.add(IfStatement( + _util.variableGreaterThanOrEqualToConstant( + argumentsLength, IntConstant(positionalParametersLength)), + _generateDispatchCase(function, callbackVariable, positionalParameters, + positionalParametersLength, + boxExternRef: boxExternRef), + null)); + // TODO(srujzs): Consider using a switch instead. + for (int i = positionalParametersLength - 1; + i >= function.requiredParameterCount; + i--) { dispatchCases.add(IfStatement( _util.variableCheckConstant( - argumentsLength!, DoubleConstant(i.toDouble())), + argumentsLength, DoubleConstant(i.toDouble())), _generateDispatchCase( function, callbackVariable, positionalParameters, i, boxExternRef: boxExternRef), null)); } - // Finally handle the case where only required parameters are passed. - dispatchCases.add(_generateDispatchCase(function, callbackVariable, - positionalParameters, function.requiredParameterCount, - boxExternRef: boxExternRef)); + // Throw since we have too few arguments. Alternatively, we can continue + // checking lengths and try to call the callback, which will then throw, but + // that's unnecessary extra code. Note that we can't exclude this and assume + // the last dispatch case will catch this. Since arguments that are not + // passed are `undefined` and `undefined` gets converted to `null`, they may + // be treated as valid `null` arguments to the Dart function even though + // they were never passed. + dispatchCases.add(ExpressionStatement(Throw(StringConcatenation([ + StringLiteral('Too few arguments passed. ' + 'Expected ${function.requiredParameterCount} or more, got '), + invokeMethod(argumentsLength, _util.numToIntTarget), + StringLiteral(' instead.') + ])))); Statement functionTrampolineBody = Block(dispatchCases); // Create a new procedure for the callback trampoline. This procedure will @@ -124,8 +134,9 @@ class CallbackSpecializer { FunctionNode(functionTrampolineBody, positionalParameters: [ callbackVariable, - if (argumentsLength != null) argumentsLength - ].followedBy(positionalParameters).toList(), + argumentsLength, + ...positionalParameters + ], returnType: _util.nullableWasmExternRefType) ..fileOffset = node.fileOffset, node.fileUri, @@ -144,11 +155,7 @@ class CallbackSpecializer { jsParameters.add('x$i'); } String jsParametersString = jsParameters.join(','); - String dartArguments = 'f'; - bool needsArguments = _needsArgumentsLength(type); - if (needsArguments) { - dartArguments = '$dartArguments,arguments.length'; - } + String dartArguments = 'f,arguments.length'; if (jsParameters.isNotEmpty) { dartArguments = '$dartArguments,$jsParametersString'; } @@ -171,24 +178,12 @@ class CallbackSpecializer { // Create JS method. // Note: We have to use a regular function for the inner closure in some // cases because we need access to `arguments`. - if (needsArguments) { - _methodCollector.addMethod( - dartProcedure, - jsMethodName, - "f => finalizeWrapper(f, function($jsParametersString) {" - " return dartInstance.exports.$functionTrampolineName($dartArguments) " - "})"); - } else { - if (parametersNeedParens(jsParameters)) { - jsParametersString = '($jsParametersString)'; - } - _methodCollector.addMethod( - dartProcedure, - jsMethodName, - "f => " - "finalizeWrapper(f,$jsParametersString => " - "dartInstance.exports.$functionTrampolineName($dartArguments))"); - } + _methodCollector.addMethod( + dartProcedure, + jsMethodName, + "f => finalizeWrapper(f, function($jsParametersString) {" + " return dartInstance.exports.$functionTrampolineName($dartArguments) " + "})"); return dartProcedure; } @@ -207,6 +202,9 @@ class CallbackSpecializer { /// these Dart functions flow to JS, they are replaced by their wrappers. If /// the wrapper should ever flow back into Dart then it will be replaced by /// the original Dart function. + // TODO(srujzs): It looks like there's no more code that references this + // function anymore in dart2wasm. Should we delete this lowering and related + // code? Expression allowInterop(StaticInvocation staticInvocation) { final argument = staticInvocation.arguments.positional.single; final type = argument.getStaticType(_staticTypeContext) as FunctionType; diff --git a/pkg/dart2wasm/lib/js/util.dart b/pkg/dart2wasm/lib/js/util.dart index bbe8759054a..38fbf896769 100644 --- a/pkg/dart2wasm/lib/js/util.dart +++ b/pkg/dart2wasm/lib/js/util.dart @@ -16,6 +16,7 @@ class CoreTypesUtil { final Procedure allowInteropTarget; final Procedure dartifyRawTarget; final Procedure functionToJSTarget; + final Procedure greaterThanOrEqualToTarget; final Procedure inlineJSTarget; final Procedure isDartFunctionWrappedTarget; final Procedure jsifyRawTarget; @@ -33,6 +34,8 @@ class CoreTypesUtil { .getTopLevelProcedure('dart:_js_helper', 'dartifyRaw'), functionToJSTarget = coreTypes.index.getTopLevelProcedure( 'dart:js_interop', 'FunctionToJSExportedDartFunction|get#toJS'), + greaterThanOrEqualToTarget = + coreTypes.index.getProcedure('dart:core', 'num', '>='), inlineJSTarget = coreTypes.index.getTopLevelProcedure('dart:_js_helper', 'JS'), isDartFunctionWrappedTarget = coreTypes.index @@ -94,6 +97,17 @@ class CoreTypesUtil { StaticInvocation(coreTypes.identicalProcedure, Arguments([VariableGet(variable), ConstantExpression(constant)])); + Expression variableGreaterThanOrEqualToConstant( + VariableDeclaration variable, Constant constant) => + InstanceInvocation( + InstanceAccessKind.Instance, + VariableGet(variable), + greaterThanOrEqualToTarget.name, + Arguments([ConstantExpression(constant)]), + interfaceTarget: greaterThanOrEqualToTarget, + functionType: greaterThanOrEqualToTarget.getterType as FunctionType, + ); + /// Cast the [invocation] if needed to conform to the expected [returnType]. Expression castInvocationForReturn( Expression invocation, DartType returnType) { diff --git a/pkg/front_end/testcases/dart2js/js_interop_transforms/functiontojs.dart.strong.transformed.expect b/pkg/front_end/testcases/dart2js/js_interop_transforms/functiontojs.dart.strong.transformed.expect index 81d2853c428..fadb6a53a3f 100644 --- a/pkg/front_end/testcases/dart2js/js_interop_transforms/functiontojs.dart.strong.transformed.expect +++ b/pkg/front_end/testcases/dart2js/js_interop_transforms/functiontojs.dart.strong.transformed.expect @@ -1,25 +1,25 @@ library; import self as self; -import "dart:js_interop" as js_; +import "dart:js_util" as js_; import "dart:core" as core; import "dart:js_interop"; static method main() → void { - js_::JSExportedDartFunction /* erasure=_interceptors::JavaScriptFunction */ jsFunction = js_::FunctionToJSExportedDartFunction|get#toJS(() → Null {}); - js_::FunctionToJSExportedDartFunction|get#toJS((core::int arg1) → core::int => arg1); - js_::FunctionToJSExportedDartFunction|get#toJS((([core::int? arg1 = #C1]) → core::int? => arg1) as () → void); - js_::FunctionToJSExportedDartFunction|get#toJS((core::int arg1, core::String arg2) → core::String => arg2); - js_::FunctionToJSExportedDartFunction|get#toJS((([core::int? arg1 = #C1, core::String? arg2 = #C1]) → core::String? => arg2) as ([core::int]) → void); - js_::FunctionToJSExportedDartFunction|get#toJS((core::int arg1, core::String arg2, js_::JSArray /* erasure=_interceptors::JSArray */ arg3) → js_::JSArray /* erasure=_interceptors::JSArray */ => arg3); - js_::FunctionToJSExportedDartFunction|get#toJS(((core::int arg1, [core::String? arg2 = #C1, js_::JSArray? /* erasure=_interceptors::JSArray? */ arg3 = #C1]) → js_::JSArray? /* erasure=_interceptors::JSArray? */ => arg3) as (core::int, [core::String]) → void); - js_::FunctionToJSExportedDartFunction|get#toJS((core::int arg1, core::String arg2, js_::JSArray /* erasure=_interceptors::JSArray */ arg3, js_::JSObject /* erasure=_interceptors::JSObject */ arg4) → js_::JSObject /* erasure=_interceptors::JSObject */ => arg4); - js_::FunctionToJSExportedDartFunction|get#toJS(((core::int arg1, core::String arg2, js_::JSArray /* erasure=_interceptors::JSArray */ arg3, [js_::JSObject? /* erasure=_interceptors::JSObject? */ arg4 = #C1]) → js_::JSObject? /* erasure=_interceptors::JSObject? */ => arg4) as (core::int, core::String, js_::JSArray /* erasure=_interceptors::JSArray */) → void); - js_::FunctionToJSExportedDartFunction|get#toJS((core::int arg1, core::String arg2, js_::JSArray /* erasure=_interceptors::JSArray */ arg3, js_::JSObject /* erasure=_interceptors::JSObject */ arg4, js_::JSPromise /* erasure=_interceptors::JSObject */ arg5) → js_::JSPromise /* erasure=_interceptors::JSObject */ => arg5); - js_::FunctionToJSExportedDartFunction|get#toJS(((core::int arg1, core::String arg2, [js_::JSArray? /* erasure=_interceptors::JSArray? */ arg3 = #C1, js_::JSObject? /* erasure=_interceptors::JSObject? */ arg4 = #C1, js_::JSPromise? /* erasure=_interceptors::JSObject? */ arg5 = #C1]) → js_::JSPromise? /* erasure=_interceptors::JSObject? */ => arg5) as (core::int, core::String, [js_::JSArray? /* erasure=_interceptors::JSArray? */, js_::JSObject? /* erasure=_interceptors::JSObject? */]) → void); - js_::FunctionToJSExportedDartFunction|get#toJS((core::int arg1, core::String arg2, js_::JSArray /* erasure=_interceptors::JSArray */ arg3, js_::JSObject /* erasure=_interceptors::JSObject */ arg4, js_::JSPromise /* erasure=_interceptors::JSObject */ arg5, js_::JSAny /* erasure=core::Object */ arg6) → js_::JSAny /* erasure=core::Object */ => arg6); - js_::FunctionToJSExportedDartFunction|get#toJS(((core::int arg1, core::String arg2, js_::JSArray /* erasure=_interceptors::JSArray */ arg3, js_::JSObject /* erasure=_interceptors::JSObject */ arg4, [js_::JSPromise? /* erasure=_interceptors::JSObject? */ arg5 = #C1, js_::JSAny? /* erasure=core::Object? */ arg6 = #C1]) → js_::JSAny? /* erasure=core::Object? */ => arg6) as (core::int, core::String, js_::JSArray /* erasure=_interceptors::JSArray */, js_::JSObject /* erasure=_interceptors::JSObject */, [js_::JSPromise? /* erasure=_interceptors::JSObject? */]) → void); - js_::JSExportedDartFunctionToFunction|get#toDart(jsFunction); + #lib1::JSExportedDartFunction /* erasure=_interceptors::JavaScriptFunction */ jsFunction = js_::_functionToJS0(() → Null {}); + js_::_functionToJS1((core::int arg1) → core::int => arg1); + js_::_functionToJS0((([core::int? arg1 = #C1]) → core::int? => arg1) as () → void); + js_::_functionToJS2((core::int arg1, core::String arg2) → core::String => arg2); + js_::_functionToJS1((([core::int? arg1 = #C1, core::String? arg2 = #C1]) → core::String? => arg2) as ([core::int]) → void); + js_::_functionToJS3((core::int arg1, core::String arg2, #lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */> /* erasure=_interceptors::JSArray */ arg3) → #lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */> /* erasure=_interceptors::JSArray */ => arg3); + js_::_functionToJS2(((core::int arg1, [core::String? arg2 = #C1, #lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */>? /* erasure=_interceptors::JSArray? */ arg3 = #C1]) → #lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */>? /* erasure=_interceptors::JSArray? */ => arg3) as (core::int, [core::String]) → void); + js_::_functionToJS4((core::int arg1, core::String arg2, #lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */> /* erasure=_interceptors::JSArray */ arg3, #lib1::JSObject /* erasure=_interceptors::JSObject */ arg4) → #lib1::JSObject /* erasure=_interceptors::JSObject */ => arg4); + js_::_functionToJS3(((core::int arg1, core::String arg2, #lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */> /* erasure=_interceptors::JSArray */ arg3, [#lib1::JSObject? /* erasure=_interceptors::JSObject? */ arg4 = #C1]) → #lib1::JSObject? /* erasure=_interceptors::JSObject? */ => arg4) as (core::int, core::String, #lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */> /* erasure=_interceptors::JSArray */) → void); + js_::_functionToJS5((core::int arg1, core::String arg2, #lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */> /* erasure=_interceptors::JSArray */ arg3, #lib1::JSObject /* erasure=_interceptors::JSObject */ arg4, #lib1::JSPromise<#lib1::JSAny? /* erasure=core::Object? */> /* erasure=_interceptors::JSObject */ arg5) → #lib1::JSPromise<#lib1::JSAny? /* erasure=core::Object? */> /* erasure=_interceptors::JSObject */ => arg5); + js_::_functionToJS4(((core::int arg1, core::String arg2, [#lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */>? /* erasure=_interceptors::JSArray? */ arg3 = #C1, #lib1::JSObject? /* erasure=_interceptors::JSObject? */ arg4 = #C1, #lib1::JSPromise<#lib1::JSAny? /* erasure=core::Object? */>? /* erasure=_interceptors::JSObject? */ arg5 = #C1]) → #lib1::JSPromise<#lib1::JSAny? /* erasure=core::Object? */>? /* erasure=_interceptors::JSObject? */ => arg5) as (core::int, core::String, [#lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */>? /* erasure=_interceptors::JSArray? */, #lib1::JSObject? /* erasure=_interceptors::JSObject? */]) → void); + js_::_functionToJSN((core::int arg1, core::String arg2, #lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */> /* erasure=_interceptors::JSArray */ arg3, #lib1::JSObject /* erasure=_interceptors::JSObject */ arg4, #lib1::JSPromise<#lib1::JSAny? /* erasure=core::Object? */> /* erasure=_interceptors::JSObject */ arg5, #lib1::JSAny /* erasure=core::Object */ arg6) → #lib1::JSAny /* erasure=core::Object */ => arg6, 6); + js_::_functionToJS5(((core::int arg1, core::String arg2, #lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */> /* erasure=_interceptors::JSArray */ arg3, #lib1::JSObject /* erasure=_interceptors::JSObject */ arg4, [#lib1::JSPromise<#lib1::JSAny? /* erasure=core::Object? */>? /* erasure=_interceptors::JSObject? */ arg5 = #C1, #lib1::JSAny? /* erasure=core::Object? */ arg6 = #C1]) → #lib1::JSAny? /* erasure=core::Object? */ => arg6) as (core::int, core::String, #lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */> /* erasure=_interceptors::JSArray */, #lib1::JSObject /* erasure=_interceptors::JSObject */, [#lib1::JSPromise<#lib1::JSAny? /* erasure=core::Object? */>? /* erasure=_interceptors::JSObject? */]) → void); + js_::_jsFunctionToDart(jsFunction); } constants { diff --git a/pkg/front_end/testcases/dart2js/js_interop_transforms/functiontojs.dart.weak.transformed.expect b/pkg/front_end/testcases/dart2js/js_interop_transforms/functiontojs.dart.weak.transformed.expect index 81d2853c428..fadb6a53a3f 100644 --- a/pkg/front_end/testcases/dart2js/js_interop_transforms/functiontojs.dart.weak.transformed.expect +++ b/pkg/front_end/testcases/dart2js/js_interop_transforms/functiontojs.dart.weak.transformed.expect @@ -1,25 +1,25 @@ library; import self as self; -import "dart:js_interop" as js_; +import "dart:js_util" as js_; import "dart:core" as core; import "dart:js_interop"; static method main() → void { - js_::JSExportedDartFunction /* erasure=_interceptors::JavaScriptFunction */ jsFunction = js_::FunctionToJSExportedDartFunction|get#toJS(() → Null {}); - js_::FunctionToJSExportedDartFunction|get#toJS((core::int arg1) → core::int => arg1); - js_::FunctionToJSExportedDartFunction|get#toJS((([core::int? arg1 = #C1]) → core::int? => arg1) as () → void); - js_::FunctionToJSExportedDartFunction|get#toJS((core::int arg1, core::String arg2) → core::String => arg2); - js_::FunctionToJSExportedDartFunction|get#toJS((([core::int? arg1 = #C1, core::String? arg2 = #C1]) → core::String? => arg2) as ([core::int]) → void); - js_::FunctionToJSExportedDartFunction|get#toJS((core::int arg1, core::String arg2, js_::JSArray /* erasure=_interceptors::JSArray */ arg3) → js_::JSArray /* erasure=_interceptors::JSArray */ => arg3); - js_::FunctionToJSExportedDartFunction|get#toJS(((core::int arg1, [core::String? arg2 = #C1, js_::JSArray? /* erasure=_interceptors::JSArray? */ arg3 = #C1]) → js_::JSArray? /* erasure=_interceptors::JSArray? */ => arg3) as (core::int, [core::String]) → void); - js_::FunctionToJSExportedDartFunction|get#toJS((core::int arg1, core::String arg2, js_::JSArray /* erasure=_interceptors::JSArray */ arg3, js_::JSObject /* erasure=_interceptors::JSObject */ arg4) → js_::JSObject /* erasure=_interceptors::JSObject */ => arg4); - js_::FunctionToJSExportedDartFunction|get#toJS(((core::int arg1, core::String arg2, js_::JSArray /* erasure=_interceptors::JSArray */ arg3, [js_::JSObject? /* erasure=_interceptors::JSObject? */ arg4 = #C1]) → js_::JSObject? /* erasure=_interceptors::JSObject? */ => arg4) as (core::int, core::String, js_::JSArray /* erasure=_interceptors::JSArray */) → void); - js_::FunctionToJSExportedDartFunction|get#toJS((core::int arg1, core::String arg2, js_::JSArray /* erasure=_interceptors::JSArray */ arg3, js_::JSObject /* erasure=_interceptors::JSObject */ arg4, js_::JSPromise /* erasure=_interceptors::JSObject */ arg5) → js_::JSPromise /* erasure=_interceptors::JSObject */ => arg5); - js_::FunctionToJSExportedDartFunction|get#toJS(((core::int arg1, core::String arg2, [js_::JSArray? /* erasure=_interceptors::JSArray? */ arg3 = #C1, js_::JSObject? /* erasure=_interceptors::JSObject? */ arg4 = #C1, js_::JSPromise? /* erasure=_interceptors::JSObject? */ arg5 = #C1]) → js_::JSPromise? /* erasure=_interceptors::JSObject? */ => arg5) as (core::int, core::String, [js_::JSArray? /* erasure=_interceptors::JSArray? */, js_::JSObject? /* erasure=_interceptors::JSObject? */]) → void); - js_::FunctionToJSExportedDartFunction|get#toJS((core::int arg1, core::String arg2, js_::JSArray /* erasure=_interceptors::JSArray */ arg3, js_::JSObject /* erasure=_interceptors::JSObject */ arg4, js_::JSPromise /* erasure=_interceptors::JSObject */ arg5, js_::JSAny /* erasure=core::Object */ arg6) → js_::JSAny /* erasure=core::Object */ => arg6); - js_::FunctionToJSExportedDartFunction|get#toJS(((core::int arg1, core::String arg2, js_::JSArray /* erasure=_interceptors::JSArray */ arg3, js_::JSObject /* erasure=_interceptors::JSObject */ arg4, [js_::JSPromise? /* erasure=_interceptors::JSObject? */ arg5 = #C1, js_::JSAny? /* erasure=core::Object? */ arg6 = #C1]) → js_::JSAny? /* erasure=core::Object? */ => arg6) as (core::int, core::String, js_::JSArray /* erasure=_interceptors::JSArray */, js_::JSObject /* erasure=_interceptors::JSObject */, [js_::JSPromise? /* erasure=_interceptors::JSObject? */]) → void); - js_::JSExportedDartFunctionToFunction|get#toDart(jsFunction); + #lib1::JSExportedDartFunction /* erasure=_interceptors::JavaScriptFunction */ jsFunction = js_::_functionToJS0(() → Null {}); + js_::_functionToJS1((core::int arg1) → core::int => arg1); + js_::_functionToJS0((([core::int? arg1 = #C1]) → core::int? => arg1) as () → void); + js_::_functionToJS2((core::int arg1, core::String arg2) → core::String => arg2); + js_::_functionToJS1((([core::int? arg1 = #C1, core::String? arg2 = #C1]) → core::String? => arg2) as ([core::int]) → void); + js_::_functionToJS3((core::int arg1, core::String arg2, #lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */> /* erasure=_interceptors::JSArray */ arg3) → #lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */> /* erasure=_interceptors::JSArray */ => arg3); + js_::_functionToJS2(((core::int arg1, [core::String? arg2 = #C1, #lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */>? /* erasure=_interceptors::JSArray? */ arg3 = #C1]) → #lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */>? /* erasure=_interceptors::JSArray? */ => arg3) as (core::int, [core::String]) → void); + js_::_functionToJS4((core::int arg1, core::String arg2, #lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */> /* erasure=_interceptors::JSArray */ arg3, #lib1::JSObject /* erasure=_interceptors::JSObject */ arg4) → #lib1::JSObject /* erasure=_interceptors::JSObject */ => arg4); + js_::_functionToJS3(((core::int arg1, core::String arg2, #lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */> /* erasure=_interceptors::JSArray */ arg3, [#lib1::JSObject? /* erasure=_interceptors::JSObject? */ arg4 = #C1]) → #lib1::JSObject? /* erasure=_interceptors::JSObject? */ => arg4) as (core::int, core::String, #lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */> /* erasure=_interceptors::JSArray */) → void); + js_::_functionToJS5((core::int arg1, core::String arg2, #lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */> /* erasure=_interceptors::JSArray */ arg3, #lib1::JSObject /* erasure=_interceptors::JSObject */ arg4, #lib1::JSPromise<#lib1::JSAny? /* erasure=core::Object? */> /* erasure=_interceptors::JSObject */ arg5) → #lib1::JSPromise<#lib1::JSAny? /* erasure=core::Object? */> /* erasure=_interceptors::JSObject */ => arg5); + js_::_functionToJS4(((core::int arg1, core::String arg2, [#lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */>? /* erasure=_interceptors::JSArray? */ arg3 = #C1, #lib1::JSObject? /* erasure=_interceptors::JSObject? */ arg4 = #C1, #lib1::JSPromise<#lib1::JSAny? /* erasure=core::Object? */>? /* erasure=_interceptors::JSObject? */ arg5 = #C1]) → #lib1::JSPromise<#lib1::JSAny? /* erasure=core::Object? */>? /* erasure=_interceptors::JSObject? */ => arg5) as (core::int, core::String, [#lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */>? /* erasure=_interceptors::JSArray? */, #lib1::JSObject? /* erasure=_interceptors::JSObject? */]) → void); + js_::_functionToJSN((core::int arg1, core::String arg2, #lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */> /* erasure=_interceptors::JSArray */ arg3, #lib1::JSObject /* erasure=_interceptors::JSObject */ arg4, #lib1::JSPromise<#lib1::JSAny? /* erasure=core::Object? */> /* erasure=_interceptors::JSObject */ arg5, #lib1::JSAny /* erasure=core::Object */ arg6) → #lib1::JSAny /* erasure=core::Object */ => arg6, 6); + js_::_functionToJS5(((core::int arg1, core::String arg2, #lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */> /* erasure=_interceptors::JSArray */ arg3, #lib1::JSObject /* erasure=_interceptors::JSObject */ arg4, [#lib1::JSPromise<#lib1::JSAny? /* erasure=core::Object? */>? /* erasure=_interceptors::JSObject? */ arg5 = #C1, #lib1::JSAny? /* erasure=core::Object? */ arg6 = #C1]) → #lib1::JSAny? /* erasure=core::Object? */ => arg6) as (core::int, core::String, #lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */> /* erasure=_interceptors::JSArray */, #lib1::JSObject /* erasure=_interceptors::JSObject */, [#lib1::JSPromise<#lib1::JSAny? /* erasure=core::Object? */>? /* erasure=_interceptors::JSObject? */]) → void); + js_::_jsFunctionToDart(jsFunction); } constants { diff --git a/pkg/front_end/testcases/dartdevc/js_interop_transforms/functiontojs.dart.strong.transformed.expect b/pkg/front_end/testcases/dartdevc/js_interop_transforms/functiontojs.dart.strong.transformed.expect index d4eb06f56e7..fb798b1460c 100644 --- a/pkg/front_end/testcases/dartdevc/js_interop_transforms/functiontojs.dart.strong.transformed.expect +++ b/pkg/front_end/testcases/dartdevc/js_interop_transforms/functiontojs.dart.strong.transformed.expect @@ -1,25 +1,25 @@ library; import self as self; -import "dart:js_interop" as js_; +import "dart:js_util" as js_; import "dart:core" as core; import "dart:js_interop"; static method main() → void { - js_::JSExportedDartFunction /* erasure=dart._interceptors::JavaScriptFunction */ jsFunction = js_::FunctionToJSExportedDartFunction|get#toJS(() → Null {}); - js_::FunctionToJSExportedDartFunction|get#toJS((core::int arg1) → core::int => arg1); - js_::FunctionToJSExportedDartFunction|get#toJS((([core::int? arg1 = #C1]) → core::int? => arg1) as () → void); - js_::FunctionToJSExportedDartFunction|get#toJS((core::int arg1, core::String arg2) → core::String => arg2); - js_::FunctionToJSExportedDartFunction|get#toJS((([core::int? arg1 = #C1, core::String? arg2 = #C1]) → core::String? => arg2) as ([core::int]) → void); - js_::FunctionToJSExportedDartFunction|get#toJS((core::int arg1, core::String arg2, js_::JSArray /* erasure=dart._interceptors::JSArray */ arg3) → js_::JSArray /* erasure=dart._interceptors::JSArray */ => arg3); - js_::FunctionToJSExportedDartFunction|get#toJS(((core::int arg1, [core::String? arg2 = #C1, js_::JSArray? /* erasure=dart._interceptors::JSArray? */ arg3 = #C1]) → js_::JSArray? /* erasure=dart._interceptors::JSArray? */ => arg3) as (core::int, [core::String]) → void); - js_::FunctionToJSExportedDartFunction|get#toJS((core::int arg1, core::String arg2, js_::JSArray /* erasure=dart._interceptors::JSArray */ arg3, js_::JSObject /* erasure=dart._interceptors::JSObject */ arg4) → js_::JSObject /* erasure=dart._interceptors::JSObject */ => arg4); - js_::FunctionToJSExportedDartFunction|get#toJS(((core::int arg1, core::String arg2, js_::JSArray /* erasure=dart._interceptors::JSArray */ arg3, [js_::JSObject? /* erasure=dart._interceptors::JSObject? */ arg4 = #C1]) → js_::JSObject? /* erasure=dart._interceptors::JSObject? */ => arg4) as (core::int, core::String, js_::JSArray /* erasure=dart._interceptors::JSArray */) → void); - js_::FunctionToJSExportedDartFunction|get#toJS((core::int arg1, core::String arg2, js_::JSArray /* erasure=dart._interceptors::JSArray */ arg3, js_::JSObject /* erasure=dart._interceptors::JSObject */ arg4, js_::JSPromise /* erasure=dart._interceptors::JSObject */ arg5) → js_::JSPromise /* erasure=dart._interceptors::JSObject */ => arg5); - js_::FunctionToJSExportedDartFunction|get#toJS(((core::int arg1, core::String arg2, [js_::JSArray? /* erasure=dart._interceptors::JSArray? */ arg3 = #C1, js_::JSObject? /* erasure=dart._interceptors::JSObject? */ arg4 = #C1, js_::JSPromise? /* erasure=dart._interceptors::JSObject? */ arg5 = #C1]) → js_::JSPromise? /* erasure=dart._interceptors::JSObject? */ => arg5) as (core::int, core::String, [js_::JSArray? /* erasure=dart._interceptors::JSArray? */, js_::JSObject? /* erasure=dart._interceptors::JSObject? */]) → void); - js_::FunctionToJSExportedDartFunction|get#toJS((core::int arg1, core::String arg2, js_::JSArray /* erasure=dart._interceptors::JSArray */ arg3, js_::JSObject /* erasure=dart._interceptors::JSObject */ arg4, js_::JSPromise /* erasure=dart._interceptors::JSObject */ arg5, js_::JSAny /* erasure=core::Object */ arg6) → js_::JSAny /* erasure=core::Object */ => arg6); - js_::FunctionToJSExportedDartFunction|get#toJS(((core::int arg1, core::String arg2, js_::JSArray /* erasure=dart._interceptors::JSArray */ arg3, js_::JSObject /* erasure=dart._interceptors::JSObject */ arg4, [js_::JSPromise? /* erasure=dart._interceptors::JSObject? */ arg5 = #C1, js_::JSAny? /* erasure=core::Object? */ arg6 = #C1]) → js_::JSAny? /* erasure=core::Object? */ => arg6) as (core::int, core::String, js_::JSArray /* erasure=dart._interceptors::JSArray */, js_::JSObject /* erasure=dart._interceptors::JSObject */, [js_::JSPromise? /* erasure=dart._interceptors::JSObject? */]) → void); - js_::JSExportedDartFunctionToFunction|get#toDart(jsFunction); + #lib1::JSExportedDartFunction /* erasure=dart._interceptors::JavaScriptFunction */ jsFunction = js_::_functionToJS0(() → Null {}); + js_::_functionToJS1((core::int arg1) → core::int => arg1); + js_::_functionToJS0((([core::int? arg1 = #C1]) → core::int? => arg1) as () → void); + js_::_functionToJS2((core::int arg1, core::String arg2) → core::String => arg2); + js_::_functionToJS1((([core::int? arg1 = #C1, core::String? arg2 = #C1]) → core::String? => arg2) as ([core::int]) → void); + js_::_functionToJS3((core::int arg1, core::String arg2, #lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */> /* erasure=dart._interceptors::JSArray */ arg3) → #lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */> /* erasure=dart._interceptors::JSArray */ => arg3); + js_::_functionToJS2(((core::int arg1, [core::String? arg2 = #C1, #lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */>? /* erasure=dart._interceptors::JSArray? */ arg3 = #C1]) → #lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */>? /* erasure=dart._interceptors::JSArray? */ => arg3) as (core::int, [core::String]) → void); + js_::_functionToJS4((core::int arg1, core::String arg2, #lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */> /* erasure=dart._interceptors::JSArray */ arg3, #lib1::JSObject /* erasure=dart._interceptors::JSObject */ arg4) → #lib1::JSObject /* erasure=dart._interceptors::JSObject */ => arg4); + js_::_functionToJS3(((core::int arg1, core::String arg2, #lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */> /* erasure=dart._interceptors::JSArray */ arg3, [#lib1::JSObject? /* erasure=dart._interceptors::JSObject? */ arg4 = #C1]) → #lib1::JSObject? /* erasure=dart._interceptors::JSObject? */ => arg4) as (core::int, core::String, #lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */> /* erasure=dart._interceptors::JSArray */) → void); + js_::_functionToJS5((core::int arg1, core::String arg2, #lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */> /* erasure=dart._interceptors::JSArray */ arg3, #lib1::JSObject /* erasure=dart._interceptors::JSObject */ arg4, #lib1::JSPromise<#lib1::JSAny? /* erasure=core::Object? */> /* erasure=dart._interceptors::JSObject */ arg5) → #lib1::JSPromise<#lib1::JSAny? /* erasure=core::Object? */> /* erasure=dart._interceptors::JSObject */ => arg5); + js_::_functionToJS4(((core::int arg1, core::String arg2, [#lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */>? /* erasure=dart._interceptors::JSArray? */ arg3 = #C1, #lib1::JSObject? /* erasure=dart._interceptors::JSObject? */ arg4 = #C1, #lib1::JSPromise<#lib1::JSAny? /* erasure=core::Object? */>? /* erasure=dart._interceptors::JSObject? */ arg5 = #C1]) → #lib1::JSPromise<#lib1::JSAny? /* erasure=core::Object? */>? /* erasure=dart._interceptors::JSObject? */ => arg5) as (core::int, core::String, [#lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */>? /* erasure=dart._interceptors::JSArray? */, #lib1::JSObject? /* erasure=dart._interceptors::JSObject? */]) → void); + js_::_functionToJSN((core::int arg1, core::String arg2, #lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */> /* erasure=dart._interceptors::JSArray */ arg3, #lib1::JSObject /* erasure=dart._interceptors::JSObject */ arg4, #lib1::JSPromise<#lib1::JSAny? /* erasure=core::Object? */> /* erasure=dart._interceptors::JSObject */ arg5, #lib1::JSAny /* erasure=core::Object */ arg6) → #lib1::JSAny /* erasure=core::Object */ => arg6, 6); + js_::_functionToJS5(((core::int arg1, core::String arg2, #lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */> /* erasure=dart._interceptors::JSArray */ arg3, #lib1::JSObject /* erasure=dart._interceptors::JSObject */ arg4, [#lib1::JSPromise<#lib1::JSAny? /* erasure=core::Object? */>? /* erasure=dart._interceptors::JSObject? */ arg5 = #C1, #lib1::JSAny? /* erasure=core::Object? */ arg6 = #C1]) → #lib1::JSAny? /* erasure=core::Object? */ => arg6) as (core::int, core::String, #lib1::JSArray<#lib1::JSAny? /* erasure=core::Object? */> /* erasure=dart._interceptors::JSArray */, #lib1::JSObject /* erasure=dart._interceptors::JSObject */, [#lib1::JSPromise<#lib1::JSAny? /* erasure=core::Object? */>? /* erasure=dart._interceptors::JSObject? */]) → void); + js_::_jsFunctionToDart(jsFunction); } constants { diff --git a/sdk/lib/_internal/js_dev_runtime/patch/js_allow_interop_patch.dart b/sdk/lib/_internal/js_dev_runtime/patch/js_allow_interop_patch.dart index b3bfee5219d..87e396448b4 100644 --- a/sdk/lib/_internal/js_dev_runtime/patch/js_allow_interop_patch.dart +++ b/sdk/lib/_internal/js_dev_runtime/patch/js_allow_interop_patch.dart @@ -4,6 +4,7 @@ // Patch file for dart:js_util library. import 'dart:_foreign_helper' show JS; +import 'dart:_interceptors' show JavaScriptFunction; import 'dart:_internal' show patch; import 'dart:_runtime' as dart; @@ -46,3 +47,254 @@ Function allowInteropCaptureThis(Function f) { } return ret; } + +// TODO(srujzs): In dart2js, this is guaranteed to be unique per isolate. DDC +// doesn't have a mechanism to guarantee that, so use a Symbol instead to match +// the unique-per-runtime semantics of [allowInterop]. +final _functionToJSPropertyName = r'_$dart_dartClosure'; +final _functionToJSProperty = JS('!', "Symbol($_functionToJSPropertyName)"); + +JavaScriptFunction _functionToJS0(Function f) { + // This can only happen if a user casted a JavaScriptFunction to Function. + // Such a cast is an error in dart2wasm, so we should make this behavior an + // error as well. + if (!dart.isDartFunction(f)) { + throw ArgumentError('Attempting to rewrap a JS function.'); + } + final ret = JS( + '!', + ''' + function () { + return #(#); + } + ''', + _callDartFunctionFast0, + f); + JS('', '#[#] = #', ret, _functionToJSProperty, f); + return ret; +} + +JavaScriptFunction _functionToJS1(Function f) { + if (!dart.isDartFunction(f)) { + throw ArgumentError('Attempting to rewrap a JS function.'); + } + final ret = JS( + '!', + ''' + function (arg1) { + return #(#, arg1, arguments.length); + } + ''', + _callDartFunctionFast1, + f); + JS('', '#[#] = #', ret, _functionToJSProperty, f); + return ret; +} + +JavaScriptFunction _functionToJS2(Function f) { + if (!dart.isDartFunction(f)) { + throw ArgumentError('Attempting to rewrap a JS function.'); + } + final ret = JS( + '!', + ''' + function (arg1, arg2) { + return #(#, arg1, arg2, arguments.length); + } + ''', + _callDartFunctionFast2, + f); + JS('', '#[#] = #', ret, _functionToJSProperty, f); + return ret; +} + +JavaScriptFunction _functionToJS3(Function f) { + if (!dart.isDartFunction(f)) { + throw ArgumentError('Attempting to rewrap a JS function.'); + } + final ret = JS( + '!', + ''' + function (arg1, arg2, arg3) { + return #(#, arg1, arg2, arg3, arguments.length); + } + ''', + _callDartFunctionFast3, + f); + JS('', '#[#] = #', ret, _functionToJSProperty, f); + return ret; +} + +JavaScriptFunction _functionToJS4(Function f) { + if (!dart.isDartFunction(f)) { + throw ArgumentError('Attempting to rewrap a JS function.'); + } + final ret = JS( + '!', + ''' + function (arg1, arg2, arg3, arg4) { + return #(#, arg1, arg2, arg3, arg4, arguments.length); + } + ''', + _callDartFunctionFast4, + f); + JS('', '#[#] = #', ret, _functionToJSProperty, f); + return ret; +} + +JavaScriptFunction _functionToJS5(Function f) { + if (!dart.isDartFunction(f)) { + throw ArgumentError('Attempting to rewrap a JS function.'); + } + final ret = JS( + '!', + ''' + function (arg1, arg2, arg3, arg4, arg5) { + return #(#, arg1, arg2, arg3, arg4, arg5, arguments.length); + } + ''', + _callDartFunctionFast5, + f); + JS('', '#[#] = #', ret, _functionToJSProperty, f); + return ret; +} + +JavaScriptFunction _functionToJSN(Function f, int maxLength) { + if (!dart.isDartFunction(f)) { + throw ArgumentError('Attempting to rewrap a JS function.'); + } + final ret = JS( + '!', + ''' + function (...args) { + return #(#, Array.prototype.slice.call(args, 0, + Math.min(args.length, #))); + } + ''', + dart.dcall, + f, + maxLength); + JS('', '#[#] = #', ret, _functionToJSProperty, f); + return ret; +} + +_callDartFunctionFast0(callback) => JS('', '#()', callback); + +_callDartFunctionFast1(callback, arg1, int length) { + if (length >= 1) { + final error = + dart.validateFunctionToJSArgs(callback, JS('!', '[#]', arg1)); + if (error != null) return error; + return JS('', '#(#)', callback, arg1); + } else { + final error = dart.validateFunctionToJSArgs(callback, JS('!', '[]')); + if (error != null) return error; + return JS('', '#()', callback); + } +} + +_callDartFunctionFast2(callback, arg1, arg2, int length) { + if (length >= 2) { + final error = dart.validateFunctionToJSArgs( + callback, JS('!', '[#, #]', arg1, arg2)); + if (error != null) return error; + return JS('', '#(#, #)', callback, arg1, arg2); + } else if (length == 1) { + final error = + dart.validateFunctionToJSArgs(callback, JS('!', '[#]', arg1)); + if (error != null) return error; + return JS('', '#(#)', callback, arg1); + } else { + final error = dart.validateFunctionToJSArgs(callback, JS('!', '[]')); + if (error != null) return error; + return JS('', '#()', callback); + } +} + +_callDartFunctionFast3(callback, arg1, arg2, arg3, int length) { + if (length >= 3) { + final error = dart.validateFunctionToJSArgs( + callback, JS('!', '[#, #, #]', arg1, arg2, arg3)); + if (error != null) return error; + return JS('', '#(#, #, #)', callback, arg1, arg2, arg3); + } else if (length == 2) { + final error = dart.validateFunctionToJSArgs( + callback, JS('!', '[#, #]', arg1, arg2)); + if (error != null) return error; + return JS('', '#(#, #)', callback, arg1, arg2); + } else if (length == 1) { + final error = + dart.validateFunctionToJSArgs(callback, JS('!', '[#]', arg1)); + if (error != null) return error; + return JS('', '#(#)', callback, arg1); + } else { + final error = dart.validateFunctionToJSArgs(callback, JS('!', '[]')); + if (error != null) return error; + return JS('', '#()', callback); + } +} + +_callDartFunctionFast4(callback, arg1, arg2, arg3, arg4, int length) { + if (length >= 4) { + final error = dart.validateFunctionToJSArgs( + callback, JS('!', '[#, #, #, #]', arg1, arg2, arg3, arg4)); + if (error != null) return error; + return JS('', '#(#, #, #, #)', callback, arg1, arg2, arg3, arg4); + } else if (length == 3) { + final error = dart.validateFunctionToJSArgs( + callback, JS('!', '[#, #, #]', arg1, arg2, arg3)); + if (error != null) return error; + return JS('', '#(#, #, #)', callback, arg1, arg2, arg3); + } else if (length == 2) { + final error = dart.validateFunctionToJSArgs( + callback, JS('!', '[#, #]', arg1, arg2)); + if (error != null) return error; + return JS('', '#(#, #)', callback, arg1, arg2); + } else if (length == 1) { + final error = + dart.validateFunctionToJSArgs(callback, JS('!', '[#]', arg1)); + if (error != null) return error; + return JS('', '#(#)', callback, arg1); + } else { + final error = dart.validateFunctionToJSArgs(callback, JS('!', '[]')); + if (error != null) return error; + return JS('', '#()', callback); + } +} + +_callDartFunctionFast5(callback, arg1, arg2, arg3, arg4, arg5, int length) { + if (length >= 5) { + final error = dart.validateFunctionToJSArgs(callback, + JS('!', '[#, #, #, #, #]', arg1, arg2, arg3, arg4, arg5)); + if (error != null) return error; + return JS('', '#(#, #, #, #, #)', callback, arg1, arg2, arg3, arg4, arg5); + } else if (length == 4) { + final error = dart.validateFunctionToJSArgs( + callback, JS('!', '[#, #, #, #]', arg1, arg2, arg3, arg4)); + if (error != null) return error; + return JS('', '#(#, #, #, #)', callback, arg1, arg2, arg3, arg4); + } else if (length == 3) { + final error = dart.validateFunctionToJSArgs( + callback, JS('!', '[#, #, #]', arg1, arg2, arg3)); + if (error != null) return error; + return JS('', '#(#, #, #)', callback, arg1, arg2, arg3); + } else if (length == 2) { + final error = dart.validateFunctionToJSArgs( + callback, JS('!', '[#, #]', arg1, arg2)); + if (error != null) return error; + return JS('', '#(#, #)', callback, arg1, arg2); + } else if (length == 1) { + final error = + dart.validateFunctionToJSArgs(callback, JS('!', '[#]', arg1)); + if (error != null) return error; + return JS('', '#(#)', callback, arg1); + } else { + final error = dart.validateFunctionToJSArgs(callback, JS('!', '[]')); + if (error != null) return error; + return JS('', '#()', callback); + } +} + +Function _jsFunctionToDart(JavaScriptFunction f) { + return JS('Function', '#[#]', f, _functionToJSProperty); +} diff --git a/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart b/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart index 6b15d72ec63..f4bb7e850ac 100644 --- a/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart +++ b/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart @@ -428,6 +428,26 @@ _checkAndCall(f, ftype, obj, typeArgs, args, named, displayName) { return callNSM(errorMessage); } +/// Given a Dart function [f] that was wrapped in a `Function.toJS` call, and +/// the corresponding [args] used to call it, validates that the arity and types +/// of [args] are correct. +/// +/// Returns null if it's valid call and a [noSuchMethod] invocation with the +/// specific error otherwise. +validateFunctionToJSArgs(f, List args) { + var errorMessage = _argumentErrors( + JS('', '#[#]', f, JS_GET_NAME(JsGetName.SIGNATURE_NAME)), + args, + null); + if (errorMessage != null) { + return noSuchMethod( + f, + InvocationImpl(JS('', 'f.name'), args, + isMethod: true, failureMessage: errorMessage)); + } + return null; +} + dcall(f, args, [@undefined named]) => _checkAndCall( f, null, JS('', 'void 0'), null, args, named, JS('', 'f.name')); diff --git a/sdk/lib/_internal/js_runtime/lib/js_allow_interop_patch.dart b/sdk/lib/_internal/js_runtime/lib/js_allow_interop_patch.dart index 831cd4911b2..0fcf4ab6224 100644 --- a/sdk/lib/_internal/js_runtime/lib/js_allow_interop_patch.dart +++ b/sdk/lib/_internal/js_runtime/lib/js_allow_interop_patch.dart @@ -5,7 +5,7 @@ // Patch file for dart:js_util library. import 'dart:_foreign_helper' show JS, DART_CLOSURE_TO_JS; -import 'dart:_interceptors' show DART_CLOSURE_PROPERTY_NAME; +import 'dart:_interceptors' show DART_CLOSURE_PROPERTY_NAME, JavaScriptFunction; import 'dart:_internal' show patch; import 'dart:_js_helper' show @@ -80,3 +80,187 @@ Function allowInteropCaptureThis(Function f) { return _convertDartFunctionFastCaptureThis(f); } } + +JavaScriptFunction _functionToJS0(Function f) { + // This can only happen if a user casted a JavaScriptFunction to Function. + // Such a cast is an error in dart2wasm, so we should make this behavior an + // error as well. + if (isJSFunction(f)) { + throw ArgumentError('Attempting to rewrap a JS function.'); + } + final result = JS( + 'JavaScriptFunction', + ''' + function(_call, f) { + return function() { + return _call(f); + } + }(#, #) + ''', + DART_CLOSURE_TO_JS(_callDartFunctionFast0), + f); + JS('', '#.# = #', result, DART_CLOSURE_PROPERTY_NAME, f); + return result; +} + +JavaScriptFunction _functionToJS1(Function f) { + if (isJSFunction(f)) { + throw ArgumentError('Attempting to rewrap a JS function.'); + } + final result = JS( + 'JavaScriptFunction', + ''' + function(_call, f) { + return function(arg1) { + return _call(f, arg1, arguments.length); + } + }(#, #) + ''', + DART_CLOSURE_TO_JS(_callDartFunctionFast1), + f); + JS('', '#.# = #', result, DART_CLOSURE_PROPERTY_NAME, f); + return result; +} + +JavaScriptFunction _functionToJS2(Function f) { + if (isJSFunction(f)) { + throw ArgumentError('Attempting to rewrap a JS function.'); + } + final result = JS( + 'JavaScriptFunction', + ''' + function(_call, f) { + return function(arg1, arg2) { + return _call(f, arg1, arg2, arguments.length); + } + }(#, #) + ''', + DART_CLOSURE_TO_JS(_callDartFunctionFast2), + f); + JS('', '#.# = #', result, DART_CLOSURE_PROPERTY_NAME, f); + return result; +} + +JavaScriptFunction _functionToJS3(Function f) { + if (isJSFunction(f)) { + throw ArgumentError('Attempting to rewrap a JS function.'); + } + final result = JS( + 'JavaScriptFunction', + ''' + function(_call, f) { + return function(arg1, arg2, arg3) { + return _call(f, arg1, arg2, arg3, arguments.length); + } + }(#, #) + ''', + DART_CLOSURE_TO_JS(_callDartFunctionFast3), + f); + JS('', '#.# = #', result, DART_CLOSURE_PROPERTY_NAME, f); + return result; +} + +JavaScriptFunction _functionToJS4(Function f) { + if (isJSFunction(f)) { + throw ArgumentError('Attempting to rewrap a JS function.'); + } + final result = JS( + 'JavaScriptFunction', + ''' + function(_call, f) { + return function(arg1, arg2, arg3, arg4) { + return _call(f, arg1, arg2, arg3, arg4, arguments.length); + } + }(#, #) + ''', + DART_CLOSURE_TO_JS(_callDartFunctionFast4), + f); + JS('', '#.# = #', result, DART_CLOSURE_PROPERTY_NAME, f); + return result; +} + +JavaScriptFunction _functionToJS5(Function f) { + if (isJSFunction(f)) { + throw ArgumentError('Attempting to rewrap a JS function.'); + } + final result = JS( + 'JavaScriptFunction', + ''' + function(_call, f) { + return function(arg1, arg2, arg3, arg4, arg5) { + return _call(f, arg1, arg2, arg3, arg4, arg5, arguments.length); + } + }(#, #) + ''', + DART_CLOSURE_TO_JS(_callDartFunctionFast5), + f); + JS('', '#.# = #', result, DART_CLOSURE_PROPERTY_NAME, f); + return result; +} + +JavaScriptFunction _functionToJSN(Function f, int maxLength) { + if (isJSFunction(f)) { + throw ArgumentError('Attempting to rewrap a JS function.'); + } + final result = JS( + 'JavaScriptFunction', + ''' + function(_call, f, maxLength) { + return function () { + return _call(f, Array.prototype.slice.call(arguments, 0, + Math.min(arguments.length, maxLength))); + } + }(#, #, #) + ''', + DART_CLOSURE_TO_JS(_callDartFunctionFastN), + f, + maxLength); + JS('', '#.# = #', result, DART_CLOSURE_PROPERTY_NAME, f); + return result; +} + +_callDartFunctionFast0(Function callback) => callback(); + +_callDartFunctionFast1(Function callback, arg1, int length) { + if (length >= 1) return callback(arg1); + return callback(); +} + +_callDartFunctionFast2(Function callback, arg1, arg2, int length) { + if (length >= 2) return callback(arg1, arg2); + if (length == 1) return callback(arg1); + return callback(); +} + +_callDartFunctionFast3(Function callback, arg1, arg2, arg3, int length) { + if (length >= 3) return callback(arg1, arg2, arg3); + if (length == 2) return callback(arg1, arg2); + if (length == 1) return callback(arg1); + return callback(); +} + +_callDartFunctionFast4(Function callback, arg1, arg2, arg3, arg4, int length) { + if (length >= 4) return callback(arg1, arg2, arg3, arg4); + if (length == 3) return callback(arg1, arg2, arg3); + if (length == 2) return callback(arg1, arg2); + if (length == 1) return callback(arg1); + return callback(); +} + +_callDartFunctionFast5( + Function callback, arg1, arg2, arg3, arg4, arg5, int length) { + if (length >= 5) return callback(arg1, arg2, arg3, arg4, arg5); + if (length == 4) return callback(arg1, arg2, arg3, arg4); + if (length == 3) return callback(arg1, arg2, arg3); + if (length == 2) return callback(arg1, arg2); + if (length == 1) return callback(arg1); + return callback(); +} + +_callDartFunctionFastN(Function callback, List arguments) { + return Function.apply(callback, arguments); +} + +Function _jsFunctionToDart(JavaScriptFunction f) { + return JS('Function', '#.#', f, DART_CLOSURE_PROPERTY_NAME); +} diff --git a/sdk/lib/_internal/js_shared/lib/js_interop_patch.dart b/sdk/lib/_internal/js_shared/lib/js_interop_patch.dart index 9513deebf41..0a66d9cf1f7 100644 --- a/sdk/lib/_internal/js_shared/lib/js_interop_patch.dart +++ b/sdk/lib/_internal/js_shared/lib/js_interop_patch.dart @@ -65,19 +65,18 @@ extension NullableObjectUtilExtension on Object? { // JSExportedDartFunction <-> Function @patch extension JSExportedDartFunctionToFunction on JSExportedDartFunction { - // TODO(srujzs): We should unwrap rather than allow arbitrary JS functions - // to be called in Dart. @patch - @pragma('dart2js:prefer-inline') - Function get toDart => this._jsFunction; + Function get toDart => throw UnimplementedError( + "'toDart' should never directly be called. Calls to 'toDart' should have " + 'been transformed by the interop transformer.'); } @patch extension FunctionToJSExportedDartFunction on Function { @patch - @pragma('dart2js:prefer-inline') - JSExportedDartFunction get toJS => - js_util.allowInterop(this) as JSExportedDartFunction; + JSExportedDartFunction get toJS => throw UnimplementedError( + "'toJS' should never directly be called. Calls to 'toJS' should have " + 'been transformed by the interop transformer.'); } // Embedded global property for wrapped Dart objects passed via JS interop. diff --git a/sdk/lib/js_interop/js_interop.dart b/sdk/lib/js_interop/js_interop.dart index eb1b51b2822..5c3d1c3d38e 100644 --- a/sdk/lib/js_interop/js_interop.dart +++ b/sdk/lib/js_interop/js_interop.dart @@ -477,7 +477,8 @@ extension JSFunctionUtilExtension on JSFunction { extension JSExportedDartFunctionToFunction on JSExportedDartFunction { /// The Dart [Function] that this [JSExportedDartFunction] wrapped. /// - /// Must be a wrapped Dart [Function]. + /// Must be a function that was wrapped with + /// [FunctionToJSExportedDartFunction.toJS]. external Function get toDart; } @@ -490,6 +491,15 @@ extension FunctionToJSExportedDartFunction on Function { /// compile. See /// https://dart.dev/interop/js-interop/js-types#requirements-on-external-declarations-and-function-tojs /// for more details on what types are allowed. + /// + /// The max number of arguments that are passed to this [Function] from the + /// wrapper JavaScript function is determined by this [Function]'s static + /// type. Any extra arguments passed to the JavaScript function after the max + /// number of arguments are discarded like they are with regular JavaScript + /// functions. + /// + /// Calling this on the same [Function] again will always result in a new + /// JavaScript function. external JSExportedDartFunction get toJS; } diff --git a/tests/lib/js/static_interop_test/js_function_arity_test.dart b/tests/lib/js/static_interop_test/js_function_arity_test.dart new file mode 100644 index 00000000000..653438a925a --- /dev/null +++ b/tests/lib/js/static_interop_test/js_function_arity_test.dart @@ -0,0 +1,453 @@ +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/// Requirements=checked-implicit-downcasts + +import 'dart:js_interop'; + +import 'package:expect/expect.dart'; + +const isDDC = const bool.fromEnvironment('dart.library._ddc_only'); +const isDart2JS = const bool.fromEnvironment('dart.tool.dart2js'); + +@JS('call') +external String _call(JSFunction f, JSArray args); + +String call(JSFunction f, List args) => + _call(f, args.map((e) => e?.jsify()).toList().toJS); + +@JS() +external void eval(String code); + +// Zero. +String zeroArgs() => '0'; + +// One. +String oneRequired(String arg1) => arg1; +String oneOptional([String arg1 = 'default']) => '$arg1'; + +// Two. +String twoRequired(String arg1, String? arg2) => '$arg1$arg2'; +String oneRequiredOneOptional(String arg1, [String? arg2 = 'default']) => + '$arg1$arg2'; +String twoOptional([String arg1 = 'default', String? arg2 = 'default']) => + '$arg1$arg2'; + +// Three. +String threeRequired(String arg1, String? arg2, String arg3) => + '$arg1$arg2$arg3'; +String twoRequiredOneOptional(String arg1, String? arg2, + [String arg3 = 'default']) => + '$arg1$arg2$arg3'; +String oneRequiredTwoOptional(String arg1, + [String? arg2 = 'default', String arg3 = 'default']) => + '$arg1$arg2$arg3'; +String threeOptional( + [String arg1 = 'default', + String? arg2 = 'default', + String arg3 = 'default']) => + '$arg1$arg2$arg3'; + +// Four. +String fourRequired(String arg1, String? arg2, String arg3, String arg4) => + '$arg1$arg2$arg3$arg4'; +String threeRequiredOneOptional(String arg1, String? arg2, String arg3, + [String arg4 = 'default']) => + '$arg1$arg2$arg3$arg4'; +String twoRequiredTwoOptional(String arg1, String? arg2, + [String arg3 = 'default', String arg4 = 'default']) => + '$arg1$arg2$arg3$arg4'; +String oneRequiredThreeOptional(String arg1, + [String? arg2 = 'default', + String arg3 = 'default', + String arg4 = 'default']) => + '$arg1$arg2$arg3$arg4'; +String fourOptional( + [String arg1 = 'default', + String? arg2 = 'default', + String arg3 = 'default', + String arg4 = 'default']) => + '$arg1$arg2$arg3$arg4'; + +// Five. +String fiveRequired( + String arg1, String? arg2, String arg3, String arg4, String arg5) => + '$arg1$arg2$arg3$arg4$arg5'; +String fourRequiredOneOptional( + String arg1, String? arg2, String arg3, String arg4, + [String arg5 = 'default']) => + '$arg1$arg2$arg3$arg4$arg5'; +String threeRequiredTwoOptional(String arg1, String? arg2, String arg3, + [String arg4 = 'default', String arg5 = 'default']) => + '$arg1$arg2$arg3$arg4$arg5'; +String twoRequiredThreeOptional(String arg1, String? arg2, + [String arg3 = 'default', + String arg4 = 'default', + String arg5 = 'default']) => + '$arg1$arg2$arg3$arg4$arg5'; +String oneRequiredFourOptional(String arg1, + [String? arg2 = 'default', + String arg3 = 'default', + String arg4 = 'default', + String arg5 = 'default']) => + '$arg1$arg2$arg3$arg4$arg5'; +String fiveOptional( + [String arg1 = 'default', + String? arg2 = 'default', + String arg3 = 'default', + String arg4 = 'default', + String arg5 = 'default']) => + '$arg1$arg2$arg3$arg4$arg5'; + +// Six. +String sixRequired(String arg1, String? arg2, String arg3, String arg4, + String arg5, String arg6) => + '$arg1$arg2$arg3$arg4$arg5$arg6'; +String fiveRequiredOneOptional( + String arg1, String? arg2, String arg3, String arg4, String arg5, + [String arg6 = 'default']) => + '$arg1$arg2$arg3$arg4$arg5$arg6'; +String fourRequiredTwoOptional( + String arg1, String? arg2, String arg3, String arg4, + [String arg5 = 'default', String arg6 = 'default']) => + '$arg1$arg2$arg3$arg4$arg5$arg6'; +String threeRequiredThreeOptional(String arg1, String? arg2, String arg3, + [String arg4 = 'default', + String arg5 = 'default', + String arg6 = 'default']) => + '$arg1$arg2$arg3$arg4$arg5$arg6'; +String twoRequiredFourOptional(String arg1, String? arg2, + [String arg3 = 'default', + String arg4 = 'default', + String arg5 = 'default', + String arg6 = 'default']) => + '$arg1$arg2$arg3$arg4$arg5$arg6'; +String oneRequiredFiveOptional(String arg1, + [String? arg2 = 'default', + String arg3 = 'default', + String arg4 = 'default', + String arg5 = 'default', + String arg6 = 'default']) => + '$arg1$arg2$arg3$arg4$arg5$arg6'; +String sixOptional( + [String arg1 = 'default', + String? arg2 = 'default', + String arg3 = 'default', + String arg4 = 'default', + String arg5 = 'default', + String arg6 = 'default']) => + '$arg1$arg2$arg3$arg4$arg5$arg6'; + +void testZero() { + // Arity tests. + Expect.equals(call(zeroArgs.toJS, []), '0'); + Expect.equals(call(zeroArgs.toJS, ['extra']), '0'); + Expect.equals(call(zeroArgs.toJS, [1.0]), '0'); + + // Conversion round-trip test. + final tearOff = zeroArgs; + Expect.equals(tearOff, tearOff.toJS.toDart); + + // Avoid rewrapping test. + if (isDDC || isDart2JS) { + Expect.throws(() => (zeroArgs.toJS as String Function()).toJS); + } +} + +void testOne() { + // Type tests. + Expect.throws(() => call(oneRequired.toJS, [0])); + Expect.throwsWhen(hasSoundNullSafety, () => call(oneOptional.toJS, [null])); + Expect.throwsWhen( + hasSoundNullSafety, () => call(oneOptional.toJS, ['undefined'])); + + // Arity tests. + Expect.throws(() => call(oneRequired.toJS, [])); + Expect.equals(call(oneRequired.toJS, ['a']), 'a'); + Expect.equals(call(oneRequired.toJS, ['a', 'extra']), 'a'); + Expect.equals(call(oneOptional.toJS, []), 'default'); + Expect.equals(call(oneOptional.toJS, ['a']), 'a'); + Expect.equals(call(oneOptional.toJS, ['a', 'extra']), 'a'); + + // Function subtyping tests. + Expect.equals(call((oneOptional as String Function()).toJS, []), 'default'); + // Throws away the additional args due to the static typing. + Expect.equals( + call((oneOptional as String Function()).toJS, ['a']), 'default'); + + // Conversion round-trip test. + final tearOff = oneRequired; + Expect.equals(tearOff, tearOff.toJS.toDart); + + // Avoid rewrapping test. + if (isDDC || isDart2JS) { + Expect.throws(() => (oneOptional.toJS as String Function()).toJS); + } +} + +void testTwo() { + // Type tests. + Expect.throws(() => call(twoOptional.toJS, [false, 'b'])); + Expect.throws(() => call(twoOptional.toJS, ['a', 1.0])); + Expect.throwsWhen(hasSoundNullSafety, + () => call(oneRequiredOneOptional.toJS, ['undefined', 'b'])); + Expect.throws(() => call(oneRequiredOneOptional.toJS, ['a', true])); + Expect.throws(() => call(twoRequired.toJS, [0, 'b'])); + Expect.throws(() => call(twoRequired.toJS, ['a', 0])); + + // Arity tests. + Expect.throws(() => call(twoRequired.toJS, [])); + Expect.throws(() => call(twoRequired.toJS, ['a'])); + Expect.equals(call(twoRequired.toJS, ['a', 'b']), 'ab'); + Expect.equals(call(twoRequired.toJS, ['a', 'b', 'extra']), 'ab'); + Expect.throws(() => call(oneRequiredOneOptional.toJS, [])); + Expect.equals(call(oneRequiredOneOptional.toJS, ['a']), 'adefault'); + Expect.equals(call(oneRequiredOneOptional.toJS, ['a', 'b']), 'ab'); + Expect.equals(call(oneRequiredOneOptional.toJS, ['a', 'b', 'extra']), 'ab'); + Expect.equals(call(twoOptional.toJS, []), 'defaultdefault'); + Expect.equals(call(twoOptional.toJS, ['a']), 'adefault'); + Expect.equals(call(twoOptional.toJS, ['a', 'b']), 'ab'); + Expect.equals(call(twoOptional.toJS, ['a', 'b', 'extra']), 'ab'); + + // Function subtyping tests. + // TODO(55881): dart2wasm's type conversions are based on the static type, + // whereas DDC and dart2js only do type checks based on the runtime type. We + // can't replicate dart2Wasm's behavior in DDC and dart2js as it would require + // a new Dart trampoline for every function, so there's a discrepancy when we + // use a static type with different parameter types. + var closure = () => + call((twoRequired as String Function(String, String)).toJS, ['a', null]); + if (isDDC || isDart2JS) { + Expect.equals(closure(), 'anull'); + } else { + Expect.throws(closure); + } + Expect.throws( + () => call((oneRequiredOneOptional as String Function(String)).toJS, [])); + Expect.equals( + call((oneRequiredOneOptional as String Function(String)).toJS, ['a']), + 'adefault'); + Expect.equals( + call((oneRequiredOneOptional as String Function(String)).toJS, ['a', 0]), + 'adefault'); + Expect.equals( + call((twoOptional as String Function()).toJS, []), 'defaultdefault'); + Expect.equals( + call((twoOptional as String Function()).toJS, ['a']), 'defaultdefault'); + Expect.equals(call((twoOptional as String Function([String])).toJS, []), + 'defaultdefault'); + Expect.equals( + call((twoOptional as String Function([String])).toJS, ['a']), 'adefault'); + Expect.equals( + call((twoOptional as String Function([String])).toJS, ['a', false]), + 'adefault'); + + // `undefined` tests. + Expect.equals(call(twoRequired.toJS, ['a', 'undefined']), 'anull'); + // TODO(55884): DDC lowers function with defaults to use the JS default + // argument syntax, which means passing `undefined` results in DDC replacing + // it with the default, instead of keeping it as `undefined`. + Expect.equals(call(oneRequiredOneOptional.toJS, ['a', 'undefined']), + isDDC ? 'adefault' : 'anull'); + Expect.equals( + call(twoOptional.toJS, ['a', 'undefined']), isDDC ? 'adefault' : 'anull'); + + // Conversion round-trip test. + final tearOff = twoRequired; + Expect.equals(tearOff, tearOff.toJS.toDart); + + // Avoid rewrapping test. + if (isDDC || isDart2JS) { + Expect.throws( + () => (oneRequiredOneOptional.toJS as String Function()).toJS); + } +} + +// To avoid making this test unreadably long, the remaining tests choose a small +// subset of all possible tests for general validation. +void testThree() { + // Type tests. + Expect.throws(() => call(threeRequired.toJS, [0, 'b', 'c'])); + Expect.throws(() => call(oneRequiredTwoOptional.toJS, ['a', false])); + + // Arity tests. + Expect.equals(call(twoRequiredOneOptional.toJS, ['a', 'b']), 'abdefault'); + Expect.throws(() => call(oneRequiredTwoOptional.toJS, [])); + + // Function subtyping tests. + var closure = () => call( + (twoRequiredOneOptional as String Function(String, String)).toJS, + ['a', null, 'c']); + if (isDDC || isDart2JS) { + Expect.equals(closure(), 'anulldefault'); + } else { + Expect.throws(closure); + } + Expect.equals( + call((threeOptional as String Function([String])).toJS, ['a', 0, true]), + 'adefaultdefault'); + + // `undefined` tests. + Expect.equals(call(threeOptional.toJS, ['a', 'undefined']), + isDDC ? 'adefaultdefault' : 'anulldefault'); + + // Conversion round-trip test. + final tearOff = threeRequired; + Expect.equals(tearOff, tearOff.toJS.toDart); + + // Avoid rewrapping test. + if (isDDC || isDart2JS) { + Expect.throws( + () => (twoRequiredOneOptional.toJS as String Function()).toJS); + } +} + +void testFour() { + // Type tests. + Expect.throws( + () => call(threeRequiredOneOptional.toJS, ['a', 'b', 'c', true])); + Expect.throws(() => call(oneRequiredThreeOptional.toJS, [false])); + + // Arity tests. + Expect.equals(call(fourRequired.toJS, ['a', 'b', 'c', 'd', false]), 'abcd'); + Expect.equals(call(fourOptional.toJS, ['a']), 'adefaultdefaultdefault'); + + // Function subtyping tests. + final closure = () => call( + (threeRequiredOneOptional as String Function(String, String, String)) + .toJS, + ['a', null, 'c']); + if (isDDC || isDart2JS) { + Expect.equals(closure(), 'anullcdefault'); + } else { + Expect.throws(closure); + } + Expect.equals( + call( + (twoRequiredTwoOptional as String Function(String, String?, [String])) + .toJS, + ['a', null]), + 'anulldefaultdefault'); + + // `undefined` tests. + Expect.equals(call(oneRequiredThreeOptional.toJS, ['a', 'undefined', 'c']), + isDDC ? 'adefaultcdefault' : 'anullcdefault'); + + // Conversion round-trip test. + final tearOff = fourRequired; + Expect.equals(tearOff, tearOff.toJS.toDart); + + // Avoid rewrapping test. + if (isDDC || isDart2JS) { + Expect.throws( + () => (oneRequiredThreeOptional.toJS as String Function()).toJS); + } +} + +void testFive() { + // Type tests. + Expect.throws(() => call(twoRequiredThreeOptional.toJS, ['a', 0])); + Expect.throws(() => call(fiveOptional.toJS, [false])); + + // Arity tests. + Expect.equals(call(fiveRequired.toJS, ['a', 'b', 'c', 'd', 'e', 0]), 'abcde'); + Expect.equals(call(fourRequiredOneOptional.toJS, ['a', null, 'c', 'd']), + 'anullcddefault'); + + // Function subtyping tests. + final closure = () => call( + (threeRequiredTwoOptional as String Function(String, String, String, + [String])) + .toJS, + ['a', null, 'c']); + if (isDDC || isDart2JS) { + Expect.equals(closure(), 'anullcdefaultdefault'); + } else { + Expect.throws(closure); + } + Expect.equals( + call( + (twoRequiredThreeOptional as String Function(String, String?, + [String])) + .toJS, + ['a', null, 'c']), + 'anullcdefaultdefault'); + + // `undefined` tests. + Expect.equals( + call(oneRequiredFourOptional.toJS, ['a', 'undefined', 'c', 'd', 'e']), + isDDC ? 'adefaultcde' : 'anullcde'); + + // Conversion round-trip test. + final tearOff = fiveRequired; + Expect.equals(tearOff, tearOff.toJS.toDart); + + // Avoid rewrapping test. + if (isDDC || isDart2JS) { + Expect.throws( + () => (twoRequiredThreeOptional.toJS as String Function()).toJS); + } +} + +// DDC and dart2js should use either a `dcall` or `Function.apply` for this. +void testSix() { + // Type tests. + Expect.throws(() => call(sixRequired.toJS, ['a', 'b', 0.0, 'd', 'e', 'f'])); + Expect.throws(() => call(threeRequiredThreeOptional.toJS, ['undefined'])); + + // Arity tests. + // Verify that we appropriately truncate arguments even though we don't have + // a special lowering for six arguments in DDC and dart2js. + Expect.equals( + call(fourRequiredTwoOptional.toJS, ['a', 'b', 'c', 'd', 'e', 'f', 0]), + 'abcdef'); + Expect.throws(() => call(twoRequiredFourOptional.toJS, [])); + + // Function subtyping tests. + var closure = () => call( + (fiveRequiredOneOptional as String Function( + String, String, String, String, String)) + .toJS, + ['a', null, 'c', 'd', 'e', 'f']); + if (isDDC || isDart2JS) { + Expect.equals(closure(), 'anullcdedefault'); + } else { + Expect.throws(closure); + } + Expect.equals( + call((oneRequiredFiveOptional as String Function(String, [String?])).toJS, + ['a', 'b', 0, 0.0, false]), + 'abdefaultdefaultdefaultdefault'); + + // `undefined` tests. + Expect.equals(call(sixOptional.toJS, ['a', 'undefined', 'c', 'd', 'e']), + isDDC ? 'adefaultcdedefault' : 'anullcdedefault'); + + // Conversion round-trip test. + final tearOff = sixRequired; + Expect.equals(tearOff, tearOff.toJS.toDart); + + // Avoid rewrapping test. + if (isDDC || isDart2JS) { + Expect.throws(() => (sixOptional.toJS as String Function()).toJS); + } +} + +void main() { + eval(''' + self.call = function(f, args) { + var convert = function(arg) { + return arg == 'undefined' ? undefined : arg; + }; + return f.apply(null, args.map((e) => convert(e))); + }; + '''); + testZero(); + testOne(); + testTwo(); + testThree(); + testFour(); + testFive(); + testSix(); +} diff --git a/tests/lib/js/static_interop_test/js_types_test.dart b/tests/lib/js/static_interop_test/js_types_test.dart index cf31b2fa015..6d8a3b83cba 100644 --- a/tests/lib/js/static_interop_test/js_types_test.dart +++ b/tests/lib/js/static_interop_test/js_types_test.dart @@ -156,15 +156,20 @@ void syncTests() { expect(confuse(fun) is JSFunction, true); // [JSExportedDartFunction] <-> [Function] - edf = (JSString a, JSString b) { + final dartFunction = (JSString a, JSString b) { return (a.toDart + b.toDart).toJS; - }.toJS; + }; + edf = dartFunction.toJS; expect(doFun('foo'.toJS, 'bar'.toJS).toDart, 'foobar'); expect( (edf.toDart as JSString Function(JSString, JSString))( 'foo'.toJS, 'bar'.toJS) .toDart, 'foobar'); + Expect.equals(edf.toDart, dartFunction); + Expect.isTrue(identical(edf.toDart, dartFunction)); + // Two wrappers should not be the same. + Expect.notEquals(edf, dartFunction.toJS); // Converting a non-function should throw. Expect.throws(() => ('foo'.toJS as JSExportedDartFunction).toDart); diff --git a/tests/lib/lib.status b/tests/lib/lib.status index cb58ac02bf2..84d5a2e7a6a 100644 --- a/tests/lib/lib.status +++ b/tests/lib/lib.status @@ -90,6 +90,7 @@ js/static_interop_test/isa/functional_test: SkipByDesign # Issue 42085. CSP poli js/static_interop_test/isa/library_renaming_test: SkipByDesign # Issue 42085. CSP policy disallows injected JS code js/static_interop_test/js_array_proxy_or_ref_test: SkipByDesign # Issue 42085. CSP policy disallows injected JS code js/static_interop_test/js_array_test: SkipByDesign # Issue 42085. CSP policy disallows injected JS code +js/static_interop_test/js_function_arity_test: SkipByDesign # Issue 42085. CSP policy disallows injected JS code js/static_interop_test/js_function_conversions_test: SkipByDesign # Issue 42085. CSP policy disallows injected JS code js/static_interop_test/js_typed_array_test: SkipByDesign # CSP policy disallows injected JS code js/static_interop_test/jsobject_type_test: SkipByDesign # Issue 42085. CSP policy disallows injected JS code