From b6388f2ea08da228ba5d36e9a6f88521cb878a66 Mon Sep 17 00:00:00 2001 From: Riley Porter Date: Sat, 17 Dec 2022 00:31:27 +0000 Subject: [PATCH] [JS interop] Expose more JavaScript operators in js_util Exposes helper functions in js_util to access the JavaScript operators `delete`, `||`, `&&`, `!`, `!!`, and `typeof`. Change-Id: I0676b143aa004c7b4ed1c6b695b8d1e78a60778d Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/276028 Commit-Queue: Riley Porter Reviewed-by: Sigmund Cherem Reviewed-by: Srujan Gaddam --- CHANGELOG.md | 5 + .../js_shared/lib/js_util_patch.dart | 36 +++++ sdk/lib/js_util/js_util.dart | 21 +++ tests/lib/js/js_util/operator_test.dart | 138 ++++++++++++++++++ tests/lib/lib.status | 1 + tests/lib_2/js/js_util/operator_test.dart | 138 ++++++++++++++++++ tests/lib_2/lib_2.status | 1 + 7 files changed, 340 insertions(+) create mode 100644 tests/lib/js/js_util/operator_test.dart create mode 100644 tests/lib_2/js/js_util/operator_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 8900449ec8d..c2e216b99d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,11 @@ removed. See [#49536](https://github.com/dart-lang/sdk/issues/49536) for details. +#### `dart:js_util` + +- Added several helper functions to access more JavaScript operator, like + `delete` and the `typeof` functionality. + #### `dart:async` - **Breaking change** [#49529][]: diff --git a/sdk/lib/_internal/js_shared/lib/js_util_patch.dart b/sdk/lib/_internal/js_shared/lib/js_util_patch.dart index ff4c8a82a1e..e8149f869b9 100644 --- a/sdk/lib/_internal/js_shared/lib/js_util_patch.dart +++ b/sdk/lib/_internal/js_shared/lib/js_util_patch.dart @@ -345,6 +345,42 @@ bool lessThanOrEqual(Object? first, Object? second) { return JS('bool', '# <= #', first, second); } +@patch +@pragma('dart2js:tryInline') +bool typeofEquals(Object? o, String type) { + return JS('bool', 'typeof # == #', o, type); +} + +@patch +@pragma('dart2js:tryInline') +T not(Object? o) { + return JS('Object', '!#', o); +} + +@patch +@pragma('dart2js:tryInline') +bool isTruthy(Object? o) { + return JS('bool', '!!#', o); +} + +@patch +@pragma('dart2js:tryInline') +T or(Object? first, Object? second) { + return JS('Object|bool', '# || #', first, second); +} + +@patch +@pragma('dart2js:tryInline') +T and(Object? first, Object? second) { + return JS('Object|bool', '# && #', first, second); +} + +@patch +@pragma('dart2js:tryInline') +bool delete(Object o, Object property) { + return JS('bool', 'delete #[#]', o, property); +} + @patch Future promiseToFuture(Object jsPromise) { final completer = Completer(); diff --git a/sdk/lib/js_util/js_util.dart b/sdk/lib/js_util/js_util.dart index 86a96681913..7885c9e1297 100644 --- a/sdk/lib/js_util/js_util.dart +++ b/sdk/lib/js_util/js_util.dart @@ -97,6 +97,27 @@ external bool lessThan(Object? first, Object? second); /// Perform JavaScript less than or equal comparison (`<=`) of two values. external bool lessThanOrEqual(Object? first, Object? second); +/// Perform JavaScript `typeof` operator on the given object and determine if +/// the result is equal to the given type. Exposes the whole `typeof` equal +/// expression to maximize browser optimization. +external bool typeofEquals(Object? o, String type); + +/// Perform JavaScript logical not (`!`) on the given object. +external T not(Object? o); + +/// Determines if the given object is truthy or falsy. +external bool isTruthy(Object? o); + +/// Perform JavaScript logical or comparison (`||`) of two expressions. +external T or(Object? first, Object? second); + +/// Perform JavaScript logical and comparison (`&&`) of two expressions. +external T and(Object? first, Object? second); + +/// Perform JavaScript delete operator (`delete`) on the given property of the +/// given object. +external bool delete(Object o, Object property); + /// Exception for when the promise is rejected with a `null` or `undefined` /// value. /// diff --git a/tests/lib/js/js_util/operator_test.dart b/tests/lib/js/js_util/operator_test.dart new file mode 100644 index 00000000000..50e7dc89dbd --- /dev/null +++ b/tests/lib/js/js_util/operator_test.dart @@ -0,0 +1,138 @@ +// Copyright (c) 2022, 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. + +// Tests invoking JS operators through js_util. + +@JS() +library js_util_operator_test; + +import 'package:js/js.dart'; +import 'package:js/js_util.dart' as js_util; +import 'package:expect/minitest.dart'; + +@JS() +external void eval(String code); + +@JS() +external Object get undefinedObject; + +@JS() +class Foo { + external Foo(); +} + +main() { + eval(r""" + function Foo() {} + """); + + test('typeofEquals', () { + expect(js_util.typeofEquals(5, 'number'), isTrue); + expect(js_util.typeofEquals(5, 'string'), isFalse); + + expect(js_util.typeofEquals('foo', 'string'), isTrue); + expect(js_util.typeofEquals('foo', 'number'), isFalse); + + expect(js_util.typeofEquals(null, 'object'), isTrue); + expect(js_util.typeofEquals(null, 'boolean'), isFalse); + + expect(js_util.typeofEquals(true, 'boolean'), isTrue); + expect(js_util.typeofEquals(true, 'number'), isFalse); + + expect(js_util.typeofEquals(Foo(), 'object'), isTrue); + expect(js_util.typeofEquals(Foo(), 'function'), isFalse); + + expect(js_util.typeofEquals(js_util.newObject(), 'object'), isTrue); + expect(js_util.typeofEquals(js_util.newObject(), 'function'), isFalse); + + expect(js_util.typeofEquals([], 'object'), isTrue); + expect(js_util.typeofEquals([], 'function'), isFalse); + + expect(js_util.typeofEquals(undefinedObject, 'undefined'), isTrue); + expect(js_util.typeofEquals(undefinedObject, 'object'), isFalse); + + expect( + js_util.typeofEquals( + js_util.getProperty(js_util.globalThis, 'Foo'), 'function'), + isTrue); + }); + + test('not', () { + expect(js_util.not(true), isFalse); + expect(js_util.not(false), isTrue); + + expect(js_util.not(null), isTrue); + expect(js_util.not(''), isTrue); + expect(js_util.not(0), isTrue); + expect(js_util.not(undefinedObject), isTrue); + + expect(js_util.not([]), isFalse); + expect(js_util.not({}), isFalse); + expect(js_util.not(js_util.newObject()), isFalse); + expect(js_util.not(Foo()), isFalse); + expect(js_util.not('foo'), isFalse); + expect(js_util.not(5), isFalse); + }); + + test('isTruthy', () { + expect(js_util.isTruthy(true), isTrue); + expect(js_util.isTruthy(false), isFalse); + + expect(js_util.isTruthy(null), isFalse); + expect(js_util.isTruthy(''), isFalse); + expect(js_util.isTruthy(0), isFalse); + expect(js_util.isTruthy(undefinedObject), isFalse); + + expect(js_util.isTruthy([]), isTrue); + expect(js_util.isTruthy({}), isTrue); + expect(js_util.isTruthy(js_util.newObject()), isTrue); + expect(js_util.isTruthy(Foo()), isTrue); + expect(js_util.isTruthy('foo'), isTrue); + expect(js_util.isTruthy(5), isTrue); + }); + + test('or', () { + expect(js_util.or(true, false), isTrue); + expect(js_util.or(true, true), isTrue); + expect(js_util.or(false, true), isTrue); + expect(js_util.or(false, false), isFalse); + + expect(js_util.or('foo', 'bar'), equals('foo')); + expect(js_util.or(null, 'foo'), equals('foo')); + expect(js_util.or(undefinedObject, 'foo'), equals('foo')); + expect(js_util.or(0, 'foo'), equals('foo')); + expect(js_util.or('', 'foo'), equals('foo')); + expect(js_util.or([], 'bar'), equals([])); + var o = js_util.newObject(); + expect(js_util.or(o, 'foo'), equals(o)); + }); + + test('and', () { + expect(js_util.and(true, false), isFalse); + expect(js_util.and(true, true), isTrue); + expect(js_util.and(false, true), isFalse); + expect(js_util.and(false, false), isFalse); + + expect(js_util.and('foo', 'bar'), equals('bar')); + expect(js_util.and(null, 'foo'), equals(null)); + // Should be undefined if we had JS types + expect(js_util.and(undefinedObject, 'foo'), equals(null)); + expect(js_util.and(0, 'foo'), equals(0)); + expect(js_util.and([], 'bar'), equals('bar')); + var o = js_util.newObject(); + expect(js_util.and(o, 'foo'), equals('foo')); + }); + + test('delete', () { + var f = Foo(); + + expect(js_util.delete(f, 'unknownProperty'), isTrue); + + expect(js_util.getProperty(f, 'a'), equals(null)); + js_util.setProperty(f, 'a', 'foo'); + expect(js_util.getProperty(f, 'a'), equals('foo')); + expect(js_util.delete(f, 'a'), isTrue); + expect(js_util.getProperty(f, 'a'), equals(null)); + }); +} diff --git a/tests/lib/lib.status b/tests/lib/lib.status index b1a88e4e29f..6f41bfafc32 100644 --- a/tests/lib/lib.status +++ b/tests/lib/lib.status @@ -52,6 +52,7 @@ js/js_util/async_test: SkipByDesign # Issue 42085. CSP policy disallows injected js/js_util/implicit_downcast_test: SkipByDesign # Issue 42085. CSP policy disallows injected JS code js/js_util/javascriptobject_extensions_test.dart: SkipByDesign # Issue 42085. CSP policy disallows injected JS code js/js_util/jsify_test: SkipByDesign # Issue 42085. CSP policy disallows injected JS code +js/js_util/operator_test: SkipByDesign # Issue 42085. CSP policy disallows injected JS code js/js_util/promise_reject_null_test: SkipByDesign # Issue 42085. CSP policy disallows injected JS code js/js_util/properties_implicit_checks_test: SkipByDesign # Issue 42085. CSP policy disallows injected JS code js/js_util/properties_test: SkipByDesign # Issue 42085. CSP policy disallows injected JS code diff --git a/tests/lib_2/js/js_util/operator_test.dart b/tests/lib_2/js/js_util/operator_test.dart new file mode 100644 index 00000000000..50e7dc89dbd --- /dev/null +++ b/tests/lib_2/js/js_util/operator_test.dart @@ -0,0 +1,138 @@ +// Copyright (c) 2022, 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. + +// Tests invoking JS operators through js_util. + +@JS() +library js_util_operator_test; + +import 'package:js/js.dart'; +import 'package:js/js_util.dart' as js_util; +import 'package:expect/minitest.dart'; + +@JS() +external void eval(String code); + +@JS() +external Object get undefinedObject; + +@JS() +class Foo { + external Foo(); +} + +main() { + eval(r""" + function Foo() {} + """); + + test('typeofEquals', () { + expect(js_util.typeofEquals(5, 'number'), isTrue); + expect(js_util.typeofEquals(5, 'string'), isFalse); + + expect(js_util.typeofEquals('foo', 'string'), isTrue); + expect(js_util.typeofEquals('foo', 'number'), isFalse); + + expect(js_util.typeofEquals(null, 'object'), isTrue); + expect(js_util.typeofEquals(null, 'boolean'), isFalse); + + expect(js_util.typeofEquals(true, 'boolean'), isTrue); + expect(js_util.typeofEquals(true, 'number'), isFalse); + + expect(js_util.typeofEquals(Foo(), 'object'), isTrue); + expect(js_util.typeofEquals(Foo(), 'function'), isFalse); + + expect(js_util.typeofEquals(js_util.newObject(), 'object'), isTrue); + expect(js_util.typeofEquals(js_util.newObject(), 'function'), isFalse); + + expect(js_util.typeofEquals([], 'object'), isTrue); + expect(js_util.typeofEquals([], 'function'), isFalse); + + expect(js_util.typeofEquals(undefinedObject, 'undefined'), isTrue); + expect(js_util.typeofEquals(undefinedObject, 'object'), isFalse); + + expect( + js_util.typeofEquals( + js_util.getProperty(js_util.globalThis, 'Foo'), 'function'), + isTrue); + }); + + test('not', () { + expect(js_util.not(true), isFalse); + expect(js_util.not(false), isTrue); + + expect(js_util.not(null), isTrue); + expect(js_util.not(''), isTrue); + expect(js_util.not(0), isTrue); + expect(js_util.not(undefinedObject), isTrue); + + expect(js_util.not([]), isFalse); + expect(js_util.not({}), isFalse); + expect(js_util.not(js_util.newObject()), isFalse); + expect(js_util.not(Foo()), isFalse); + expect(js_util.not('foo'), isFalse); + expect(js_util.not(5), isFalse); + }); + + test('isTruthy', () { + expect(js_util.isTruthy(true), isTrue); + expect(js_util.isTruthy(false), isFalse); + + expect(js_util.isTruthy(null), isFalse); + expect(js_util.isTruthy(''), isFalse); + expect(js_util.isTruthy(0), isFalse); + expect(js_util.isTruthy(undefinedObject), isFalse); + + expect(js_util.isTruthy([]), isTrue); + expect(js_util.isTruthy({}), isTrue); + expect(js_util.isTruthy(js_util.newObject()), isTrue); + expect(js_util.isTruthy(Foo()), isTrue); + expect(js_util.isTruthy('foo'), isTrue); + expect(js_util.isTruthy(5), isTrue); + }); + + test('or', () { + expect(js_util.or(true, false), isTrue); + expect(js_util.or(true, true), isTrue); + expect(js_util.or(false, true), isTrue); + expect(js_util.or(false, false), isFalse); + + expect(js_util.or('foo', 'bar'), equals('foo')); + expect(js_util.or(null, 'foo'), equals('foo')); + expect(js_util.or(undefinedObject, 'foo'), equals('foo')); + expect(js_util.or(0, 'foo'), equals('foo')); + expect(js_util.or('', 'foo'), equals('foo')); + expect(js_util.or([], 'bar'), equals([])); + var o = js_util.newObject(); + expect(js_util.or(o, 'foo'), equals(o)); + }); + + test('and', () { + expect(js_util.and(true, false), isFalse); + expect(js_util.and(true, true), isTrue); + expect(js_util.and(false, true), isFalse); + expect(js_util.and(false, false), isFalse); + + expect(js_util.and('foo', 'bar'), equals('bar')); + expect(js_util.and(null, 'foo'), equals(null)); + // Should be undefined if we had JS types + expect(js_util.and(undefinedObject, 'foo'), equals(null)); + expect(js_util.and(0, 'foo'), equals(0)); + expect(js_util.and([], 'bar'), equals('bar')); + var o = js_util.newObject(); + expect(js_util.and(o, 'foo'), equals('foo')); + }); + + test('delete', () { + var f = Foo(); + + expect(js_util.delete(f, 'unknownProperty'), isTrue); + + expect(js_util.getProperty(f, 'a'), equals(null)); + js_util.setProperty(f, 'a', 'foo'); + expect(js_util.getProperty(f, 'a'), equals('foo')); + expect(js_util.delete(f, 'a'), isTrue); + expect(js_util.getProperty(f, 'a'), equals(null)); + }); +} diff --git a/tests/lib_2/lib_2.status b/tests/lib_2/lib_2.status index 5c5589ff081..257479978ca 100644 --- a/tests/lib_2/lib_2.status +++ b/tests/lib_2/lib_2.status @@ -52,6 +52,7 @@ js/js_util/async_test: SkipByDesign # Issue 42085. CSP policy disallows injected js/js_util/implicit_downcast_test: SkipByDesign # Issue 42085. CSP policy disallows injected JS code js/js_util/javascriptobject_extensions_test.dart: SkipByDesign # Issue 42085. CSP policy disallows injected JS code js/js_util/jsify_test: SkipByDesign # Issue 42085. CSP policy disallows injected JS code +js/js_util/operator_test: SkipByDesign # Issue 42085. CSP policy disallows injected JS code js/js_util/promise_reject_null_test: SkipByDesign # Issue 42085. CSP policy disallows injected JS code js/js_util/properties_implicit_checks_test: SkipByDesign # Issue 42085. CSP policy disallows injected JS code js/js_util/properties_test: SkipByDesign # Issue 42085. CSP policy disallows injected JS code