[js_util] Add `dartify` and some helpers.

Change-Id: I40822d87a7fef9e4de563ccb73046eee78f34b21
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/243846
Reviewed-by: Srujan Gaddam <srujzs@google.com>
Reviewed-by: Riley Porter <rileyporter@google.com>
Commit-Queue: Joshua Litt <joshualitt@google.com>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c44a181..5a3cc15 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@
 - Add `connectionState` attribute and `connectionstatechange` listener to
   `RtcPeerConnection`.
 
+#### `dart:js_util`
+
+- Added `dartify` and a number of minor helper functions.
+
 ### Tools
 
 #### Linter
diff --git a/sdk/lib/js_util/js_util.dart b/sdk/lib/js_util/js_util.dart
index ce77e95..0f57f47 100644
--- a/sdk/lib/js_util/js_util.dart
+++ b/sdk/lib/js_util/js_util.dart
@@ -417,3 +417,76 @@
   JS('', '#.then(#, #)', jsPromise, success, error);
   return completer.future;
 }
+
+Object? _getConstructor(String constructorName) =>
+    getProperty(globalThis, constructorName);
+
+/// Like [instanceof] only takes a [String] for the object name instead of a
+/// constructor object.
+bool instanceOfString(Object? element, String objectType) {
+  Object? constructor = _getConstructor(objectType);
+  return constructor != null && instanceof(element, constructor);
+}
+
+/// Returns the prototype of a given object. Equivalent to
+/// `Object.getPrototypeOf`.
+Object? objectGetPrototypeOf(Object? object) =>
+    JS('', 'Object.getPrototypeOf(#)', object);
+
+/// Returns the `Object` prototype. Equivalent to `Object.prototype`.
+Object? get objectPrototype => JS('', 'Object.prototype');
+
+/// Returns the keys for a given object. Equivalent to `Object.keys(object)`.
+List<Object?> objectKeys(Object? object) => JS('', 'Object.keys(#)', object);
+
+/// Returns `true` if a given object is a JavaScript array.
+bool isJavaScriptArray(value) => instanceOfString(value, 'Array');
+
+/// Returns `true` if a given object is a simple JavaScript object.
+bool isJavaScriptSimpleObject(value) {
+  final Object? proto = objectGetPrototypeOf(value);
+  return proto == null || proto == objectPrototype;
+}
+
+/// Effectively the inverse of [jsify], [dartify] Takes a JavaScript object, and
+/// converts it to a Dart based object. Only JS primitives, arrays, or 'map'
+/// like JS objects are supported.
+Object? dartify(Object? o) {
+  var _convertedObjects = HashMap.identity();
+  Object? convert() {
+    if (_convertedObjects.containsKey(o)) {
+      return _convertedObjects[o];
+    }
+    if (o == null || o is bool || o is num || o is String) return o;
+    if (isJavaScriptSimpleObject(o)) {
+      Map<Object?, Object?> dartObject = {};
+      _convertedObjects[o] = dartObject;
+      List<Object?> originalKeys = objectKeys(o);
+      List<Object?> dartKeys = [];
+      for (Object? key in originalKeys) {
+        dartKeys.add(dartify(key));
+      }
+      for (int i = 0; i < originalKeys.length; i++) {
+        Object? jsKey = originalKeys[i];
+        Object? dartKey = dartKeys[i];
+        if (jsKey != null) {
+          dartObject[dartKey] = dartify(getProperty(o, jsKey));
+        }
+      }
+      return dartObject;
+    }
+    if (isJavaScriptArray(o)) {
+      List<Object?> dartObject = [];
+      _convertedObjects[o] = dartObject;
+      int length = getProperty(o, 'length');
+      for (int i = 0; i < length; i++) {
+        dartObject.add(dartify(getProperty(o, i)));
+      }
+      return dartObject;
+    }
+    throw ArgumentError(
+        "JavaScriptObject $o must be a primitive, simple object, or array");
+  }
+
+  return convert();
+}
diff --git a/tests/lib/js/js_util/dartify_test.dart b/tests/lib/js/js_util/dartify_test.dart
new file mode 100644
index 0000000..9fef877
--- /dev/null
+++ b/tests/lib/js/js_util/dartify_test.dart
@@ -0,0 +1,88 @@
+// 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 the dartify functionality of the js_util library.
+
+@JS()
+library js_util_jsify_test;
+
+import 'package:js/js.dart';
+import 'package:js/js_util.dart' as js_util;
+import 'package:expect/expect.dart';
+import 'package:expect/minitest.dart';
+
+@JS()
+external void eval(String code);
+
+main() {
+  eval(r"""
+    globalThis.arrayData = [1, 2, false, 4, 'hello', 6, [1, 2], {'foo': 'bar'}];
+    globalThis.recArrayData = [];
+    globalThis.recArrayData = [globalThis.recArrayData];
+    globalThis.objectData = {
+      'a': 1,
+      'b': [1, 2, 3],
+      'c': {
+        'a': true,
+        'b': 'foo',
+      },
+    };
+    globalThis.recObjectData = {};
+    globalThis.recObjectData = {'foo': globalThis.recObjectData}
+    globalThis.throwData = function() {};
+    """);
+
+  test('convert an array', () {
+    Object? jsArray = js_util.getProperty(js_util.globalThis, 'arrayData');
+    Object? dartArray = js_util.dartify(jsArray);
+    List<Object?> expectedValues = [
+      1,
+      2,
+      false,
+      4,
+      'hello',
+      6,
+      [1, 2],
+      {'foo': 'bar'}
+    ];
+    Expect.deepEquals(expectedValues, dartArray);
+  });
+
+  test('convert a recursive array', () {
+    Object? jsArray = js_util.getProperty(js_util.globalThis, 'recArrayData');
+    Object? dartArray = js_util.dartify(jsArray);
+    List<Object?> expectedValues = [[]];
+    Expect.deepEquals(expectedValues, dartArray);
+  });
+
+  test('convert an object literal', () {
+    Object? jsObject = js_util.getProperty(js_util.globalThis, 'objectData');
+    Object? dartObject = js_util.dartify(jsObject);
+    Map<Object?, Object?> expectedValues = {
+      'a': 1,
+      'b': [1, 2, 3],
+      'c': {
+        'a': true,
+        'b': 'foo',
+      },
+    };
+    Expect.deepEquals(expectedValues, dartObject);
+  });
+
+  test('convert a recursive object literal', () {
+    Object? jsObject = js_util.getProperty(js_util.globalThis, 'recObjectData');
+    Object? dartObject = js_util.dartify(jsObject);
+    Map<Object?, Object?> expectedValues = {
+      'foo': {},
+    };
+    Expect.deepEquals(expectedValues, dartObject);
+  });
+
+  test('throws if object is not an object literal or array', () {
+    expect(
+        () => js_util
+            .dartify(js_util.getProperty(js_util.globalThis, 'throwData')),
+        throwsArgumentError);
+  });
+}
diff --git a/tests/lib/lib_dart2js.status b/tests/lib/lib_dart2js.status
index b1d2d3b..c7caca5 100644
--- a/tests/lib/lib_dart2js.status
+++ b/tests/lib/lib_dart2js.status
@@ -88,6 +88,7 @@
 html/js_typed_interop_window_property_test: SkipByDesign
 html/js_util_test: SkipByDesign
 html/postmessage_structured_test: SkipByDesign
+js/js_util/dartify_test: SkipByDesign
 
 [ $compiler == dart2js && ($runtime == chrome || $runtime == ff) ]
 async/slow_consumer2_test: SkipSlow # Times out. Issue 22050
diff --git a/tests/lib_2/js/js_util/dartify_test.dart b/tests/lib_2/js/js_util/dartify_test.dart
new file mode 100644
index 0000000..9fef877
--- /dev/null
+++ b/tests/lib_2/js/js_util/dartify_test.dart
@@ -0,0 +1,88 @@
+// 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 the dartify functionality of the js_util library.
+
+@JS()
+library js_util_jsify_test;
+
+import 'package:js/js.dart';
+import 'package:js/js_util.dart' as js_util;
+import 'package:expect/expect.dart';
+import 'package:expect/minitest.dart';
+
+@JS()
+external void eval(String code);
+
+main() {
+  eval(r"""
+    globalThis.arrayData = [1, 2, false, 4, 'hello', 6, [1, 2], {'foo': 'bar'}];
+    globalThis.recArrayData = [];
+    globalThis.recArrayData = [globalThis.recArrayData];
+    globalThis.objectData = {
+      'a': 1,
+      'b': [1, 2, 3],
+      'c': {
+        'a': true,
+        'b': 'foo',
+      },
+    };
+    globalThis.recObjectData = {};
+    globalThis.recObjectData = {'foo': globalThis.recObjectData}
+    globalThis.throwData = function() {};
+    """);
+
+  test('convert an array', () {
+    Object? jsArray = js_util.getProperty(js_util.globalThis, 'arrayData');
+    Object? dartArray = js_util.dartify(jsArray);
+    List<Object?> expectedValues = [
+      1,
+      2,
+      false,
+      4,
+      'hello',
+      6,
+      [1, 2],
+      {'foo': 'bar'}
+    ];
+    Expect.deepEquals(expectedValues, dartArray);
+  });
+
+  test('convert a recursive array', () {
+    Object? jsArray = js_util.getProperty(js_util.globalThis, 'recArrayData');
+    Object? dartArray = js_util.dartify(jsArray);
+    List<Object?> expectedValues = [[]];
+    Expect.deepEquals(expectedValues, dartArray);
+  });
+
+  test('convert an object literal', () {
+    Object? jsObject = js_util.getProperty(js_util.globalThis, 'objectData');
+    Object? dartObject = js_util.dartify(jsObject);
+    Map<Object?, Object?> expectedValues = {
+      'a': 1,
+      'b': [1, 2, 3],
+      'c': {
+        'a': true,
+        'b': 'foo',
+      },
+    };
+    Expect.deepEquals(expectedValues, dartObject);
+  });
+
+  test('convert a recursive object literal', () {
+    Object? jsObject = js_util.getProperty(js_util.globalThis, 'recObjectData');
+    Object? dartObject = js_util.dartify(jsObject);
+    Map<Object?, Object?> expectedValues = {
+      'foo': {},
+    };
+    Expect.deepEquals(expectedValues, dartObject);
+  });
+
+  test('throws if object is not an object literal or array', () {
+    expect(
+        () => js_util
+            .dartify(js_util.getProperty(js_util.globalThis, 'throwData')),
+        throwsArgumentError);
+  });
+}
diff --git a/tests/lib_2/lib_2_dart2js.status b/tests/lib_2/lib_2_dart2js.status
index 1f09063..39852b5 100644
--- a/tests/lib_2/lib_2_dart2js.status
+++ b/tests/lib_2/lib_2_dart2js.status
@@ -86,6 +86,7 @@
 html/js_typed_interop_window_property_test: SkipByDesign
 html/js_util_test: SkipByDesign
 html/postmessage_structured_test: SkipByDesign
+js/js_util/dartify_test: SkipByDesign
 
 [ $compiler == dart2js && ($runtime == chrome || $runtime == ff) ]
 async/slow_consumer2_test: SkipSlow # Times out. Issue 22050