[dart:js_interop] Make isA<JSExportedDartFunction>() check that it's a wrapped function

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

isA<JSExportedDartFunction>() used to just check if the object
was a function, leading to a possible runtime error if `toDart`
is called. Fixes that by introducing some helpers and moving
around some functions in the JS compilers to other internal
libraries (public members can't be added to
js_allow_interop_patch.dart). Also fixes a minor issue in dart2js
where `allowInterop`ed functions could successfully invoke
`JSExportedDartFunction.toDart`.

CoreLibraryReviewExempt: Documentation change.
Change-Id: I5a9d7c31d3143eb3fb6ebd3273a4bf06ca329479
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/482967
Reviewed-by: Martin Kustermann <kustermann@google.com>
Commit-Queue: Srujan Gaddam <srujzs@google.com>
Reviewed-by: Nate Biggs <natebiggs@google.com>
This commit is contained in:
Srujan Gaddam
2026-02-24 10:40:43 -08:00
committed by Commit Queue
parent c87555403c
commit 0615d3a467
15 changed files with 158 additions and 46 deletions
+11
View File
@@ -79,6 +79,14 @@ main() {
functions must now also accept an `instantiator` callback to which they
should pass the loaded results.
#### dart2js
- `JSExportedDartFunction.toDart` sometimes incorrectly returned the original
Dart function even if the wrapper JS function was cast from a call to the
deprecated `allowInterop`. Instead, to be consistent with DDC and dart2wasm,
it now throws if the wrapper JS function wasn't a result of `Function.toJS` or
`Function.toJSCaptureThis`.
### Libraries
#### `dart:js_interop`
@@ -92,6 +100,9 @@ main() {
the supertype `Object?`, this change is only breaking if users referred to the
extension name directly, either through applying the extension directly or
through using `show`/`hide` directives.
- `isA<JSExportedDartFunction>()` now checks if the function is actually a JS
wrapper function that is returned from `Function.toJS` or
`Function.toJSCaptureThis`.
[#56905]: https://github.com/dart-lang/sdk/issues/56905
@@ -36,9 +36,11 @@ class SharedInteropTransformer extends Transformer {
final Procedure _isATearoff;
final Procedure _isJSAny;
final Procedure _isJSBoxedDartObject;
final Procedure _isJSExportedDartFunction;
final Procedure _isJSObject;
final Procedure _isNullableJSAny;
final Procedure _isNullableJSBoxedDartObject;
final Procedure _isNullableJSExportedDartFunction;
final Procedure _isNullableJSObject;
final ExtensionTypeDeclaration _jsAny;
final ExtensionTypeDeclaration _jsFunction;
@@ -103,6 +105,8 @@ class SharedInteropTransformer extends Transformer {
),
_isJSBoxedDartObject = _typeEnvironment.coreTypes.index
.getTopLevelProcedure('dart:js_interop', '_isJSBoxedDartObject'),
_isJSExportedDartFunction = _typeEnvironment.coreTypes.index
.getTopLevelProcedure('dart:js_interop', '_isJSExportedDartFunction'),
_isJSObject = _typeEnvironment.coreTypes.index.getTopLevelProcedure(
'dart:js_interop',
'_isJSObject',
@@ -116,6 +120,11 @@ class SharedInteropTransformer extends Transformer {
'dart:js_interop',
'_isNullableJSBoxedDartObject',
),
_isNullableJSExportedDartFunction = _typeEnvironment.coreTypes.index
.getTopLevelProcedure(
'dart:js_interop',
'_isNullableJSExportedDartFunction',
),
_isNullableJSObject = _typeEnvironment.coreTypes.index
.getTopLevelProcedure('dart:js_interop', '_isNullableJSObject'),
_jsAny = _typeEnvironment.coreTypes.index.getExtensionType(
@@ -601,8 +610,6 @@ class SharedInteropTransformer extends Transformer {
Expression? check;
String? typeofString;
String? instanceOfString;
// TODO(srujzs): Add specific check for `JSExportedDartFunction`.
// https://github.com/dart-lang/sdk/issues/62573
// TODO(srujzs): Maybe use `Array.isArray` for `JSArray`.
// https://github.com/dart-lang/sdk/issues/62699
switch (jsTypeName) {
@@ -643,6 +650,18 @@ class SharedInteropTransformer extends Transformer {
Arguments([VariableGet(receiverVar)]),
);
break;
case 'JSExportedDartFunction' when interopTypeDecl == jsType:
// Only do this special case when users are referring directly to the
// `dart:js_interop` type and not some wrapper.
isJSAnyCheck = null;
nullChecksNeeded = false;
check = StaticInvocation(
interopTypeNullable
? _isNullableJSExportedDartFunction
: _isJSExportedDartFunction,
Arguments([VariableGet(receiverVar)]),
);
break;
case 'JSTypedArray' when interopTypeDecl == jsType:
// Only do this special case when users are referring directly to the
// `dart:js_interop` type and not some wrapper.
@@ -200,12 +200,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`, `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.
/// `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
@@ -45,11 +45,13 @@ void test(JSAny any, JSAny? nullableAny, Object obj, Object? nullableObj) {
any.isA<JSArray>();
obj.isA<JSArray?>();
// JSTypedArray and JSBoxedDartObject handled differently.
// Check that there's a specific lowering for some JS types.
any.isA<JSTypedArray>();
any.isA<JSBoxedDartObject>();
any.isA<JSExportedDartFunction>();
obj.isA<JSTypedArray>();
obj.isA<JSBoxedDartObject>();
obj.isA<JSExportedDartFunction>();
// User-defined types.
any.isA<CustomJSAny>();
@@ -76,8 +76,10 @@ static method test(js_::JSAny /* erasure=core::Object */ any, js_::JSAny? /* era
js_::NullableObjectUtilExtension|isA<js_::JSArray<js_::JSAny? /* erasure=core::Object? */>? /* erasure=_interceptors::JSArray<core::Object?>? */>(obj);
js_::NullableObjectUtilExtension|isA<js_::JSTypedArray /* erasure=dart.typed_data.implementation::NativeTypedData */>(any);
js_::NullableObjectUtilExtension|isA<js_::JSBoxedDartObject /* erasure=_interceptors::JSObject */>(any);
js_::NullableObjectUtilExtension|isA<js_::JSExportedDartFunction /* erasure=_interceptors::JavaScriptFunction */>(any);
js_::NullableObjectUtilExtension|isA<js_::JSTypedArray /* erasure=dart.typed_data.implementation::NativeTypedData */>(obj);
js_::NullableObjectUtilExtension|isA<js_::JSBoxedDartObject /* erasure=_interceptors::JSObject */>(obj);
js_::NullableObjectUtilExtension|isA<js_::JSExportedDartFunction /* erasure=_interceptors::JavaScriptFunction */>(obj);
js_::NullableObjectUtilExtension|isA<self::CustomJSAny /* erasure=core::Object */>(any);
js_::NullableObjectUtilExtension|isA<self::CustomJSObject /* erasure=_interceptors::JSObject */>(any);
js_::NullableObjectUtilExtension|isA<self::CustomTypedArray /* erasure=dart.typed_data.implementation::NativeTypedData */>(any);
@@ -76,8 +76,10 @@ static method test(js_::JSAny /* erasure=core::Object */ any, js_::JSAny? /* era
js_::NullableObjectUtilExtension|isA<js_::JSArray<js_::JSAny? /* erasure=core::Object? */>? /* erasure=_interceptors::JSArray<core::Object?>? */>(obj);
js_::NullableObjectUtilExtension|isA<js_::JSTypedArray /* erasure=dart.typed_data.implementation::NativeTypedData */>(any);
js_::NullableObjectUtilExtension|isA<js_::JSBoxedDartObject /* erasure=_interceptors::JSObject */>(any);
js_::NullableObjectUtilExtension|isA<js_::JSExportedDartFunction /* erasure=_interceptors::JavaScriptFunction */>(any);
js_::NullableObjectUtilExtension|isA<js_::JSTypedArray /* erasure=dart.typed_data.implementation::NativeTypedData */>(obj);
js_::NullableObjectUtilExtension|isA<js_::JSBoxedDartObject /* erasure=_interceptors::JSObject */>(obj);
js_::NullableObjectUtilExtension|isA<js_::JSExportedDartFunction /* erasure=_interceptors::JavaScriptFunction */>(obj);
js_::NullableObjectUtilExtension|isA<self::CustomJSAny /* erasure=core::Object */>(any);
js_::NullableObjectUtilExtension|isA<self::CustomJSObject /* erasure=_interceptors::JSObject */>(any);
js_::NullableObjectUtilExtension|isA<self::CustomTypedArray /* erasure=dart.typed_data.implementation::NativeTypedData */>(any);
@@ -77,8 +77,10 @@ static method test(js_::JSAny /* erasure=core::Object */ any, js_::JSAny? /* era
obj == null || js_::_isJSAny(obj) && js_::JSAnyUtilityExtension|instanceOfString(obj as js_::JSAny? /* erasure=core::Object? */, "Array");
!(any == null) && js_::JSAnyUtilityExtension|instanceof(any, js_2::JSObjectUnsafeUtilExtension|callMethodVarArgs<js_::JSFunction /* erasure=_interceptors::JavaScriptFunction */>(js_2::JSObjectUnsafeUtilExtension|[](js_::globalContext, "Object") as js_::JSObject /* erasure=_interceptors::JSObject */, js_::StringToJSString|get#toJS("getPrototypeOf"), <js_::JSAny? /* erasure=core::Object? */>[js_2::JSObjectUnsafeUtilExtension|[](js_::globalContext, "Int8Array") as js_::JSObject /* erasure=_interceptors::JSObject */]));
js_::_isJSBoxedDartObject(any);
js_::_isJSExportedDartFunction(any);
!(obj == null) && (js_::_isJSAny(obj) && js_::JSAnyUtilityExtension|instanceof(obj as js_::JSAny? /* erasure=core::Object? */, js_2::JSObjectUnsafeUtilExtension|callMethodVarArgs<js_::JSFunction /* erasure=_interceptors::JavaScriptFunction */>(js_2::JSObjectUnsafeUtilExtension|[](js_::globalContext, "Object") as js_::JSObject /* erasure=_interceptors::JSObject */, js_::StringToJSString|get#toJS("getPrototypeOf"), <js_::JSAny? /* erasure=core::Object? */>[js_2::JSObjectUnsafeUtilExtension|[](js_::globalContext, "Int8Array") as js_::JSObject /* erasure=_interceptors::JSObject */])));
js_::_isJSBoxedDartObject(obj);
js_::_isJSExportedDartFunction(obj);
!(any == null) && js_::JSAnyUtilityExtension|instanceOfString(any, "library1.CustomJSAny");
!(any == null) && js_::JSAnyUtilityExtension|instanceOfString(any, "library1.CustomJSObject");
!(any == null) && js_::JSAnyUtilityExtension|instanceOfString(any, "library1.CustomTypedArray");
@@ -4,7 +4,7 @@
// Patch file for dart:js_util library.
import 'dart:_foreign_helper' show JS;
import 'dart:_interceptors' show JavaScriptFunction;
import 'dart:_interceptors' show functionToJSProperty, JavaScriptFunction;
import 'dart:_internal' show patch;
import 'dart:_runtime' as dart;
@@ -50,12 +50,6 @@ 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
@@ -73,7 +67,7 @@ JavaScriptFunction _functionToJS0(Function f) {
_callDartFunctionFast0,
f,
);
JS('', '#[#] = #', ret, _functionToJSProperty, f);
JS('', '#[#] = #', ret, functionToJSProperty, f);
return ret;
}
@@ -91,7 +85,7 @@ JavaScriptFunction _functionToJS1(Function f) {
_callDartFunctionFast1,
f,
);
JS('', '#[#] = #', ret, _functionToJSProperty, f);
JS('', '#[#] = #', ret, functionToJSProperty, f);
return ret;
}
@@ -109,7 +103,7 @@ JavaScriptFunction _functionToJS2(Function f) {
_callDartFunctionFast2,
f,
);
JS('', '#[#] = #', ret, _functionToJSProperty, f);
JS('', '#[#] = #', ret, functionToJSProperty, f);
return ret;
}
@@ -127,7 +121,7 @@ JavaScriptFunction _functionToJS3(Function f) {
_callDartFunctionFast3,
f,
);
JS('', '#[#] = #', ret, _functionToJSProperty, f);
JS('', '#[#] = #', ret, functionToJSProperty, f);
return ret;
}
@@ -145,7 +139,7 @@ JavaScriptFunction _functionToJS4(Function f) {
_callDartFunctionFast4,
f,
);
JS('', '#[#] = #', ret, _functionToJSProperty, f);
JS('', '#[#] = #', ret, functionToJSProperty, f);
return ret;
}
@@ -163,7 +157,7 @@ JavaScriptFunction _functionToJS5(Function f) {
_callDartFunctionFast5,
f,
);
JS('', '#[#] = #', ret, _functionToJSProperty, f);
JS('', '#[#] = #', ret, functionToJSProperty, f);
return ret;
}
@@ -183,7 +177,7 @@ JavaScriptFunction _functionToJSN(Function f, int maxLength) {
f,
maxLength,
);
JS('', '#[#] = #', ret, _functionToJSProperty, f);
JS('', '#[#] = #', ret, functionToJSProperty, f);
return ret;
}
@@ -208,7 +202,7 @@ JavaScriptFunction _functionToJSCaptureThisN(Function f, int maxLength) {
f,
maxLength,
);
JS('', '#[#] = #', ret, _functionToJSProperty, f);
JS('', '#[#] = #', ret, functionToJSProperty, f);
return ret;
}
@@ -360,5 +354,5 @@ _callDartFunctionFast5(callback, arg1, arg2, arg3, arg4, arg5, int length) {
}
Function _jsFunctionToDart(JavaScriptFunction f) {
return JS('Function', '#[#]', f, _functionToJSProperty);
return JS('Function', '#[#]', f, functionToJSProperty);
}
@@ -305,6 +305,17 @@ findInterceptorForType(Type? type) {}
/// the dart:rti library because stores information used for type checks.
class JavaScriptFunction extends LegacyJavaScriptObject implements Function {}
// 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)");
/// Returns whether [f] is a wrapped Dart function through `dart:js_interop`'s
/// conversion methods.
bool isJSExportedDartFunction(JavaScriptFunction f) =>
JS('', '#.#', f, functionToJSProperty) != null;
/// Interceptor for JavaScript BigInt primitive values, i.e. values `x` for
/// which `typeof x == "bigint"`.
@JsPeerInterface(name: 'BigInt')
@@ -79,6 +79,17 @@ final String DART_CLOSURE_PROPERTY_NAME = getIsolateAffinityTag(
r'_$dart_dartClosure',
);
/// Present in JS functions that were converted specifically using
/// `dart:js_interop`.
final String DART_CLOSURE_DART_JSINTEROP_PROPERTY_NAME = getIsolateAffinityTag(
r'_$dart_dartClosure_dartJSInterop',
);
/// Returns whether [f] is a wrapped Dart function through `dart:js_interop`'s
/// conversion methods.
bool isJSExportedDartFunction(JavaScriptFunction f) =>
JS('', '#.#', f, DART_CLOSURE_DART_JSINTEROP_PROPERTY_NAME) != null;
getDispatchProperty(object) {
return JS(
'',
@@ -484,7 +495,9 @@ final class JavaScriptFunction extends LegacyJavaScriptObject
const JavaScriptFunction();
String toString() {
var dartClosure = JS('', '#.#', this, DART_CLOSURE_PROPERTY_NAME);
var dartClosure =
JS('', '#.#', this, DART_CLOSURE_PROPERTY_NAME) ??
JS('', '#.#', this, DART_CLOSURE_DART_JSINTEROP_PROPERTY_NAME);
if (dartClosure == null) return super.toString();
return 'JavaScript function for ${dartClosure.toString()}';
}
@@ -5,7 +5,11 @@
// 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, JavaScriptFunction;
import 'dart:_interceptors'
show
DART_CLOSURE_PROPERTY_NAME,
DART_CLOSURE_DART_JSINTEROP_PROPERTY_NAME,
JavaScriptFunction;
import 'dart:_internal' show patch;
import 'dart:_js_helper'
show
@@ -103,7 +107,7 @@ JavaScriptFunction _functionToJS0(Function f) {
DART_CLOSURE_TO_JS(_callDartFunctionFast0),
f,
);
JS('', '#.# = #', result, DART_CLOSURE_PROPERTY_NAME, f);
JS('', '#.# = #', result, DART_CLOSURE_DART_JSINTEROP_PROPERTY_NAME, f);
return result;
}
@@ -123,7 +127,7 @@ JavaScriptFunction _functionToJSCaptureThis0(Function f) {
DART_CLOSURE_TO_JS(_callDartFunctionFast1),
f,
);
JS('', '#.# = #', result, DART_CLOSURE_PROPERTY_NAME, f);
JS('', '#.# = #', result, DART_CLOSURE_DART_JSINTEROP_PROPERTY_NAME, f);
return result;
}
@@ -143,7 +147,7 @@ JavaScriptFunction _functionToJS1(Function f) {
DART_CLOSURE_TO_JS(_callDartFunctionFast1),
f,
);
JS('', '#.# = #', result, DART_CLOSURE_PROPERTY_NAME, f);
JS('', '#.# = #', result, DART_CLOSURE_DART_JSINTEROP_PROPERTY_NAME, f);
return result;
}
@@ -163,7 +167,7 @@ JavaScriptFunction _functionToJSCaptureThis1(Function f) {
DART_CLOSURE_TO_JS(_callDartFunctionFast2),
f,
);
JS('', '#.# = #', result, DART_CLOSURE_PROPERTY_NAME, f);
JS('', '#.# = #', result, DART_CLOSURE_DART_JSINTEROP_PROPERTY_NAME, f);
return result;
}
@@ -183,7 +187,7 @@ JavaScriptFunction _functionToJS2(Function f) {
DART_CLOSURE_TO_JS(_callDartFunctionFast2),
f,
);
JS('', '#.# = #', result, DART_CLOSURE_PROPERTY_NAME, f);
JS('', '#.# = #', result, DART_CLOSURE_DART_JSINTEROP_PROPERTY_NAME, f);
return result;
}
@@ -203,7 +207,7 @@ JavaScriptFunction _functionToJSCaptureThis2(Function f) {
DART_CLOSURE_TO_JS(_callDartFunctionFast3),
f,
);
JS('', '#.# = #', result, DART_CLOSURE_PROPERTY_NAME, f);
JS('', '#.# = #', result, DART_CLOSURE_DART_JSINTEROP_PROPERTY_NAME, f);
return result;
}
@@ -223,7 +227,7 @@ JavaScriptFunction _functionToJS3(Function f) {
DART_CLOSURE_TO_JS(_callDartFunctionFast3),
f,
);
JS('', '#.# = #', result, DART_CLOSURE_PROPERTY_NAME, f);
JS('', '#.# = #', result, DART_CLOSURE_DART_JSINTEROP_PROPERTY_NAME, f);
return result;
}
@@ -243,7 +247,7 @@ JavaScriptFunction _functionToJSCaptureThis3(Function f) {
DART_CLOSURE_TO_JS(_callDartFunctionFast4),
f,
);
JS('', '#.# = #', result, DART_CLOSURE_PROPERTY_NAME, f);
JS('', '#.# = #', result, DART_CLOSURE_DART_JSINTEROP_PROPERTY_NAME, f);
return result;
}
@@ -263,7 +267,7 @@ JavaScriptFunction _functionToJS4(Function f) {
DART_CLOSURE_TO_JS(_callDartFunctionFast4),
f,
);
JS('', '#.# = #', result, DART_CLOSURE_PROPERTY_NAME, f);
JS('', '#.# = #', result, DART_CLOSURE_DART_JSINTEROP_PROPERTY_NAME, f);
return result;
}
@@ -283,7 +287,7 @@ JavaScriptFunction _functionToJSCaptureThis4(Function f) {
DART_CLOSURE_TO_JS(_callDartFunctionFast5),
f,
);
JS('', '#.# = #', result, DART_CLOSURE_PROPERTY_NAME, f);
JS('', '#.# = #', result, DART_CLOSURE_DART_JSINTEROP_PROPERTY_NAME, f);
return result;
}
@@ -303,7 +307,7 @@ JavaScriptFunction _functionToJS5(Function f) {
DART_CLOSURE_TO_JS(_callDartFunctionFast5),
f,
);
JS('', '#.# = #', result, DART_CLOSURE_PROPERTY_NAME, f);
JS('', '#.# = #', result, DART_CLOSURE_DART_JSINTEROP_PROPERTY_NAME, f);
return result;
}
@@ -325,7 +329,7 @@ JavaScriptFunction _functionToJSN(Function f, int maxLength) {
f,
maxLength,
);
JS('', '#.# = #', result, DART_CLOSURE_PROPERTY_NAME, f);
JS('', '#.# = #', result, DART_CLOSURE_DART_JSINTEROP_PROPERTY_NAME, f);
return result;
}
@@ -353,7 +357,7 @@ JavaScriptFunction _functionToJSCaptureThisN(Function f, int maxLength) {
f,
maxLength,
);
JS('', '#.# = #', result, DART_CLOSURE_PROPERTY_NAME, f);
JS('', '#.# = #', result, DART_CLOSURE_DART_JSINTEROP_PROPERTY_NAME, f);
return result;
}
@@ -407,5 +411,5 @@ _callDartFunctionFastN(Function callback, List arguments) {
}
Function _jsFunctionToDart(JavaScriptFunction f) {
return JS('Function', '#.#', f, DART_CLOSURE_PROPERTY_NAME);
return JS('Function', '#.#', f, DART_CLOSURE_DART_JSINTEROP_PROPERTY_NAME);
}
@@ -3,7 +3,12 @@
// BSD-style license that can be found in the LICENSE file.
import 'dart:_foreign_helper' as foreign_helper;
import 'dart:_interceptors' show Interceptor, JavaScriptObject;
import 'dart:_interceptors'
show
Interceptor,
JavaScriptFunction,
JavaScriptObject,
isJSExportedDartFunction;
import 'dart:_internal' show patch;
import 'dart:_js_helper' show createObjectLiteral, staticInteropGlobalContext;
import 'dart:_js_types';
@@ -130,6 +135,14 @@ bool _isJSObject(Object? any) =>
@pragma('dart2js:prefer-inline')
bool _isNullableJSObject(Object? any) => any == null || _isJSObject(any);
@pragma('dart2js:prefer-inline')
bool _isJSExportedDartFunction(Object? any) =>
any is JavaScriptFunction && isJSExportedDartFunction(any);
@pragma('dart2js:prefer-inline')
bool _isNullableJSExportedDartFunction(Object? any) =>
any == null || _isJSExportedDartFunction(any);
// -----------------------------------------------------------------------------
// JSBoxedDartObject <-> Object
@patch
@@ -266,6 +266,13 @@ bool _isJSObject(Object? any) =>
bool _isNullableJSObject(Object? any) => any == null || _isJSObject(any);
bool _isJSExportedDartFunction(Object? any) =>
_isJSAny(any) &&
js_helper.isJSWrappedDartFunction(unsafeCast<JSAny>(any).toExternRef);
bool _isNullableJSExportedDartFunction(Object? any) =>
any == null || _isJSExportedDartFunction(any);
// -----------------------------------------------------------------------------
// JSBoxedDartObject <-> Object
@patch
+4
View File
@@ -888,6 +888,10 @@ extension NullableObjectUtilExtension on Object? {
/// - `JSObject`: `isA<JSObject>` will call an intrinsic function to check
/// that the value is a JS object (`instanceof Object` is insufficient for
/// some objects).
/// - `JSExportedDartFunction`: `isA<JSExportedDartFunction>` will check if
/// the value is a result of a previous
/// [FunctionToJSExportedDartFunction.toJS] or
/// [FunctionToJSExportedDartFunction.toJSCaptureThis] call.
/// - User interop types whose representation types are JS primitive types:
/// This will result in an error to avoid confusion on whether the user
/// interop type is used in the type-check. Use the primitive JS type as the
@@ -43,9 +43,15 @@ extension type ArraySubtype._(JSArray _) implements JSObject {
extension type WrapJSBoxedDartObject(JSBoxedDartObject _)
implements JSBoxedDartObject {}
extension type WrapJSExportedDartFunction(JSExportedDartFunction _)
implements JSExportedDartFunction {}
@JS('WrapJSBoxedDartObject.prototype')
external JSObject get wrapJSBoxedDartObjectPrototype;
@JS('WrapJSExportedDartFunction.prototype')
external JSObject get wrapJSExportedDartFunctionPrototype;
@JS('Object.setPrototypeOf')
external void setPrototypeOf(JSObject obj, JSObject prototype);
@@ -100,19 +106,23 @@ void testNull() {
Expect.isTrue(nil.isA<JSObject?>());
Expect.isTrue(nil.isA<JSAny?>());
Expect.isTrue(nil.isA<JSBoxedDartObject?>());
Expect.isTrue(nil.isA<JSExportedDartFunction?>());
Expect.isFalse(nil.isA<JSString>());
Expect.isFalse(nil.isA<JSObject>());
Expect.isFalse(nil.isA<JSAny>());
Expect.isFalse(nil.isA<JSBoxedDartObject>());
Expect.isFalse(nil.isA<JSExportedDartFunction>());
Object? nilObj = null;
Expect.isTrue(nilObj.isA<JSString?>());
Expect.isTrue(nilObj.isA<JSObject?>());
Expect.isTrue(nilObj.isA<JSAny?>());
Expect.isTrue(nilObj.isA<JSBoxedDartObject?>());
Expect.isTrue(nilObj.isA<JSExportedDartFunction?>());
Expect.isFalse(nilObj.isA<JSString>());
Expect.isFalse(nilObj.isA<JSObject>());
Expect.isFalse(nilObj.isA<JSAny>());
Expect.isFalse(nilObj.isA<JSBoxedDartObject>());
Expect.isFalse(nilObj.isA<JSExportedDartFunction>());
// JS nullish values should behave no differently.
eval('''
globalThis.nullable = null;
@@ -121,10 +131,12 @@ void testNull() {
Expect.isTrue(nullable.isA<JSObject?>());
Expect.isTrue(nullable.isA<JSAny?>());
Expect.isTrue(nullable.isA<JSBoxedDartObject?>());
Expect.isTrue(nullable.isA<JSExportedDartFunction?>());
Expect.isFalse(nullable.isA<JSString>());
Expect.isFalse(nullable.isA<JSObject>());
Expect.isFalse(nullable.isA<JSAny>());
Expect.isFalse(nullable.isA<JSBoxedDartObject>());
Expect.isFalse(nullable.isA<JSExportedDartFunction>());
eval('''
globalThis.nullable = undefined;
''');
@@ -132,10 +144,12 @@ void testNull() {
Expect.isTrue(nullable.isA<JSObject?>());
Expect.isTrue(nullable.isA<JSAny?>());
Expect.isTrue(nullable.isA<JSBoxedDartObject?>());
Expect.isTrue(nullable.isA<JSExportedDartFunction?>());
Expect.isFalse(nullable.isA<JSString>());
Expect.isFalse(nullable.isA<JSObject>());
Expect.isFalse(nullable.isA<JSAny>());
Expect.isFalse(nullable.isA<JSBoxedDartObject>());
Expect.isFalse(nullable.isA<JSExportedDartFunction>());
}
void testPrimitives() {
@@ -292,14 +306,12 @@ void testJSObjects() {
Expect.isTrue(jsFunction.isA<JSFunction?>());
testIsJSObject(jsFunction);
Expect.isFalse(jsFunction.isA<JSNumber>());
// TODO(srujzs): Currently, we can't distinguish between a JS function and an
// exported function. https://github.com/dart-lang/sdk/issues/62573
Expect.isTrue(jsFunction.isA<JSExportedDartFunction>());
Expect.isFalse(jsFunction.isA<JSExportedDartFunction>());
Object jsFunctionObj = jsFunction;
Expect.isTrue(jsFunctionObj.isA<JSFunction>());
Expect.isFalse(jsFunctionObj.isA<JSBoxedDartObject>());
Expect.isTrue(jsFunctionObj.isA<JSExportedDartFunction>());
Expect.isFalse(jsFunctionObj.isA<JSExportedDartFunction>());
// JSExportedDartFunction.
final jsExportedDartFunction = () {}.toJS;
@@ -526,6 +538,22 @@ void testUserTypes() {
Expect.isTrue(wrapJsBox.isA<WrapJSBoxedDartObject?>());
Expect.isTrue(wrapJsBoxObj.isA<WrapJSBoxedDartObject>());
// Test that a type wrapping `JSExportedDartFunction` should have a different
// type-check.
eval('''
class WrapJSExportedDartFunction {}
globalThis.WrapJSExportedDartFunction = WrapJSExportedDartFunction;
''');
final wrapJsEdf = WrapJSExportedDartFunction(() {}.toJS);
Expect.isFalse(wrapJsEdf.isA<WrapJSExportedDartFunction>());
setPrototypeOf(wrapJsEdf, wrapJSExportedDartFunctionPrototype);
Expect.isTrue(wrapJsEdf.isA<WrapJSExportedDartFunction>());
Expect.isTrue(nil.isA<WrapJSExportedDartFunction?>());
Expect.isFalse(nil.isA<WrapJSExportedDartFunction>());
Object? wrapJsEdfObj = wrapJsEdf;
Expect.isTrue(wrapJsEdfObj.isA<WrapJSExportedDartFunction>());
// Test subtyping a type in the browser.
eval('''
class SubtypeArray extends Array {}