blob: 79fe85fb6b2b7d064339ee5afeb6bc6272c37868 [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();
}