| // Copyright (c) 2023, 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 | |
| // Test that Function.toJS properly converts/casts arguments and return values | |
| // when using non-JS types. | |
| import 'dart:js_interop'; | |
| import 'package:expect/expect.dart'; | |
| import 'package:expect/variations.dart'; | |
| const isJSBackend = 0 is JSNumber; | |
| const soundNullSafety = !unsoundNullSafety; | |
| @JS() | |
| external void eval(String code); | |
| @JS() | |
| external set jsFunction(JSFunction f); | |
| @JS('jsFunction') | |
| external JSString stringF(JSString s); | |
| @JS('jsFunction') | |
| external JSNumber numF(JSNumber n); | |
| @JS('jsFunction') | |
| external JSBoolean boolF(JSBoolean b); | |
| @JS('jsFunction') | |
| external void voidF(JSString s); | |
| @JS('jsFunction') | |
| external JSAny? anyF([JSAny? a, JSAny? b, JSAny? c, JSAny? d, JSAny? e]); | |
| @JS('jsFunction') | |
| external ExternalDartReference? externalDartReferenceF( | |
| ExternalDartReference? e, | |
| ); | |
| @JS() | |
| external JSAny? callFunctionWithUndefined(); | |
| @JS() | |
| external JSAny? callFunctionWithJSNull(); | |
| extension type IntE(int _) {} | |
| extension type NullableIntE(int? _) {} | |
| void main() { | |
| // Test primitive conversions. | |
| jsFunction = ((String arg) => arg).toJS; | |
| Expect.equals('stringF', stringF('stringF'.toJS).toDart); | |
| Expect.throws(() => anyF(0.toJS)); | |
| jsFunction = ((int arg) => arg).toJS; | |
| Expect.equals(0, numF(0.toJS).toDartInt); | |
| Expect.equals(0, numF(0.0.toJS).toDartInt); | |
| Expect.throws(() => numF(0.1.toJS)); | |
| Expect.throws(() => anyF(true.toJS)); | |
| jsFunction = ((double arg) => arg).toJS; | |
| Expect.equals(0.0, numF(0.toJS).toDartDouble); | |
| Expect.equals(0.0, numF(0.0.toJS).toDartDouble); | |
| Expect.equals(0.1, numF(0.1.toJS).toDartDouble); | |
| Expect.throws(() => anyF(true.toJS)); | |
| jsFunction = ((num arg) => arg).toJS; | |
| Expect.equals(0, numF(0.toJS).toDartInt); | |
| Expect.equals(0, numF(0.0.toJS).toDartInt); | |
| Expect.equals(0.1, numF(0.1.toJS).toDartDouble); | |
| Expect.throws(() => anyF(true.toJS)); | |
| jsFunction = ((bool arg) => arg).toJS; | |
| Expect.isTrue(boolF(true.toJS).toDart); | |
| Expect.throws(() => anyF(''.toJS)); | |
| jsFunction = ((String arg) {}).toJS; | |
| voidF('voidF'.toJS); | |
| jsFunction = ((String arg) => arg).toJS; | |
| voidF('voidF'.toJS); | |
| // Extension type on primitives. | |
| jsFunction = ((IntE i) => i).toJS; | |
| Expect.equals(0, numF(0.toJS).toDartInt); | |
| Expect.throwsWhen(soundNullSafety, () => anyF(null)); | |
| jsFunction = ((IntE? i) => i).toJS; | |
| Expect.equals(0, numF(0.toJS).toDartInt); | |
| Expect.isNull(anyF(null)); | |
| jsFunction = ((NullableIntE i) => i).toJS; | |
| Expect.equals(0, numF(0.toJS).toDartInt); | |
| Expect.isNull(anyF(null)); | |
| // Test ExternalDartReference. | |
| Set set = {}; | |
| jsFunction = ((ExternalDartReference<Set> arg) => arg).toJS; | |
| Expect.equals( | |
| set, | |
| externalDartReferenceF(set.toExternalReference)!.toDartObject, | |
| ); | |
| // However, if we try to internalize it to the wrong value, that should fail. | |
| // In the JS backends, it fails in the cast. In dart2wasm, it fails in the | |
| // internalization. | |
| Expect.throwsWhen( | |
| isJSBackend && soundNullSafety, | |
| () => externalDartReferenceF(null), | |
| ); | |
| Expect.throwsWhen( | |
| isJSBackend, | |
| () => externalDartReferenceF([].toExternalReference), | |
| ); | |
| Expect.throwsWhen(isJSBackend, () => anyF(''.toJS)); | |
| jsFunction = ((ExternalDartReference<Set> arg) { | |
| arg.toDartObject; | |
| }).toJS; | |
| Expect.throwsWhen(soundNullSafety, () => externalDartReferenceF(null)); | |
| Expect.throws(() => externalDartReferenceF([].toExternalReference)); | |
| // TODO(srujzs): On dart2wasm, this is a `RuntimeError: illegal cast` because | |
| // of the call to `internalize`. Is there any way to first check that the | |
| // value can be internalized and throw if not? Would that slow down the | |
| // round-trip? Most likely, so just check the JS compilers for now. | |
| if (isJSBackend) Expect.throws(() => anyF(''.toJS)); | |
| // Test nullability with JS null and JS undefined. | |
| eval(''' | |
| globalThis.callFunctionWithUndefined = function() { | |
| return globalThis.jsFunction(undefined, undefined, undefined, undefined, undefined); | |
| } | |
| globalThis.callFunctionWithJSNull = function() { | |
| return globalThis.jsFunction(null, null, null, null, null); | |
| } | |
| '''); | |
| void expectNullPass(JSFunction f) { | |
| jsFunction = f; | |
| Expect.isNull(anyF(null)); | |
| Expect.isNull(callFunctionWithUndefined()); | |
| Expect.isNull(callFunctionWithJSNull()); | |
| } | |
| void expectNullFail(JSFunction f) { | |
| if (soundNullSafety) { | |
| jsFunction = f; | |
| Expect.throws(() => anyF(null)); | |
| Expect.throws(() => callFunctionWithUndefined()); | |
| Expect.throws(() => callFunctionWithJSNull()); | |
| } | |
| } | |
| expectNullPass( | |
| ((String? arg) { | |
| Expect.isNull(arg); | |
| return arg; | |
| }).toJS, | |
| ); | |
| expectNullPass( | |
| ((JSString? arg) { | |
| Expect.isNull(arg); | |
| return arg; | |
| }).toJS, | |
| ); | |
| expectNullFail(((String arg) => arg).toJS); | |
| expectNullFail(((JSString arg) => arg).toJS); | |
| // Test conversions with allowed type parameters. | |
| void setBoundAnyFunction<T extends JSAny?>() { | |
| jsFunction = ((T t) => t).toJS; | |
| } | |
| final zero = 0.toJS; | |
| final empty = ''.toJS; | |
| setBoundAnyFunction(); | |
| Expect.isNull(anyF(null)); | |
| Expect.equals(zero, anyF(zero)); | |
| setBoundAnyFunction<JSAny>(); | |
| Expect.throwsWhen(soundNullSafety, () => anyF(null)); | |
| Expect.equals(zero, anyF(zero)); | |
| setBoundAnyFunction<JSNumber>(); | |
| Expect.throwsWhen(soundNullSafety, () => anyF(null)); | |
| Expect.throws(() { | |
| final any = anyF(empty); | |
| // TODO(54179): Better way of writing this is to cast to JSNumber and | |
| // convert, but currently that does not throw on dart2wasm. | |
| if (!any.isA<JSNumber>()) throw TypeError(); | |
| }); | |
| Expect.equals(zero, anyF(zero)); | |
| void setBoundNonNullAnyFunction<T extends JSAny>() { | |
| jsFunction = ((T t) => t).toJS; | |
| } | |
| setBoundNonNullAnyFunction(); | |
| Expect.throwsWhen(soundNullSafety, () => anyF(null)); | |
| Expect.equals(zero, anyF(zero)); | |
| setBoundNonNullAnyFunction<JSNumber>(); | |
| Expect.throwsWhen(soundNullSafety, () => anyF(null)); | |
| Expect.throws(() { | |
| final any = anyF(empty); | |
| // TODO(54179): Better way of writing this is to cast to JSNumber and | |
| // convert, but currently that does not throw on dart2wasm. | |
| if (!any.isA<JSNumber>()) throw TypeError(); | |
| }); | |
| Expect.equals(zero, anyF(zero)); | |
| void setBoundJSNumberFunction<T extends JSNumber>() { | |
| jsFunction = ((T t) => t).toJS; | |
| } | |
| setBoundJSNumberFunction(); | |
| Expect.throwsWhen(soundNullSafety, () => anyF(null)); | |
| Expect.throws(() { | |
| final any = anyF(empty); | |
| // TODO(54179): Better way of writing this is to cast to JSNumber and | |
| // convert, but currently that does not throw on dart2wasm. | |
| if (!any.isA<JSNumber>()) throw TypeError(); | |
| }); | |
| Expect.equals(zero, anyF(zero)); | |
| void setBoundNonNullAnyMultipleParametersFunction< | |
| T extends JSAny, | |
| 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(); | |
| Expect.throwsWhen(soundNullSafety, () => anyF(null, zero, zero)); | |
| Expect.throwsWhen(soundNullSafety, () => anyF(zero, null, zero)); | |
| 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; | |
| } | |
| setBoundExternalDartReference<ExternalDartReference<Object?>, Object?>(); | |
| Expect.isNull(externalDartReferenceF(null)); | |
| Expect.equals(0, externalDartReferenceF(0.toExternalReference)!.toDartObject); | |
| setBoundExternalDartReference<ExternalDartReference<Object>?, Object>(); | |
| Expect.isNull(externalDartReferenceF(null)); | |
| Expect.equals(0, externalDartReferenceF(0.toExternalReference)!.toDartObject); | |
| void setBoundNonNullExternalDartReference< | |
| T extends ExternalDartReference<U>, | |
| U | |
| >() { | |
| jsFunction = ((T t) => t.toDartObject.toExternalReference).toJS; | |
| } | |
| setBoundNonNullExternalDartReference<ExternalDartReference<Object>, Object>(); | |
| Expect.throwsWhen(soundNullSafety, () => externalDartReferenceF(null)); | |
| Expect.equals(0, externalDartReferenceF(0.toExternalReference)!.toDartObject); | |
| // Test that type parameters local to the expression can still be casted to. | |
| T Function(T) getF<T extends JSAny?>() { | |
| return (T t) => t; | |
| } | |
| jsFunction = (getF<JSAny>()).toJS; | |
| Expect.throwsWhen(soundNullSafety, () => anyF(null)); | |
| anyF(zero); | |
| jsFunction = | |
| ((<T extends JSAny?>() => | |
| (T t) => t)<JSAny>()) | |
| .toJS; | |
| Expect.throwsWhen(soundNullSafety, () => anyF(null)); | |
| anyF(zero); | |
| // Make sure function expression is only evaluated once in lowerings. | |
| var evalCount = 0; | |
| jsFunction = (() { | |
| evalCount++; | |
| return () {}; | |
| }()).toJS; | |
| anyF(); | |
| anyF(); | |
| Expect.equals(1, evalCount); | |
| } |