[dart:js_interop] Add/fix some js_interop helpers

Minus all the operator-related functionalities, members that
can be worked around using static interop e.g. getPrototypeOf,
and createDartExport/createStaticInteropMock, this bridges the
gap of js_util.

Adds:
- instanceOfString from js_util as an extension methods
- JSObject constructor to replace js_util.newObject
- Unnamed factory constructor to JSAny so users can't extend it

Fixes:
- JSArray.withLength to take an int
- typeofEquals to take a String and return a bool
- instanceof to return a bool

CoreLibraryReviewExempt: Backend-specific library.
Change-Id: I7db1651f641a4fc84392957dfa7ad64904f110e8
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/326691
Reviewed-by: Sigmund Cherem <sigmund@google.com>
Commit-Queue: Srujan Gaddam <srujzs@google.com>
This commit is contained in:
Srujan Gaddam 2023-09-23 18:32:59 +00:00 committed by Commit Queue
parent e130bb36ce
commit 74c407da2f
14 changed files with 90 additions and 40 deletions

View file

@ -167,6 +167,14 @@ constraint][language version] lower bound to 3.2 or greater (`sdk: '^3.2.0'`).
conflates the two values and treats them both as Dart null. Therefore, these
two helper methods should not be used on dart2wasm and will throw to avoid
potentially erroneous code.
- **Breaking Change on `dart:js_interop` `typeofEquals` and `instanceof`**:
Both APIs now return a `bool` instead of a `JSBoolean`. `typeofEquals` also
now takes in a `String` instead of a `JSString`.
- **Breaking Change on `dart:js_interop` `JSAny` and `JSObject`**:
These types can only be implemented, and no longer extended, by user
`@staticInterop` types.
- **Breaking Change on `dart:js_interop` `JSArray.withLength`**:
This API now takes in an `int` instead of `JSNumber`.
### Tools

View file

@ -997,3 +997,6 @@ external Object jsObjectSetPrototypeOf(@notNull Object obj, Object? prototype);
/// inline the call so this will be an extra level of indirection. DDC manually
/// inlines this method in the compiler instead.
external Object get staticInteropGlobalContext;
/// Return a fresh object literal.
T createObjectLiteral<T>() => JS('PlainJavaScriptObject', '{}');

View file

@ -3376,3 +3376,6 @@ abstract class TrustedGetRuntimeType {}
/// This should match the global context that non-static interop members use.
Object get staticInteropGlobalContext =>
JS('creates:;returns:Object;depends:none;effects:none;gvn:true', 'self');
/// Return a fresh object literal.
T createObjectLiteral<T>() => JS('PlainJavaScriptObject', '{}');

View file

@ -19,7 +19,7 @@ JSObject get globalContext => staticInteropGlobalContext as JSObject;
extension NullableUndefineableJSAnyExtension on JSAny? {
@patch
@pragma('dart2js:prefer-inline')
bool get isUndefined => js_util.typeofEquals(this, 'undefined');
bool get isUndefined => typeofEquals('undefined');
@patch
@pragma('dart2js:prefer-inline')
@ -27,7 +27,7 @@ extension NullableUndefineableJSAnyExtension on JSAny? {
@patch
@pragma('dart2js:prefer-inline')
JSBoolean typeofEquals(JSString typeString) =>
bool typeofEquals(String typeString) =>
foreign_helper.JS('bool', 'typeof # === #', this, typeString);
@patch
@ -48,7 +48,7 @@ extension NullableObjectUtilExtension on Object? {
extension JSObjectUtilExtension on JSObject {
@patch
@pragma('dart2js:prefer-inline')
JSBoolean instanceof(JSFunction constructor) =>
bool instanceof(JSFunction constructor) =>
foreign_helper.JS('bool', '# instanceof #', this, constructor);
}

View file

@ -30,14 +30,21 @@
library _js_types;
import 'dart:_js_annotations';
import 'dart:_js_helper' show createObjectLiteral;
@JS()
@staticInterop
class JSAny {}
class JSAny {
// Unnamed factory constructor so users can only implement JSAny.
external factory JSAny._();
}
@JS()
@staticInterop
class JSObject implements JSAny {}
class JSObject implements JSAny {
/// Returns a new object literal.
factory JSObject() => createObjectLiteral<JSObject>();
}
@JS()
@staticInterop
@ -51,8 +58,7 @@ class JSExportedDartFunction implements JSFunction {}
@staticInterop
class JSArray implements JSObject {
external factory JSArray();
// TODO(srujzs): Should we make this an `int`?
external factory JSArray.withLength(JSNumber length);
external factory JSArray.withLength(int length);
}
@JS()

View file

@ -5,7 +5,11 @@
import 'dart:_foreign_helper' show JS;
import 'dart:_internal' show patch;
import 'dart:_js_helper'
show convertDartClosureToJS, assertInterop, assertInteropArgs;
show
assertInterop,
assertInteropArgs,
convertDartClosureToJS,
createObjectLiteral;
import 'dart:collection' show HashMap;
import 'dart:async' show Completer;
import 'dart:typed_data';
@ -67,7 +71,7 @@ dynamic jsify(Object? object) {
Object get globalThis => JS('', 'globalThis');
@patch
T newObject<T>() => JS('PlainJavaScriptObject', '{}');
T newObject<T>() => createObjectLiteral<T>();
@patch
bool hasProperty(Object o, Object name) => JS('bool', '# in #', name, o);

View file

@ -523,7 +523,7 @@ List<int> jsIntTypedArrayToDartIntTypedData(
JSArray toJSArray(List<JSAny?> list) {
int length = list.length;
JSArray result = JSArray.withLength(length.toJS);
JSArray result = JSArray.withLength(length);
for (int i = 0; i < length; i++) {
result[i.toJS] = list[i];
}

View file

@ -41,9 +41,10 @@ extension NullableUndefineableJSAnyExtension on JSAny? {
"this API should not be used when compiling to Wasm.");
@patch
JSBoolean typeofEquals(JSString type) =>
_box<JSBoolean>(js_helper.JS<WasmExternRef?>(
'(o, t) => typeof o === t', this?.toExternRef, type.toExternRef));
bool typeofEquals(String type) =>
_box<JSBoolean>(js_helper.JS<WasmExternRef?>('(o, t) => typeof o === t',
this?.toExternRef, type.toJS.toExternRef))
.toDart;
@patch
Object? dartify() => js_util.dartify(this);
@ -60,9 +61,10 @@ extension NullableObjectUtilExtension on Object? {
@patch
extension JSObjectUtilExtension on JSObject {
@patch
JSBoolean instanceof(JSFunction constructor) =>
bool instanceof(JSFunction constructor) =>
_box<JSBoolean>(js_helper.JS<WasmExternRef?>(
'(o, c) => o instanceof c', toExternRef, constructor.toExternRef));
'(o, c) => o instanceof c', toExternRef, constructor.toExternRef))
.toDart;
}
/// [JSExportedDartFunction] <-> [Function]

View file

@ -77,11 +77,17 @@ part 'js_typed_array.dart';
/// JS and Wasm backends until we have a better story here.
@JS()
@staticInterop
class JSAny {}
class JSAny {
// Unnamed factory constructor so users can only implement JSAny.
external factory JSAny._();
}
@JS()
@staticInterop
class JSObject implements JSAny {}
class JSObject implements JSAny {
/// Returns a new object literal.
factory JSObject() => js.JSValue(js.newObjectRaw()) as JSObject;
}
@JS()
@staticInterop
@ -101,8 +107,7 @@ class JSPromise implements JSObject {
@staticInterop
class JSArray implements JSObject {
external factory JSArray();
// TODO(srujzs): Should we make this an `int`?
external factory JSArray.withLength(JSNumber length);
external factory JSArray.withLength(int length);
}
@JS()

View file

@ -178,7 +178,7 @@ extension NullableUndefineableJSAnyExtension on JSAny? {
bool get isUndefinedOrNull => this == null;
bool get isDefinedAndNotNull => !isUndefinedOrNull;
external JSBoolean typeofEquals(JSString typeString);
external bool typeofEquals(String typeString);
/// Effectively the inverse of [jsify], [dartify] Takes a JavaScript object,
/// and converts it to a Dart based object. Only JS primitives, arrays, or
@ -195,7 +195,14 @@ extension NullableObjectUtilExtension on Object? {
/// Utility extensions for [JSObject].
extension JSObjectUtilExtension on JSObject {
external JSBoolean instanceof(JSFunction constructor);
external bool instanceof(JSFunction constructor);
/// Like [instanceof], but only takes a [String] for the constructor name,
/// which is then looked up in the [globalContext].
bool instanceOfString(String constructorName) {
final constructor = globalContext[constructorName] as JSFunction?;
return constructor != null && instanceof(constructor);
}
}
/// The type of `JSUndefined` when returned from functions. Unlike pure JS,

View file

@ -309,7 +309,7 @@ void syncTests() {
expect(nullAny, null);
expect(nullAny.isUndefinedOrNull, true);
expect(nullAny.isDefinedAndNotNull, false);
expect(typeofEquals(nullAny, 'object'), true);
expect(nullAny.typeofEquals('object'), true);
if (isJSBackend) {
expect(undefinedAny.isNull, false);
expect(undefinedAny.isUndefined, true);
@ -317,15 +317,15 @@ void syncTests() {
expect(undefinedAny.isUndefinedOrNull, true);
expect(undefinedAny.isDefinedAndNotNull, false);
if (isJSBackend) {
expect(typeofEquals(undefinedAny, 'undefined'), true);
expect(undefinedAny.typeofEquals('undefined'), true);
expect(definedNonNullAny.isNull, false);
expect(definedNonNullAny.isUndefined, false);
} else {
expect(typeofEquals(undefinedAny, 'object'), true);
expect(undefinedAny.typeofEquals('object'), true);
}
expect(definedNonNullAny.isUndefinedOrNull, false);
expect(definedNonNullAny.isDefinedAndNotNull, true);
expect(typeofEquals(definedNonNullAny, 'object'), true);
expect(definedNonNullAny.typeofEquals('object'), true);
}
@JS()
@ -436,8 +436,7 @@ Future<void> asyncTests() async {
} catch (e) {
expect(e is JSObject, true);
final jsError = e as JSObject;
expect(jsError.instanceof(globalContext['Error'] as JSFunction).toDart,
true);
expect(jsError.instanceof(globalContext['Error'] as JSFunction), true);
expect((jsError['error'] as JSBoxedDartObject).toDart is Exception, true);
StackTrace.fromString((jsError['stack'] as JSString).toDart);
}
@ -462,8 +461,7 @@ Future<void> asyncTests() async {
} catch (e) {
expect(e is JSObject, true);
final jsError = e as JSObject;
expect(jsError.instanceof(globalContext['Error'] as JSFunction).toDart,
true);
expect(jsError.instanceof(globalContext['Error'] as JSFunction), true);
expect((jsError['error'] as JSBoxedDartObject).toDart is Exception, true);
StackTrace.fromString((jsError['stack'] as JSString).toDart);
}

View file

@ -10,6 +10,10 @@ import 'dart:js_interop';
@JS()
@staticInterop
class ExtendsJSAny extends JSAny {}
// ^
// [web] The superclass, 'JSAny', has no unnamed constructor that takes no arguments.
// ^^^^^
// [analyzer] COMPILE_TIME_ERROR.NO_GENERATIVE_CONSTRUCTORS_IN_SUPERCLASS
@JS()
@staticInterop
@ -18,6 +22,10 @@ class ImplementsJSAny implements JSAny {}
@JS()
@staticInterop
class ExtendsJSObject extends JSObject {}
// ^
// [web] The superclass, 'JSObject', has no unnamed constructor that takes no arguments.
// ^^^^^^^^
// [analyzer] COMPILE_TIME_ERROR.NO_GENERATIVE_CONSTRUCTORS_IN_SUPERCLASS
@JS()
@staticInterop

View file

@ -4,9 +4,6 @@
import 'dart:js_interop';
import 'dart:js_interop_unsafe';
// TODO(joshualitt): Once we reify JS types on JS backends, we can add a
// constructor to [JSObject].
import 'dart:js_util' show newObject;
import 'dart:typed_data';
import 'package:expect/expect.dart';
@ -15,7 +12,7 @@ import 'package:expect/expect.dart';
external void eval(String code);
void createObjectTest() {
JSObject o = newObject<JSObject>();
JSObject o = JSObject();
Expect.isFalse(o.hasProperty('foo'.toJS).toDart);
o['foo'] = 'bar'.toJS;
Expect.isTrue(o.hasProperty('foo'.toJS).toDart);
@ -25,8 +22,8 @@ void createObjectTest() {
void equalTest() {
// Different objects aren't equal.
{
JSObject o1 = newObject<JSObject>();
JSObject o2 = newObject<JSObject>();
JSObject o1 = JSObject();
JSObject o2 = JSObject();
Expect.notEquals(o1, o2);
}
@ -82,11 +79,10 @@ void typeofTest() {
'symbol'
};
void test(String property, String expectedType) {
Expect.isTrue(
globalContext[property]?.typeofEquals(expectedType.toJS).toDart);
Expect.isTrue(globalContext[property]?.typeofEquals(expectedType));
for (final type in types) {
if (type != expectedType) {
Expect.isFalse(globalContext[property]?.typeofEquals(type.toJS).toDart);
Expect.isFalse(globalContext[property]?.typeofEquals(type));
}
}
}
@ -112,8 +108,10 @@ void instanceOfTest() {
JSObject obj = gc['obj'] as JSObject;
JSFunction jsClass1Constructor = gc['JSClass1'] as JSFunction;
JSFunction jsClass2Constructor = gc['JSClass2'] as JSFunction;
Expect.isTrue(obj.instanceof(jsClass1Constructor).toDart);
Expect.isFalse(obj.instanceof(jsClass2Constructor).toDart);
Expect.isTrue(obj.instanceof(jsClass1Constructor));
Expect.isFalse(obj.instanceof(jsClass2Constructor));
Expect.isTrue(obj.instanceOfString('JSClass1'));
Expect.isFalse(obj.instanceOfString('JSClass2'));
}
void _expectIterableEquals(Iterable<Object?> l, Iterable<Object?> r) {

View file

@ -10,6 +10,10 @@ import 'dart:js_interop';
@JS()
@staticInterop
class ExtendsJSAny extends JSAny {}
// ^
// [web] The superclass, 'JSAny', has no unnamed constructor that takes no arguments.
// ^^^^^
// [analyzer] COMPILE_TIME_ERROR.NO_GENERATIVE_CONSTRUCTORS_IN_SUPERCLASS
@JS()
@staticInterop
@ -18,6 +22,10 @@ class ImplementsJSAny implements JSAny {}
@JS()
@staticInterop
class ExtendsJSObject extends JSObject {}
// ^
// [web] The superclass, 'JSObject', has no unnamed constructor that takes no arguments.
// ^^^^^^^^
// [analyzer] COMPILE_TIME_ERROR.NO_GENERATIVE_CONSTRUCTORS_IN_SUPERCLASS
@JS()
@staticInterop