[dart:js_interop] Add toJSCaptureThis

Closes https://github.com/dart-lang/sdk/issues/54381

- Adds an API to capture the this value so that users
can use it in the callback.
- Adds specialized stubs to dart2js similar to what was
done for toJS.
- Adds generic stub to DDC as these stubs don't get
tree-shaken away and toJSCaptureThis is less likely to be
used.
- Modifies dart2wasm lowerings to add this to the JS
function wrapper if calling toJSCaptureThis.

Change-Id: Ic0a7fd768de1dd6b491998e029ff5eb406ee7992
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/377160
Reviewed-by: Stephen Adams <sra@google.com>
Commit-Queue: Srujan Gaddam <srujzs@google.com>
Reviewed-by: Leaf Petersen <leafp@google.com>
Reviewed-by: Martin Kustermann <kustermann@google.com>
This commit is contained in:
Srujan Gaddam
2024-08-01 01:02:47 +00:00
committed by Commit Queue
parent 93054a62d5
commit ae3dbcdfb1
15 changed files with 456 additions and 69 deletions
+2
View File
@@ -17,6 +17,8 @@
- Added constructors for `JSArrayBuffer`, `JSDataView`, and concrete typed array
types e.g. `JSInt8Array`.
- Added `length` and `[]`/`[]=` operators to `JSArray`.
- Added `toJSCaptureThis` so `this` is passed in from JavaScript to the
callback as the first parameter.
## 3.5.0
@@ -33,26 +33,37 @@ typedef _InvocationBuilder = Expression Function(
/// Replaces js_util methods with inline calls to foreign_helper JS which
/// emits the code as a JavaScript code fragment.
class JsUtilOptimizer extends Transformer {
final Procedure _allowInteropTarget;
final Iterable<Procedure> _allowedInteropJsUtilTargets;
final Procedure _callMethodTarget;
final Procedure _callMethodTrustTypeTarget;
final List<Procedure> _callMethodUncheckedTargets;
final List<Procedure> _callMethodUncheckedTrustTypeTargets;
final Procedure _callConstructorTarget;
final List<Procedure> _callConstructorUncheckedTargets;
final CloneVisitorNotMembers _cloner = CloneVisitorWithMembers();
final Map<Member, _InvocationBuilder?> _externalInvocationBuilders = {};
final Procedure _functionToJSTarget;
final Procedure _functionToJSCaptureThisTarget;
final List<Procedure> _functionToJSTargets;
final List<Procedure>? _functionToJSCaptureThisTargets;
final Procedure _functionToJSNTarget;
final Procedure _functionToJSCaptureThisNTarget;
final Procedure _getPropertyTarget;
final Procedure _getPropertyTrustTypeTarget;
final Procedure _globalContextTarget;
final Procedure _jsExportedDartFunctionToDartTarget;
final Procedure _jsFunctionToDart;
final Procedure _jsTarget;
final Procedure _listEmptyFactory;
final InterfaceType _objectType;
final Procedure _setPropertyTarget;
final Procedure _setPropertyUncheckedTarget;
final CoreTypes _coreTypes;
final CloneVisitorNotMembers _cloner = CloneVisitorWithMembers();
final ExtensionIndex _extensionIndex;
final Map<Member, _InvocationBuilder?> _externalInvocationBuilders = {};
final StatefulStaticTypeContext _staticTypeContext;
/// Dynamic members in js_util that interop allowed.
static const List<String> _allowedInteropJsUtilMembers = [
'callConstructor',
@@ -63,18 +74,9 @@ class JsUtilOptimizer extends Transformer {
'setProperty'
];
final Procedure _allowInteropTarget;
final Iterable<Procedure> _allowedInteropJsUtilTargets;
final Procedure _jsTarget;
final Procedure _listEmptyFactory;
final CoreTypes _coreTypes;
final StatefulStaticTypeContext _staticTypeContext;
final ExtensionIndex _extensionIndex;
JsUtilOptimizer(
this._coreTypes, ClassHierarchy hierarchy, this._extensionIndex)
this._coreTypes, ClassHierarchy hierarchy, this._extensionIndex,
{required bool isDart2JS})
: _callMethodTarget =
_coreTypes.index.getTopLevelProcedure('dart:js_util', 'callMethod'),
_callMethodTrustTypeTarget = _coreTypes.index
@@ -95,12 +97,23 @@ class JsUtilOptimizer extends Transformer {
'dart:js_util', '_callConstructorUnchecked$i')),
_functionToJSTarget = _coreTypes.index.getTopLevelProcedure(
'dart:js_interop', 'FunctionToJSExportedDartFunction|get#toJS'),
_functionToJSCaptureThisTarget = _coreTypes.index.getTopLevelProcedure(
'dart:js_interop',
'FunctionToJSExportedDartFunction|get#toJSCaptureThis'),
_functionToJSTargets = List<Procedure>.generate(
6,
(i) => _coreTypes.index
.getTopLevelProcedure('dart:js_util', '_functionToJS$i')),
_functionToJSCaptureThisTargets = isDart2JS
? List<Procedure>.generate(
5,
(i) => _coreTypes.index.getTopLevelProcedure(
'dart:js_util', '_functionToJSCaptureThis$i'))
: null,
_functionToJSNTarget = _coreTypes.index
.getTopLevelProcedure('dart:js_util', '_functionToJSN'),
_functionToJSCaptureThisNTarget = _coreTypes.index
.getTopLevelProcedure('dart:js_util', '_functionToJSCaptureThisN'),
_getPropertyTarget = _coreTypes.index
.getTopLevelProcedure('dart:js_util', 'getProperty'),
_getPropertyTrustTypeTarget = _coreTypes.index
@@ -500,6 +513,8 @@ class JsUtilOptimizer extends Transformer {
// https://github.com/dart-lang/sdk/issues/53367 is resolved.
} else if (target == _functionToJSTarget) {
invocation = _lowerFunctionToJS(node);
} else if (target == _functionToJSCaptureThisTarget) {
invocation = _lowerFunctionToJS(node, captureThis: true);
} else if (target == _jsExportedDartFunctionToDartTarget) {
invocation = _lowerJSExportedDartFunctionToDart(node);
} else if (target.isExternal && !JsInteropChecks.isPatchedMember(target)) {
@@ -684,27 +699,47 @@ 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.
/// For the given `dart:js_interop` `Function.toJS` or
/// `Function.toJSCaptureThis` invocation [node], returns an invocation of the
/// corresponding private stub in `js_util` with the invocation's [Function]
/// argument and the number of positional parameters of that [Function].
///
/// If the number of the positional arguments is larger than 5, returns an
/// invocation of `_functionToJSN` instead.
StaticInvocation _lowerFunctionToJS(StaticInvocation node) {
/// If [captureThis] is false, the node target is assumed to be
/// `Function.toJS` and otherwise `Function.toJSCaptureThis`.
///
/// There are specialized stubs up to a certain positional parameter length,
/// and after that, either an invocation of `_functionToJSN` or
/// `_functionToJSCaptureThisN` is returned.
StaticInvocation _lowerFunctionToJS(StaticInvocation node,
{bool captureThis = false}) {
// 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;
final parametersLength = functionType.positionalParameters.length;
List<Procedure>? specializedStubs;
int stubIndex;
Procedure genericStub;
if (captureThis) {
specializedStubs = _functionToJSCaptureThisTargets;
stubIndex = parametersLength - 1; // Account for `this`.
genericStub = _functionToJSCaptureThisNTarget;
} else {
specializedStubs = _functionToJSTargets;
stubIndex = parametersLength;
genericStub = _functionToJSNTarget;
}
Procedure target;
Arguments arguments;
if (argumentsLength < _functionToJSTargets.length) {
target = _functionToJSTargets[argumentsLength];
if (specializedStubs != null &&
stubIndex >= 0 &&
stubIndex < specializedStubs.length) {
target = specializedStubs[stubIndex];
arguments = Arguments([function]);
} else {
target = _functionToJSNTarget;
arguments = Arguments([function, IntLiteral(argumentsLength)]);
target = genericStub;
arguments = Arguments([function, IntLiteral(parametersLength)]);
}
return StaticInvocation(
target, arguments..fileOffset = node.arguments.fileOffset)
@@ -156,8 +156,9 @@ class Dart2jsTarget extends Target {
jsInteropReporter,
jsInteropChecks.exportChecker,
jsInteropChecks.extensionIndex);
var jsUtilOptimizer =
JsUtilOptimizer(coreTypes, hierarchy, jsInteropChecks.extensionIndex);
var jsUtilOptimizer = JsUtilOptimizer(
coreTypes, hierarchy, jsInteropChecks.extensionIndex,
isDart2JS: true);
for (var library in libraries) {
// Shared transformer has static checks, so we still visit even if there
// are errors.
+29 -12
View File
@@ -177,11 +177,12 @@ class CallbackSpecializer {
/// Create a [Procedure] that will wrap a Dart callback in a JS wrapper.
///
/// [node] is the conversion function that is called by the user (either
/// `allowInterop` or `Function.toJS`). [type] is the static type of the
/// callback. [boxExternRef] determines if the trampoline should box the
/// arguments and return value or convert every value. [needsCastClosure]
/// determines if a cast closure is needed in order to validate the types of
/// some arguments.
/// `allowInterop`, `Function.toJS`, or `Function.toJSCaptureThis`). [type] is
/// the static type of the callback. [boxExternRef] determines if the
/// trampoline should box the arguments and return value or convert every
/// value. [needsCastClosure] determines if a cast closure is needed in order
/// to validate the types of some arguments. [captureThis] determines if
/// `this` needs to be passed into the trampoline from the JS wrapper.
///
/// The procedure will call a JS method that will create a wrapper, cache the
/// callback, and call the trampoline function with the callback, the JS
@@ -190,20 +191,30 @@ class CallbackSpecializer {
///
/// Returns the created [Procedure].
Procedure _getJSWrapperFunction(Procedure node, FunctionType type,
{required bool boxExternRef, required bool needsCastClosure}) {
{required bool boxExternRef,
required bool needsCastClosure,
required bool captureThis}) {
final functionTrampolineName =
_createFunctionTrampoline(node, type, boxExternRef: boxExternRef);
List<String> jsParameters = [];
for (int i = 0; i < type.positionalParameters.length; i++) {
var jsParametersLength = type.positionalParameters.length;
if (captureThis) jsParametersLength--;
for (int i = 0; i < jsParametersLength; i++) {
jsParameters.add('x$i');
}
String jsWrapperParams = jsParameters.join(',');
String dartArguments = 'f,arguments.length';
// We could avoid incrementing the arguments length in the case of
// `captureThis` and have the function trampoline account for the extra
// argument, but there's no benefit in doing that.
String argumentsLength =
captureThis ? 'arguments.length + 1' : 'arguments.length';
String dartArguments = 'f,$argumentsLength';
String jsMethodParams = 'f';
if (needsCastClosure) {
dartArguments = '$dartArguments,castClosure';
jsMethodParams = '($jsMethodParams,castClosure)';
}
if (captureThis) dartArguments = '$dartArguments,this';
if (jsParameters.isNotEmpty) {
dartArguments = '$dartArguments,$jsWrapperParams';
}
@@ -263,7 +274,7 @@ class CallbackSpecializer {
final type = argument.getStaticType(_staticTypeContext) as FunctionType;
final jsWrapperFunction = _getJSWrapperFunction(
staticInvocation.target, type,
boxExternRef: false, needsCastClosure: false);
boxExternRef: false, needsCastClosure: false, captureThis: false);
final v = VariableDeclaration('#var',
initializer: argument, type: type, isSynthesized: true);
return Let(
@@ -332,7 +343,7 @@ class CallbackSpecializer {
returnType: VoidType()));
}
/// Given an invocation of `<Function>.toJS`, returns an [Expression]
/// Given an invocation of `Function.toJS`, returns an [Expression]
/// representing:
///
/// JSValue(jsWrapperFunction(<Function>))
@@ -340,13 +351,19 @@ class CallbackSpecializer {
/// or if a cast closure is needed:
///
/// JSValue(jsWrapperFunction(<Function>, <CastClosure>))
Expression functionToJS(StaticInvocation staticInvocation) {
///
/// If [captureThis] is true, this is assumed to be an invocation of
/// `Function.toJSCaptureThis`.
Expression functionToJS(StaticInvocation staticInvocation,
{bool captureThis = false}) {
final argument = staticInvocation.arguments.positional.single;
final type = argument.getStaticType(_staticTypeContext) as FunctionType;
final castClosure = _createCastClosure(type);
final jsWrapperFunction = _getJSWrapperFunction(
staticInvocation.target, type,
boxExternRef: true, needsCastClosure: castClosure != null);
boxExternRef: true,
needsCastClosure: castClosure != null,
captureThis: captureThis);
return _createJSValue(StaticInvocation(
jsWrapperFunction,
Arguments([
@@ -79,6 +79,8 @@ class InteropTransformer extends Transformer {
return _callbackSpecializer.allowInterop(node);
} else if (target == _util.functionToJSTarget) {
return _callbackSpecializer.functionToJS(node);
} else if (target == _util.functionToJSCaptureThisTarget) {
return _callbackSpecializer.functionToJS(node, captureThis: true);
} else if (target == _util.inlineJSTarget) {
return _inlineExpander.expand(node);
} else {
+4
View File
@@ -16,6 +16,7 @@ class CoreTypesUtil {
final Procedure allowInteropTarget;
final Procedure dartifyRawTarget;
final Procedure functionToJSTarget;
final Procedure functionToJSCaptureThisTarget;
final Procedure greaterThanOrEqualToTarget;
final Procedure inlineJSTarget;
final Procedure isDartFunctionWrappedTarget;
@@ -35,6 +36,9 @@ class CoreTypesUtil {
.getTopLevelProcedure('dart:_js_helper', 'dartifyRaw'),
functionToJSTarget = coreTypes.index.getTopLevelProcedure(
'dart:js_interop', 'FunctionToJSExportedDartFunction|get#toJS'),
functionToJSCaptureThisTarget = coreTypes.index.getTopLevelProcedure(
'dart:js_interop',
'FunctionToJSExportedDartFunction|get#toJSCaptureThis'),
greaterThanOrEqualToTarget =
coreTypes.index.getProcedure('dart:core', 'num', '>='),
inlineJSTarget =
+3 -2
View File
@@ -209,8 +209,9 @@ class DevCompilerTarget extends Target {
jsInteropReporter,
jsInteropChecks.exportChecker,
jsInteropChecks.extensionIndex);
final jsUtilOptimizer =
JsUtilOptimizer(coreTypes, hierarchy, jsInteropChecks.extensionIndex);
final jsUtilOptimizer = JsUtilOptimizer(
coreTypes, hierarchy, jsInteropChecks.extensionIndex,
isDart2JS: false);
for (var node in nodes) {
_CovarianceTransformer(node).transform();
// Shared interop transformer has static checks, so we still visit.
@@ -178,6 +178,30 @@ JavaScriptFunction _functionToJSN(Function f, int maxLength) {
return ret;
}
// TODO(srujzs): We could add specific arity stubs for this like we do with
// `Function.toJS`, but unlike dart2js, unused ones aren't tree-shaken away.
// Considering this isn't a very commonly used API, that seems wasteful.
JavaScriptFunction _functionToJSCaptureThisN(Function f, int maxLength) {
if (!dart.isDartFunction(f)) {
throw ArgumentError('Attempting to rewrap a JS function.');
}
final ret = JS<JavaScriptFunction>(
'!',
'''
function (...arguments) {
let args = [this];
args.push.apply(args, arguments);
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) {
@@ -103,6 +103,25 @@ JavaScriptFunction _functionToJS0(Function f) {
return result;
}
JavaScriptFunction _functionToJSCaptureThis0(Function f) {
if (isJSFunction(f)) {
throw ArgumentError('Attempting to rewrap a JS function.');
}
final result = JS(
'JavaScriptFunction',
'''
function(_call, f) {
return function() {
return _call(f, this, 1);
}
}(#, #)
''',
DART_CLOSURE_TO_JS(_callDartFunctionFast1),
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.');
@@ -122,6 +141,25 @@ JavaScriptFunction _functionToJS1(Function f) {
return result;
}
JavaScriptFunction _functionToJSCaptureThis1(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, this, arg1, arguments.length + 1);
}
}(#, #)
''',
DART_CLOSURE_TO_JS(_callDartFunctionFast2),
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.');
@@ -141,6 +179,25 @@ JavaScriptFunction _functionToJS2(Function f) {
return result;
}
JavaScriptFunction _functionToJSCaptureThis2(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, this, arg1, arg2, arguments.length + 1);
}
}(#, #)
''',
DART_CLOSURE_TO_JS(_callDartFunctionFast3),
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.');
@@ -160,6 +217,25 @@ JavaScriptFunction _functionToJS3(Function f) {
return result;
}
JavaScriptFunction _functionToJSCaptureThis3(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, this, arg1, arg2, arg3, arguments.length + 1);
}
}(#, #)
''',
DART_CLOSURE_TO_JS(_callDartFunctionFast4),
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.');
@@ -179,6 +255,25 @@ JavaScriptFunction _functionToJS4(Function f) {
return result;
}
JavaScriptFunction _functionToJSCaptureThis4(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, this, arg1, arg2, arg3, arg4, arguments.length + 1);
}
}(#, #)
''',
DART_CLOSURE_TO_JS(_callDartFunctionFast5),
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.');
@@ -206,7 +301,7 @@ JavaScriptFunction _functionToJSN(Function f, int maxLength) {
'JavaScriptFunction',
'''
function(_call, f, maxLength) {
return function () {
return function() {
return _call(f, Array.prototype.slice.call(arguments, 0,
Math.min(arguments.length, maxLength)));
}
@@ -219,6 +314,33 @@ JavaScriptFunction _functionToJSN(Function f, int maxLength) {
return result;
}
// TODO(srujzs): It would be nice if we can generate these and the
// `Function.toJS` stubs dynamically as needed in the interop transformer. It
// would reduce the potential of accidentally introducing bugs in specific
// stubs.
JavaScriptFunction _functionToJSCaptureThisN(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() {
var args = [this];
args.push.apply(args, arguments);
return _call(f, Array.prototype.slice.call(args, 0,
Math.min(args.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) {
@@ -77,6 +77,12 @@ extension FunctionToJSExportedDartFunction on Function {
JSExportedDartFunction get toJS => throw UnimplementedError(
"'toJS' should never directly be called. Calls to 'toJS' should have "
'been transformed by the interop transformer.');
@patch
JSExportedDartFunction get toJSCaptureThis => throw UnimplementedError(
"'toJSCaptureThis' should never directly be called. Calls to "
"'toJSCaptureThis' should have been transformed by the interop "
'transformer.');
}
// Embedded global property for wrapped Dart objects passed via JS interop.
@@ -92,6 +92,12 @@ extension FunctionToJSExportedDartFunction on Function {
JSExportedDartFunction get toJS => throw UnimplementedError(
"This should never be called. Calls to 'toJS' should have been "
'transformed by the interop transformer.');
@patch
JSExportedDartFunction get toJSCaptureThis => throw UnimplementedError(
"'toJSCaptureThis' should never directly be called. Calls to "
"'toJSCaptureThis' should have been transformed by the interop "
'transformer.');
}
// Embedded global property for wrapped Dart objects passed via JS interop.
+15 -3
View File
@@ -123,8 +123,9 @@ extension type JSFunction._(JSFunctionRepType _jsFunction)
/// A JavaScript callable function created from a Dart function.
///
/// See [FunctionToJSExportedDartFunction.toJS] for more details on how to
/// convert a Dart function.
/// See [FunctionToJSExportedDartFunction.toJS] or
/// [FunctionToJSExportedDartFunction.toJSCaptureThis] for more details on how
/// to convert a Dart function.
@JS('Function')
extension type JSExportedDartFunction._(
JSExportedDartFunctionRepType _jsExportedDartFunction)
@@ -591,7 +592,8 @@ extension JSExportedDartFunctionToFunction on JSExportedDartFunction {
/// The Dart [Function] that this [JSExportedDartFunction] wrapped.
///
/// Must be a function that was wrapped with
/// [FunctionToJSExportedDartFunction.toJS].
/// [FunctionToJSExportedDartFunction.toJS] or
/// [FunctionToJSExportedDartFunction.toJSCaptureThis].
external Function get toDart;
}
@@ -614,6 +616,16 @@ extension FunctionToJSExportedDartFunction on Function {
/// Calling this on the same [Function] again will always result in a new
/// JavaScript function.
external JSExportedDartFunction get toJS;
/// A callable JavaScript function that wraps this [Function] and captures the
/// `this` value when called.
///
/// Identical to [toJS], except the resulting [JSExportedDartFunction] will
/// pass `this` from JavaScript as the first argument to the converted
/// [Function]. Any [Function] that is converted with this member should take
/// in an extra parameter at the beginning of the parameter list to handle
/// this.
external JSExportedDartFunction get toJSCaptureThis;
}
/// Conversions from [JSBoxedDartObject] to [Object].
@@ -7,9 +7,11 @@
import 'dart:js_interop';
import 'package:expect/expect.dart';
import 'package:expect/variations.dart';
const isDDC = const bool.fromEnvironment('dart.library._ddc_only');
const isDart2JS = const bool.fromEnvironment('dart.tool.dart2js');
const soundNullSafety = !unsoundNullSafety;
@JS('call')
external String _call(JSFunction f, JSArray<JSAny?> args);
@@ -22,10 +24,12 @@ external void eval(String code);
// Zero.
String zeroArgs() => '0';
String zeroArgsThis([JSObject? this_]) => '0';
// One.
String oneRequired(String arg1) => arg1;
String oneOptional([String arg1 = 'default']) => '$arg1';
String oneOptionalThis(JSObject? this_, [String arg1 = 'default']) => '$arg1';
// Two.
String twoRequired(String arg1, String? arg2) => '$arg1$arg2';
@@ -33,6 +37,9 @@ String oneRequiredOneOptional(String arg1, [String? arg2 = 'default']) =>
'$arg1$arg2';
String twoOptional([String arg1 = 'default', String? arg2 = 'default']) =>
'$arg1$arg2';
String oneRequiredOneOptionalThis(JSObject? this_, String arg1,
[String? arg2 = 'default']) =>
'$arg1$arg2';
// Three.
String threeRequired(String arg1, String? arg2, String arg3) =>
@@ -48,6 +55,12 @@ String threeOptional(
String? arg2 = 'default',
String arg3 = 'default']) =>
'$arg1$arg2$arg3';
String threeOptionalThis(
[JSObject? this_,
String arg1 = 'default',
String? arg2 = 'default',
String arg3 = 'default']) =>
'$arg1$arg2$arg3';
// Four.
String fourRequired(String arg1, String? arg2, String arg3, String arg4) =>
@@ -69,6 +82,11 @@ String fourOptional(
String arg3 = 'default',
String arg4 = 'default']) =>
'$arg1$arg2$arg3$arg4';
String oneRequiredThreeOptionalThis(JSObject? this_, String arg1,
[String? arg2 = 'default',
String arg3 = 'default',
String arg4 = 'default']) =>
'$arg1$arg2$arg3$arg4';
// Five.
String fiveRequired(
@@ -99,6 +117,10 @@ String fiveOptional(
String arg4 = 'default',
String arg5 = 'default']) =>
'$arg1$arg2$arg3$arg4$arg5';
String threeRequiredTwoOptionalThis(
JSObject? this_, String arg1, String? arg2, String arg3,
[String arg4 = 'default', String arg5 = 'default']) =>
'$arg1$arg2$arg3$arg4$arg5';
// Six.
String sixRequired(String arg1, String? arg2, String arg3, String arg4,
@@ -138,29 +160,45 @@ String sixOptional(
String arg5 = 'default',
String arg6 = 'default']) =>
'$arg1$arg2$arg3$arg4$arg5$arg6';
String sixOptionalThis(
[JSObject? this_,
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');
Expect.equals(call(zeroArgsThis.toJSCaptureThis, []), '0');
Expect.equals(call(zeroArgs.toJSCaptureThis, []), '0');
// Conversion round-trip test.
final tearOff = zeroArgs;
Expect.equals(tearOff, tearOff.toJS.toDart);
Expect.identical(tearOff, tearOff.toJS.toDart);
final tearOffThis = zeroArgsThis;
Expect.identical(tearOffThis, tearOffThis.toJSCaptureThis.toDart);
// Avoid rewrapping test.
if (isDDC || isDart2JS) {
Expect.throws(() => (zeroArgs.toJS as String Function()).toJS);
Expect.throwsArgumentError(() => (zeroArgs.toJS as String Function()).toJS);
Expect.throwsArgumentError(() =>
(zeroArgsThis.toJSCaptureThis as String Function()).toJSCaptureThis);
}
}
void testOne() {
// Type tests.
Expect.throws(() => call(oneRequired.toJS, [0]));
Expect.throwsWhen(hasSoundNullSafety, () => call(oneOptional.toJS, [null]));
Expect.throwsWhen(soundNullSafety, () => call(oneOptional.toJS, [null]));
Expect.throwsWhen(
hasSoundNullSafety, () => call(oneOptional.toJS, ['undefined']));
soundNullSafety, () => call(oneOptional.toJS, ['undefined']));
Expect.throws(() => call(oneOptionalThis.toJSCaptureThis, [true]));
// Arity tests.
Expect.throws(() => call(oneRequired.toJS, []));
@@ -169,20 +207,32 @@ void testOne() {
Expect.equals(call(oneOptional.toJS, []), 'default');
Expect.equals(call(oneOptional.toJS, ['a']), 'a');
Expect.equals(call(oneOptional.toJS, ['a', 'extra']), 'a');
Expect.equals(call(oneOptionalThis.toJSCaptureThis, ['a']), 'a');
Expect.throws(() => call(oneRequired.toJSCaptureThis, []));
// 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');
Expect.equals(
call((oneOptionalThis as String Function(JSObject?)).toJSCaptureThis,
['a']),
'default');
// Conversion round-trip test.
final tearOff = oneRequired;
Expect.equals(tearOff, tearOff.toJS.toDart);
Expect.identical(tearOff, tearOff.toJS.toDart);
final tearOffThis = oneOptionalThis;
Expect.identical(tearOffThis, tearOffThis.toJSCaptureThis.toDart);
// Avoid rewrapping test.
if (isDDC || isDart2JS) {
Expect.throws(() => (oneOptional.toJS as String Function()).toJS);
Expect.throwsArgumentError(
() => (oneOptional.toJS as String Function()).toJS);
Expect.throwsArgumentError(() =>
(oneOptionalThis.toJSCaptureThis as String Function(JSObject?))
.toJSCaptureThis);
}
}
@@ -190,11 +240,12 @@ void testTwo() {
// Type tests.
Expect.throws(() => call(twoOptional.toJS, [false, 'b']));
Expect.throws(() => call(twoOptional.toJS, ['a', 1.0]));
Expect.throwsWhen(hasSoundNullSafety,
Expect.throwsWhen(soundNullSafety,
() => 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]));
Expect.throws(() => call(oneRequiredOneOptional.toJSCaptureThis, [0]));
// Arity tests.
Expect.throws(() => call(twoRequired.toJS, []));
@@ -209,6 +260,11 @@ void testTwo() {
Expect.equals(call(twoOptional.toJS, ['a']), 'adefault');
Expect.equals(call(twoOptional.toJS, ['a', 'b']), 'ab');
Expect.equals(call(twoOptional.toJS, ['a', 'b', 'extra']), 'ab');
Expect.equals(
call(oneRequiredOneOptionalThis.toJSCaptureThis, ['a', 'b', 'extra']),
'ab');
Expect.equals(
call(oneRequiredOneOptionalThis.toJSCaptureThis, ['a']), 'adefault');
// Function subtyping tests.
// TODO(55881): dart2wasm's type conversions are based on the static type,
@@ -242,6 +298,12 @@ void testTwo() {
Expect.equals(
call((twoOptional as String Function([String])).toJS, ['a', false]),
'adefault');
Expect.equals(
call(
(oneRequiredOneOptionalThis as String Function(JSObject?, String))
.toJSCaptureThis,
['a', 'b']),
'adefault');
// `undefined` tests.
Expect.equals(call(twoRequired.toJS, ['a', 'undefined']), 'anull');
@@ -255,12 +317,17 @@ void testTwo() {
// Conversion round-trip test.
final tearOff = twoRequired;
Expect.equals(tearOff, tearOff.toJS.toDart);
Expect.identical(tearOff, tearOff.toJS.toDart);
final tearOffThis = oneRequiredOneOptionalThis;
Expect.identical(tearOffThis, tearOffThis.toJSCaptureThis.toDart);
// Avoid rewrapping test.
if (isDDC || isDart2JS) {
Expect.throws(
() => (oneRequiredOneOptional.toJS as String Function()).toJS);
Expect.throwsArgumentError(
() => (oneRequiredOneOptional.toJS as String Function(String)).toJS);
Expect.throwsArgumentError(() => (oneRequiredOneOptionalThis.toJSCaptureThis
as String Function(JSObject?, String))
.toJSCaptureThis);
}
}
@@ -270,10 +337,13 @@ void testThree() {
// Type tests.
Expect.throws(() => call(threeRequired.toJS, [0, 'b', 'c']));
Expect.throws(() => call(oneRequiredTwoOptional.toJS, ['a', false]));
Expect.throws(() => call(threeOptionalThis.toJSCaptureThis, [true]));
// Arity tests.
Expect.equals(call(twoRequiredOneOptional.toJS, ['a', 'b']), 'abdefault');
Expect.throws(() => call(oneRequiredTwoOptional.toJS, []));
Expect.equals(
call(threeOptionalThis.toJSCaptureThis, ['a', 'b']), 'abdefault');
// Function subtyping tests.
var closure = () => call(
@@ -287,6 +357,12 @@ void testThree() {
Expect.equals(
call((threeOptional as String Function([String])).toJS, ['a', 0, true]),
'adefaultdefault');
Expect.equals(
call(
(threeOptionalThis as String Function([JSObject?, String, String?]))
.toJSCaptureThis,
['a', 'b', false]),
'abdefault');
// `undefined` tests.
Expect.equals(call(threeOptional.toJS, ['a', 'undefined']),
@@ -294,12 +370,17 @@ void testThree() {
// Conversion round-trip test.
final tearOff = threeRequired;
Expect.equals(tearOff, tearOff.toJS.toDart);
Expect.identical(tearOff, tearOff.toJS.toDart);
final tearOffThis = threeOptionalThis;
Expect.identical(tearOffThis, tearOffThis.toJSCaptureThis.toDart);
// Avoid rewrapping test.
if (isDDC || isDart2JS) {
Expect.throws(
() => (twoRequiredOneOptional.toJS as String Function()).toJS);
Expect.throwsArgumentError(() =>
(twoRequiredOneOptional.toJS as String Function(String, String?)).toJS);
Expect.throwsArgumentError(() =>
(threeOptionalThis.toJSCaptureThis as String Function(JSObject?))
.toJSCaptureThis);
}
}
@@ -308,10 +389,13 @@ void testFour() {
Expect.throws(
() => call(threeRequiredOneOptional.toJS, ['a', 'b', 'c', true]));
Expect.throws(() => call(oneRequiredThreeOptional.toJS, [false]));
Expect.throws(() => call(oneRequiredThreeOptional.toJSCaptureThis, ['a']));
// Arity tests.
Expect.equals(call(fourRequired.toJS, ['a', 'b', 'c', 'd', false]), 'abcd');
Expect.equals(call(fourOptional.toJS, ['a']), 'adefaultdefaultdefault');
Expect.equals(call(oneRequiredThreeOptionalThis.toJSCaptureThis, ['a', 'b']),
'abdefaultdefault');
// Function subtyping tests.
final closure = () => call(
@@ -329,6 +413,12 @@ void testFour() {
.toJS,
['a', null]),
'anulldefaultdefault');
Expect.equals(
call(
(oneRequiredThreeOptionalThis as String Function(JSObject?, String))
.toJSCaptureThis,
['a', 'b']),
'adefaultdefaultdefault');
// `undefined` tests.
Expect.equals(call(oneRequiredThreeOptional.toJS, ['a', 'undefined', 'c']),
@@ -336,12 +426,17 @@ void testFour() {
// Conversion round-trip test.
final tearOff = fourRequired;
Expect.equals(tearOff, tearOff.toJS.toDart);
Expect.identical(tearOff, tearOff.toJS.toDart);
final tearOffThis = oneRequiredThreeOptionalThis;
Expect.identical(tearOffThis, tearOffThis.toJSCaptureThis.toDart);
// Avoid rewrapping test.
if (isDDC || isDart2JS) {
Expect.throws(
() => (oneRequiredThreeOptional.toJS as String Function()).toJS);
Expect.throwsArgumentError(
() => (oneRequiredThreeOptional.toJS as String Function(String)).toJS);
Expect.throwsArgumentError(() => (oneRequiredThreeOptionalThis
.toJSCaptureThis as String Function(JSObject?, String))
.toJSCaptureThis);
}
}
@@ -349,11 +444,16 @@ void testFive() {
// Type tests.
Expect.throws(() => call(twoRequiredThreeOptional.toJS, ['a', 0]));
Expect.throws(() => call(fiveOptional.toJS, [false]));
Expect.throws(() =>
call(threeRequiredTwoOptionalThis.toJSCaptureThis, ['a', 'b', 1.0]));
// Arity tests.
Expect.equals(call(fiveRequired.toJS, ['a', 'b', 'c', 'd', 'e', 0]), 'abcde');
Expect.equals(call(fourRequiredOneOptional.toJS, ['a', null, 'c', 'd']),
'anullcddefault');
Expect.equals(
call(threeRequiredTwoOptionalThis.toJSCaptureThis, ['a', 'b', 'c', 'd']),
'abcddefault');
// Function subtyping tests.
final closure = () => call(
@@ -373,6 +473,13 @@ void testFive() {
.toJS,
['a', null, 'c']),
'anullcdefaultdefault');
Expect.equals(
call(
(threeRequiredTwoOptionalThis as String Function(
JSObject?, String, String?, String))
.toJSCaptureThis,
['a', 'b', 'c', 'd']),
'abcdefaultdefault');
// `undefined` tests.
Expect.equals(
@@ -381,12 +488,19 @@ void testFive() {
// Conversion round-trip test.
final tearOff = fiveRequired;
Expect.equals(tearOff, tearOff.toJS.toDart);
Expect.identical(tearOff, tearOff.toJS.toDart);
final tearOffThis = threeRequiredTwoOptionalThis;
Expect.identical(tearOffThis, tearOffThis.toJSCaptureThis.toDart);
// Avoid rewrapping test.
if (isDDC || isDart2JS) {
Expect.throws(
() => (twoRequiredThreeOptional.toJS as String Function()).toJS);
Expect.throwsArgumentError(() =>
(twoRequiredThreeOptional.toJS as String Function(String, String?))
.toJS);
Expect.throwsArgumentError(() =>
(threeRequiredTwoOptionalThis.toJSCaptureThis as String Function(
JSObject?, String, String?, String))
.toJSCaptureThis);
}
}
@@ -395,6 +509,8 @@ void testSix() {
// Type tests.
Expect.throws(() => call(sixRequired.toJS, ['a', 'b', 0.0, 'd', 'e', 'f']));
Expect.throws(() => call(threeRequiredThreeOptional.toJS, ['undefined']));
Expect.throwsWhen(
soundNullSafety, () => call(sixOptionalThis.toJSCaptureThis, [null]));
// Arity tests.
// Verify that we appropriately truncate arguments even though we don't have
@@ -403,6 +519,9 @@ void testSix() {
call(fourRequiredTwoOptional.toJS, ['a', 'b', 'c', 'd', 'e', 'f', 0]),
'abcdef');
Expect.throws(() => call(twoRequiredFourOptional.toJS, []));
Expect.equals(
call(sixOptionalThis.toJSCaptureThis, ['a', 'b', 'c', 'd', 'e', 'f', 0]),
'abcdef');
// Function subtyping tests.
var closure = () => call(
@@ -419,6 +538,10 @@ void testSix() {
call((oneRequiredFiveOptional as String Function(String, [String?])).toJS,
['a', 'b', 0, 0.0, false]),
'abdefaultdefaultdefaultdefault');
Expect.equals(
call((sixOptionalThis as String Function()).toJSCaptureThis,
[true, 0, 0.0]),
'defaultdefaultdefaultdefaultdefaultdefault');
// `undefined` tests.
Expect.equals(call(sixOptional.toJS, ['a', 'undefined', 'c', 'd', 'e']),
@@ -426,11 +549,17 @@ void testSix() {
// Conversion round-trip test.
final tearOff = sixRequired;
Expect.equals(tearOff, tearOff.toJS.toDart);
Expect.identical(tearOff, tearOff.toJS.toDart);
final tearOffThis = sixOptionalThis;
Expect.identical(tearOffThis, tearOffThis.toJSCaptureThis.toDart);
// Avoid rewrapping test.
if (isDDC || isDart2JS) {
Expect.throws(() => (sixOptional.toJS as String Function()).toJS);
Expect.throwsArgumentError(
() => (sixOptional.toJS as String Function()).toJS);
Expect.throwsArgumentError(() =>
(sixOptionalThis.toJSCaptureThis as String Function(JSObject?))
.toJSCaptureThis);
}
}
@@ -215,8 +215,12 @@ void main() {
expect(anyF(zero), zero);
void setBoundNonNullAnyMultipleParametersFunction<T extends JSAny,
U extends JSAny, V extends JSAny>() {
jsFunction = ((T t, U u, [V? v]) => t).toJS;
U extends JSAny, V extends JSAny>({bool captureThis = false}) {
if (captureThis) {
jsFunction = ((T this_, U u, [V? v]) => this_).toJSCaptureThis;
} else {
jsFunction = ((T t, U u, [V? v]) => t).toJS;
}
}
setBoundNonNullAnyMultipleParametersFunction();
@@ -225,6 +229,17 @@ void main() {
anyF(zero, zero, null);
anyF(zero, zero, zero);
setBoundNonNullAnyMultipleParametersFunction(captureThis: true);
Expect.throwsWhen(soundNullSafety, () => anyF(null, zero));
anyF(zero, null);
anyF(zero, zero);
// TODO(srujzs): It'd be nice if we can test that passing a null value for
// `this` throws. However, unless we're in strict mode, that isn't possible
// on all backends. While we can define local functions in strict mode, the
// wrapping of the function when converting a Dart function to JS may not be
// in a strict mode context, and therefore `this` will still be non-nullable.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call#thisarg
void setBoundExternalDartReference<T extends ExternalDartReference<U>?, U>() {
jsFunction = ((T t) => t?.toDartObject.toExternalReference).toJS;
}
@@ -184,12 +184,23 @@ void syncTests() {
'foo'.toJS, 'bar'.toJS)
.toDart,
'foobar');
Expect.equals(edf.toDart, dartFunction);
Expect.isTrue(identical(edf.toDart, dartFunction));
Expect.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);
// `this` should be captured correctly in `toJSCaptureThis`.
final this_ = JSObject();
final dartFunctionThis = (JSObject this__, JSString a, JSString b) {
Expect.equals(this_, this__);
return (a.toDart + b.toDart).toJS;
};
edf = dartFunctionThis.toJSCaptureThis;
Expect.equals(
(edf.callAsFunction(this_, 'foo'.toJS, 'bar'.toJS) as JSString).toDart,
'foobar');
Expect.identical(edf.toDart, dartFunctionThis);
Expect.notEquals(edf, dartFunctionThis.toJSCaptureThis);
// [JSBoxedDartObject] <-> [Object]
edo = DartObject().toJSBox;