blob: a7ee811568700b41d9209d477f88c29063a5e0ef [file] [log] [blame]
// 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';
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);
String call(JSFunction f, List<Object?> args) =>
_call(f, args.map((e) => e?.jsify()).toList().toJS);
@JS()
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';
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) =>
'$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';
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) =>
'$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';
String oneRequiredThreeOptionalThis(JSObject? this_, String arg1,
[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';
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,
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';
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.identical(tearOff, tearOff.toJS.toDart);
final tearOffThis = zeroArgsThis;
Expect.identical(tearOffThis, tearOffThis.toJSCaptureThis.toDart);
// Avoid rewrapping test.
if (isDDC || isDart2JS) {
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(soundNullSafety, () => call(oneOptional.toJS, [null]));
Expect.throwsWhen(
soundNullSafety, () => call(oneOptional.toJS, ['undefined']));
Expect.throws(() => call(oneOptionalThis.toJSCaptureThis, [true]));
// 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');
Expect.equals(call(oneOptionalThis.toJSCaptureThis, ['a']), 'a');
if (soundNullSafety) {
// `this` can be null or a JSObject depending on strict mode, which in turn
// depends on the compiler. To make this consistent, only run when sound
// null safety is enabled.
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.identical(tearOff, tearOff.toJS.toDart);
final tearOffThis = oneOptionalThis;
Expect.identical(tearOffThis, tearOffThis.toJSCaptureThis.toDart);
// Avoid rewrapping test.
if (isDDC || isDart2JS) {
Expect.throwsArgumentError(
() => (oneOptional.toJS as String Function()).toJS);
Expect.throwsArgumentError(() =>
(oneOptionalThis.toJSCaptureThis as String Function(JSObject?))
.toJSCaptureThis);
}
}
void testTwo() {
// Type tests.
Expect.throws(() => call(twoOptional.toJS, [false, 'b']));
Expect.throws(() => call(twoOptional.toJS, ['a', 1.0]));
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, []));
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');
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,
// 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');
Expect.equals(
call(
(oneRequiredOneOptionalThis as String Function(JSObject?, String))
.toJSCaptureThis,
['a', 'b']),
'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.identical(tearOff, tearOff.toJS.toDart);
final tearOffThis = oneRequiredOneOptionalThis;
Expect.identical(tearOffThis, tearOffThis.toJSCaptureThis.toDart);
// Avoid rewrapping test.
if (isDDC || isDart2JS) {
Expect.throwsArgumentError(
() => (oneRequiredOneOptional.toJS as String Function(String)).toJS);
Expect.throwsArgumentError(() => (oneRequiredOneOptionalThis.toJSCaptureThis
as String Function(JSObject?, String))
.toJSCaptureThis);
}
}
// 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]));
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(
(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');
Expect.equals(
call(
(threeOptionalThis as String Function([JSObject?, String, String?]))
.toJSCaptureThis,
['a', 'b', false]),
'abdefault');
// `undefined` tests.
Expect.equals(call(threeOptional.toJS, ['a', 'undefined']),
isDDC ? 'adefaultdefault' : 'anulldefault');
// Conversion round-trip test.
final tearOff = threeRequired;
Expect.identical(tearOff, tearOff.toJS.toDart);
final tearOffThis = threeOptionalThis;
Expect.identical(tearOffThis, tearOffThis.toJSCaptureThis.toDart);
// Avoid rewrapping test.
if (isDDC || isDart2JS) {
Expect.throwsArgumentError(() =>
(twoRequiredOneOptional.toJS as String Function(String, String?)).toJS);
Expect.throwsArgumentError(() =>
(threeOptionalThis.toJSCaptureThis as String Function(JSObject?))
.toJSCaptureThis);
}
}
void testFour() {
// Type tests.
Expect.throws(
() => call(threeRequiredOneOptional.toJS, ['a', 'b', 'c', true]));
Expect.throws(() => call(oneRequiredThreeOptional.toJS, [false]));
if (soundNullSafety) {
// `this` can be null or a JSObject depending on strict mode, which in turn
// depends on the compiler. To make this consistent, only run when sound
// null safety is enabled.
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(
(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');
Expect.equals(
call(
(oneRequiredThreeOptionalThis as String Function(JSObject?, String))
.toJSCaptureThis,
['a', 'b']),
'adefaultdefaultdefault');
// `undefined` tests.
Expect.equals(call(oneRequiredThreeOptional.toJS, ['a', 'undefined', 'c']),
isDDC ? 'adefaultcdefault' : 'anullcdefault');
// Conversion round-trip test.
final tearOff = fourRequired;
Expect.identical(tearOff, tearOff.toJS.toDart);
final tearOffThis = oneRequiredThreeOptionalThis;
Expect.identical(tearOffThis, tearOffThis.toJSCaptureThis.toDart);
// Avoid rewrapping test.
if (isDDC || isDart2JS) {
Expect.throwsArgumentError(
() => (oneRequiredThreeOptional.toJS as String Function(String)).toJS);
Expect.throwsArgumentError(() => (oneRequiredThreeOptionalThis
.toJSCaptureThis as String Function(JSObject?, String))
.toJSCaptureThis);
}
}
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(
(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');
Expect.equals(
call(
(threeRequiredTwoOptionalThis as String Function(
JSObject?, String, String?, String))
.toJSCaptureThis,
['a', 'b', 'c', 'd']),
'abcdefaultdefault');
// `undefined` tests.
Expect.equals(
call(oneRequiredFourOptional.toJS, ['a', 'undefined', 'c', 'd', 'e']),
isDDC ? 'adefaultcde' : 'anullcde');
// Conversion round-trip test.
final tearOff = fiveRequired;
Expect.identical(tearOff, tearOff.toJS.toDart);
final tearOffThis = threeRequiredTwoOptionalThis;
Expect.identical(tearOffThis, tearOffThis.toJSCaptureThis.toDart);
// Avoid rewrapping test.
if (isDDC || isDart2JS) {
Expect.throwsArgumentError(() =>
(twoRequiredThreeOptional.toJS as String Function(String, String?))
.toJS);
Expect.throwsArgumentError(() =>
(threeRequiredTwoOptionalThis.toJSCaptureThis as String Function(
JSObject?, String, String?, String))
.toJSCaptureThis);
}
}
// 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']));
Expect.throwsWhen(
soundNullSafety, () => call(sixOptionalThis.toJSCaptureThis, [null]));
// 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, []));
Expect.equals(
call(sixOptionalThis.toJSCaptureThis, ['a', 'b', 'c', 'd', 'e', 'f', 0]),
'abcdef');
// 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');
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']),
isDDC ? 'adefaultcdedefault' : 'anullcdedefault');
// Conversion round-trip test.
final tearOff = sixRequired;
Expect.identical(tearOff, tearOff.toJS.toDart);
final tearOffThis = sixOptionalThis;
Expect.identical(tearOffThis, tearOffThis.toJSCaptureThis.toDart);
// Avoid rewrapping test.
if (isDDC || isDart2JS) {
Expect.throwsArgumentError(
() => (sixOptional.toJS as String Function()).toJS);
Expect.throwsArgumentError(() =>
(sixOptionalThis.toJSCaptureThis as String Function(JSObject?))
.toJSCaptureThis);
}
}
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();
}