diff --git a/pkg/dev_compiler/lib/runtime/dart_sdk.js b/pkg/dev_compiler/lib/runtime/dart_sdk.js index 67c5d7ae92bb..99b1c89f7514 100644 --- a/pkg/dev_compiler/lib/runtime/dart_sdk.js +++ b/pkg/dev_compiler/lib/runtime/dart_sdk.js @@ -20,6 +20,7 @@ dart_library.library('dart_sdk', null, /* Imports */[ const core = Object.create(null); const isolate = Object.create(null); const js = Object.create(null); + const js_util = Object.create(null); const math = Object.create(null); const mirrors = Object.create(null); const typed_data = Object.create(null); @@ -629,6 +630,7 @@ dart_library.library('dart_sdk', null, /* Imports */[ let dynamicAnddynamicAndFnToObject = () => (dynamicAnddynamicAndFnToObject = dart.constFn(dart.definiteFunctionType(core.Object, [dart.dynamic, dart.dynamic, dynamicTodynamic()])))(); let FToF = () => (FToF = dart.constFn(dart.definiteFunctionType(F => [F, [F]])))(); let FunctionToFunction = () => (FunctionToFunction = dart.constFn(dart.definiteFunctionType(core.Function, [core.Function])))(); + let FunctionAndListTodynamic = () => (FunctionAndListTodynamic = dart.constFn(dart.definiteFunctionType(dart.dynamic, [core.Function, core.List])))(); let TAndTToT = () => (TAndTToT = dart.constFn(dart.definiteFunctionType(T => [T, [T, T]])))(); let TAndTToT$ = () => (TAndTToT$ = dart.constFn(dart.definiteFunctionType(T => [T, [T, T]])))(); let numAndnumTodouble = () => (numAndnumTodouble = dart.constFn(dart.definiteFunctionType(core.double, [core.num, core.num])))(); @@ -33773,6 +33775,109 @@ dart_library.library('dart_sdk', null, /* Imports */[ return ret; }; dart.fn(js.allowInteropCaptureThis, FunctionToFunction()); + js_util.jsify = function(object) { + if (!core.Map.is(object) && !core.Iterable.is(object)) { + dart.throw(new core.ArgumentError("object must be a Map or Iterable")); + } + return js_util._convertDataTree(object); + }; + dart.fn(js_util.jsify, dynamicTodynamic$()); + js_util._convertDataTree = function(data) { + let _convertedObjects = collection.HashMap.identity(); + function _convert(o) { + if (dart.test(_convertedObjects.containsKey(o))) { + return _convertedObjects.get(o); + } + if (core.Map.is(o)) { + let convertedMap = {}; + _convertedObjects.set(o, convertedMap); + for (let key of o[dartx.keys]) { + convertedMap[key] = _convert(o[dartx.get](key)); + } + return convertedMap; + } else if (core.Iterable.is(o)) { + let convertedList = []; + _convertedObjects.set(o, convertedList); + convertedList[dartx.addAll](o[dartx.map](dart.dynamic)(_convert)); + return convertedList; + } else { + return o; + } + } + dart.fn(_convert, dynamicTodynamic$()); + return _convert(data); + }; + dart.fn(js_util._convertDataTree, dynamicTodynamic$()); + js_util.newObject = function() { + return {}; + }; + dart.fn(js_util.newObject, VoidTodynamic$()); + js_util.hasProperty = function(o, name) { + return name in o; + }; + dart.fn(js_util.hasProperty, dynamicAnddynamicTodynamic$()); + js_util.getProperty = function(o, name) { + return o[name]; + }; + dart.fn(js_util.getProperty, dynamicAnddynamicTodynamic$()); + js_util.setProperty = function(o, name, value) { + return o[name] = value; + }; + dart.fn(js_util.setProperty, dynamicAnddynamicAnddynamicTodynamic()); + js_util.callMethod = function(o, method, args) { + return o[method].apply(o, args); + }; + dart.fn(js_util.callMethod, dynamicAndStringAndListTodynamic()); + js_util.instanceof = function(o, type) { + return o instanceof type; + }; + dart.fn(js_util.instanceof, dynamicAndFunctionTodynamic()); + js_util.callConstructor = function(constr, arguments$) { + if (arguments$ == null) { + return new constr(); + } + if (arguments$ instanceof Array) { + let argumentCount = arguments$.length; + switch (argumentCount) { + case 0: + { + return new constr(); + } + case 1: + { + let arg0 = arguments$[0]; + return new constr(arg0); + } + case 2: + { + let arg0 = arguments$[0]; + let arg1 = arguments$[1]; + return new constr(arg0, arg1); + } + case 3: + { + let arg0 = arguments$[0]; + let arg1 = arguments$[1]; + let arg2 = arguments$[2]; + return new constr(arg0, arg1, arg2); + } + case 4: + { + let arg0 = arguments$[0]; + let arg1 = arguments$[1]; + let arg2 = arguments$[2]; + let arg3 = arguments$[3]; + return new constr(arg0, arg1, arg2, arg3); + } + } + } + let args = [null]; + args[dartx.addAll](arguments$); + let factoryFunction = constr.bind.apply(constr, args); + String(factoryFunction); + return new factoryFunction(); + }; + dart.fn(js_util.callConstructor, FunctionAndListTodynamic()); math.E = 2.718281828459045; math.LN10 = 2.302585092994046; math.LN2 = 0.6931471805599453; @@ -84098,6 +84203,7 @@ dart_library.library('dart_sdk', null, /* Imports */[ exports.core = core; exports.isolate = isolate; exports.js = js; + exports.js_util = js_util; exports.math = math; exports.mirrors = mirrors; exports.typed_data = typed_data; diff --git a/pkg/dev_compiler/test/browser/language_tests.js b/pkg/dev_compiler/test/browser/language_tests.js index c92dfd853382..026b5f5adb12 100644 --- a/pkg/dev_compiler/test/browser/language_tests.js +++ b/pkg/dev_compiler/test/browser/language_tests.js @@ -2629,6 +2629,7 @@ 'js_function_getter_trust_types_test': 'unittest', 'js_interop_1_test': 'unittest', 'js_test': 'unittest', + 'js_util_test': 'unittest', 'js_typed_interop_anonymous2_exp_test': 'unittest', 'js_typed_interop_anonymous2_test': 'unittest', 'js_typed_interop_anonymous_exp_test': 'unittest', diff --git a/pkg/dev_compiler/test/codegen/lib/html/js_util_test.dart b/pkg/dev_compiler/test/codegen/lib/html/js_util_test.dart new file mode 100644 index 000000000000..b0130b24d6b6 --- /dev/null +++ b/pkg/dev_compiler/test/codegen/lib/html/js_util_test.dart @@ -0,0 +1,347 @@ +// Copyright (c) 2013, 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. + +@JS() +library js_native_test; + +import 'dart:async'; +import 'dart:html'; +import 'dart:typed_data' show ByteBuffer, Int32List; +import 'dart:indexed_db' show IdbFactory, KeyRange; + +import 'package:js/js.dart'; +import 'package:js/js_util.dart' as js_util; + +import 'package:unittest/unittest.dart'; +import 'package:unittest/html_individual_config.dart'; + +_injectJs() { + final script = new ScriptElement(); + script.type = 'text/javascript'; + script.innerHtml = r""" +var x = 42; + +var _x = 123; + +var myArray = ["value1"]; + +function returnThis() { + return this; +} + +function getTypeOf(o) { + return typeof(o); +} + +function Foo(a) { + this.a = a; +} + +Foo.b = 38; + +Foo.prototype.bar = function() { + return this.a; +} +Foo.prototype.toString = function() { + return "I'm a Foo a=" + this.a; +} + +var container = new Object(); +container.Foo = Foo; + +function checkMap(m, key, value) { + if (m.hasOwnProperty(key)) + return m[key] == value; + else + return false; +} + +"""; + document.body.append(script); +} + +@JS() +external bool checkMap(m, String, value); + +@JS('JSON.stringify') +external String stringify(o); + +@JS('Node') +external get JSNodeType; + +@JS('Element') +external get JSElementType; + +@JS('Text') +external get JSTextType; + +@JS('HTMLCanvasElement') +external get JSHtmlCanvasElementType; + +@JS() +class Foo { + external Foo(num a); + + external num get a; + external num bar(); +} + +@JS('Foo') +external get JSFooType; + +@JS() +@anonymous +class ExampleTypedLiteral { + external factory ExampleTypedLiteral({a, b, JS$_c, JS$class}); + + external get a; + external get b; + external get JS$_c; + external set JS$_c(v); + // Identical to JS$_c but only accessible within the library. + external get _c; + external get JS$class; + external set JS$class(v); +} + +@JS("Object.prototype.hasOwnProperty") +external get _hasOwnProperty; + +bool hasOwnProperty(o, String name) { + return js_util.callMethod(_hasOwnProperty, 'call', [o, name]); +} + +main() { + _injectJs(); + useHtmlIndividualConfiguration(); + + group('js_util.jsify()', () { + test('convert a List', () { + final list = [1, 2, 3, 4, 5, 6, 7, 8]; + var array = js_util.jsify(list); + expect(array is List, isTrue); + expect(identical(array, list), isFalse); + expect(array.length, equals(list.length)); + for (var i = 0; i < list.length; i++) { + expect(array[i], equals(list[i])); + } + }); + + test('convert an Iterable', () { + final set = new Set.from([1, 2, 3, 4, 5, 6, 7, 8]); + var array = js_util.jsify(set); + expect(array is List, isTrue); + expect(array.length, equals(set.length)); + for (var i = 0; i < array.length; i++) { + expect(set.contains(array[i]), isTrue); + } + }); + + test('convert a Map', () { + var map = {'a': 1, 'b': 2, 'c': 3}; + var jsMap = js_util.jsify(map); + expect(jsMap is! List, isTrue); + for (var key in map.keys) { + expect(checkMap(jsMap, key, map[key]), isTrue); + } + }); + + test('deep convert a complex object', () { + final object = { + 'a': [ + 1, + [2, 3] + ], + 'b': {'c': 3, 'd': new Foo(42)}, + 'e': null + }; + var jsObject = js_util.jsify(object); + expect(js_util.getProperty(jsObject, 'a')[0], equals(object['a'][0])); + expect( + js_util.getProperty(jsObject, 'a')[1][0], equals(object['a'][1][0])); + expect( + js_util.getProperty(jsObject, 'a')[1][1], equals(object['a'][1][1])); + expect(js_util.getProperty(js_util.getProperty(jsObject, 'b'), 'c'), + equals(object['b']['c'])); + expect(js_util.getProperty(js_util.getProperty(jsObject, 'b'), 'd'), + equals(object['b']['d'])); + expect( + js_util.callMethod( + js_util.getProperty(js_util.getProperty(jsObject, 'b'), 'd'), + 'bar', []), + equals(42)); + expect(js_util.getProperty(jsObject, 'e'), isNull); + }); + + test('throws if object is not a Map or Iterable', () { + expect(() => js_util.jsify('a'), + throwsA(new isInstanceOf())); + }); + }); + + group('js_util.newObject', () { + test('create', () { + expect(identical(js_util.newObject(), js_util.newObject()), isFalse); + }); + + test('callMethod', () { + var o = js_util.newObject(); + expect(js_util.callMethod(o, 'toString', []), equals('[object Object]')); + expect(stringify(o), equals('{}')); + }); + + test('properties', () { + var o = js_util.newObject(); + expect(js_util.hasProperty(o, 'foo bar'), isFalse); + expect(js_util.hasProperty(o, 'toString'), isTrue); + expect(hasOwnProperty(o, 'toString'), isFalse); + expect(hasOwnProperty(o, 'foo bar'), isFalse); + js_util.setProperty(o, 'foo bar', 42); + expect(hasOwnProperty(o, 'foo bar'), isTrue); + expect(js_util.getProperty(o, 'foo bar'), equals(42)); + expect(js_util.hasProperty(o, 'foo bar'), isTrue); + expect(stringify(o), equals('{"foo bar":42}')); + }); + }); + + group('hasProperty', () { + test('typed object', () { + var f = new Foo(42); + expect(js_util.hasProperty(f, 'a'), isTrue); + expect(js_util.hasProperty(f, 'toString'), isTrue); + js_util.setProperty(f, '__proto__', null); + expect(js_util.hasProperty(f, 'toString'), isFalse); + }); + test('typed literal', () { + var l = + new ExampleTypedLiteral(a: 'x', b: 42, JS$_c: null, JS$class: true); + expect(js_util.hasProperty(l, 'a'), isTrue); + expect(js_util.hasProperty(l, 'b'), isTrue); + expect(js_util.hasProperty(l, '_c'), isTrue); + expect(l.JS$_c, isNull); + expect(js_util.hasProperty(l, 'class'), isTrue); + // JS$_c escapes to _c so the property JS$_c will not exist on the object. + expect(js_util.hasProperty(l, r'JS$_c'), isFalse); + expect(js_util.hasProperty(l, r'JS$class'), isFalse); + expect(l.JS$class, isTrue); + + l = new ExampleTypedLiteral(a: null); + expect(js_util.hasProperty(l, 'a'), isTrue); + expect(js_util.hasProperty(l, 'b'), isFalse); + expect(js_util.hasProperty(l, '_c'), isFalse); + expect(js_util.hasProperty(l, 'class'), isFalse); + + l = new ExampleTypedLiteral(JS$_c: 74); + expect(js_util.hasProperty(l, '_c'), isTrue); + expect(l.JS$_c, equals(74)); + }); + }); + + group('getProperty', () { + test('typed object', () { + var f = new Foo(42); + expect(js_util.getProperty(f, 'a'), equals(42)); + expect(js_util.getProperty(f, 'toString') is Function, isTrue); + js_util.setProperty(f, '__proto__', null); + expect(js_util.getProperty(f, 'toString'), isNull); + }); + + test('typed literal', () { + var l = new ExampleTypedLiteral(a: 'x', b: 42, JS$_c: 7, JS$class: true); + expect(js_util.getProperty(l, 'a'), equals('x')); + expect(js_util.getProperty(l, 'b'), equals(42)); + expect(js_util.getProperty(l, '_c'), equals(7)); + expect(l.JS$_c, equals(7)); + expect(js_util.getProperty(l, 'class'), isTrue); + expect(js_util.getProperty(l, r'JS$_c'), isNull); + expect(js_util.getProperty(l, r'JS$class'), isNull); + }); + }); + + group('setProperty', () { + test('typed object', () { + var f = new Foo(42); + expect(js_util.getProperty(f, 'a'), equals(42)); + js_util.setProperty(f, 'a', 100); + expect(f.a, equals(100)); + expect(js_util.getProperty(f, 'a'), equals(100)); + }); + + test('typed literal', () { + var l = new ExampleTypedLiteral(); + js_util.setProperty(l, 'a', 'foo'); + expect(js_util.getProperty(l, 'a'), equals('foo')); + expect(l.a, equals('foo')); + js_util.setProperty(l, 'a', l); + expect(identical(l.a, l), isTrue); + var list = ['arr']; + js_util.setProperty(l, 'a', list); + expect(identical(l.a, list), isTrue); + l.JS$class = 42; + expect(l.JS$class, equals(42)); + js_util.setProperty(l, 'class', 100); + expect(l.JS$class, equals(100)); + }); + }); + + group('callMethod', () { + test('html object', () { + var canvas = new Element.tag('canvas'); + expect( + identical(canvas.getContext('2d'), + js_util.callMethod(canvas, 'getContext', ['2d'])), + isTrue); + }); + + test('typed object', () { + var f = new Foo(42); + expect(js_util.callMethod(f, 'bar', []), equals(42)); + }); + }); + + group('instanceof', () { + test('html object', () { + var canvas = new Element.tag('canvas'); + expect(js_util.instanceof(canvas, JSNodeType), isTrue); + expect(js_util.instanceof(canvas, JSTextType), isFalse); + expect(js_util.instanceof(canvas, JSElementType), isTrue); + expect(js_util.instanceof(canvas, JSHtmlCanvasElementType), isTrue); + var div = new Element.tag('div'); + expect(js_util.instanceof(div, JSNodeType), isTrue); + expect(js_util.instanceof(div, JSTextType), isFalse); + expect(js_util.instanceof(div, JSElementType), isTrue); + expect(js_util.instanceof(div, JSHtmlCanvasElementType), isFalse); + + var text = new Text('foo'); + expect(js_util.instanceof(text, JSNodeType), isTrue); + expect(js_util.instanceof(text, JSTextType), isTrue); + expect(js_util.instanceof(text, JSElementType), isFalse); + }); + + test('typed object', () { + var f = new Foo(42); + expect(js_util.instanceof(f, JSFooType), isTrue); + expect(js_util.instanceof(f, JSNodeType), isFalse); + }); + + test('typed literal', () { + var l = new ExampleTypedLiteral(); + expect(js_util.instanceof(l, JSFooType), isFalse); + }); + }); + + group('callConstructor', () { + test('html object', () { + var textNode = js_util.callConstructor(JSTextType, ['foo']); + expect(js_util.instanceof(textNode, JSTextType), isTrue); + expect(textNode is Text, isTrue); + expect(textNode.text, equals('foo')); + }); + + test('typed object', () { + Foo f = js_util.callConstructor(JSFooType, [42]); + expect(f.a, equals(42)); + }); + }); +} diff --git a/pkg/dev_compiler/tool/build_sdk.dart b/pkg/dev_compiler/tool/build_sdk.dart index 77c1a5c71b66..7041b383aebe 100644 --- a/pkg/dev_compiler/tool/build_sdk.dart +++ b/pkg/dev_compiler/tool/build_sdk.dart @@ -37,6 +37,7 @@ main(List arguments) { 'dart:core', 'dart:isolate', 'dart:js', + 'dart:js_util', 'dart:math', 'dart:mirrors', 'dart:typed_data', diff --git a/pkg/dev_compiler/tool/input_sdk/lib/_internal/libraries.dart b/pkg/dev_compiler/tool/input_sdk/lib/_internal/libraries.dart index 5587e5bbf12a..2814a088ceda 100644 --- a/pkg/dev_compiler/tool/input_sdk/lib/_internal/libraries.dart +++ b/pkg/dev_compiler/tool/input_sdk/lib/_internal/libraries.dart @@ -76,6 +76,12 @@ const Map LIBRARIES = const { maturity: Maturity.STABLE, dart2jsPath: "js/dart2js/js_dart2js.dart"), + "js_util": const LibraryInfo( + "js_util/dartium/js_util_dartium.dart", + categories: "Client", + maturity: Maturity.STABLE, + dart2jsPath: "js_util/dart2js/js_util_dart2js.dart"), + "math": const LibraryInfo( "math/math.dart", maturity: Maturity.STABLE, diff --git a/pkg/dev_compiler/tool/input_sdk/lib/js_util/dart2js/js_util_dart2js.dart b/pkg/dev_compiler/tool/input_sdk/lib/js_util/dart2js/js_util_dart2js.dart new file mode 100644 index 000000000000..a58745977906 --- /dev/null +++ b/pkg/dev_compiler/tool/input_sdk/lib/js_util/dart2js/js_util_dart2js.dart @@ -0,0 +1,126 @@ +// Copyright (c) 2016, 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. + +/// Utility methods to efficiently manipulate typed JSInterop objects in cases +/// where the name to call is not known at runtime. You should only use these +/// methods when the same effect cannot be achieved with @JS annotations. +/// These methods would be extension methods on JSObject if Dart supported +/// extension methods. +library dart.js_util; + +import 'dart:_foreign_helper' show JS; +import 'dart:collection' show HashMap; + +/// WARNING: performance of this method is much worse than other uitil +/// methods in this library. Only use this method as a last resort. +/// +/// Recursively converts a JSON-like collection of Dart objects to a +/// collection of JavaScript objects and returns a [JsObject] proxy to it. +/// +/// [object] must be a [Map] or [Iterable], the contents of which are also +/// converted. Maps and Iterables are copied to a new JavaScript object. +/// Primitives and other transferrable values are directly converted to their +/// JavaScript type, and all other objects are proxied. +jsify(object) { + if ((object is! Map) && (object is! Iterable)) { + throw new ArgumentError("object must be a Map or Iterable"); + } + return _convertDataTree(object); +} + +_convertDataTree(data) { + var _convertedObjects = new HashMap.identity(); + + _convert(o) { + if (_convertedObjects.containsKey(o)) { + return _convertedObjects[o]; + } + if (o is Map) { + final convertedMap = JS('=Object', '{}'); + _convertedObjects[o] = convertedMap; + for (var key in o.keys) { + JS('=Object', '#[#]=#', convertedMap, key, _convert(o[key])); + } + return convertedMap; + } else if (o is Iterable) { + var convertedList = []; + _convertedObjects[o] = convertedList; + convertedList.addAll(o.map(_convert)); + return convertedList; + } else { + return o; + } + } + + return _convert(data); +} + +newObject() => JS('=Object', '{}'); + +hasProperty(o, name) => JS('bool', '# in #', name, o); +getProperty(o, name) => JS('Object', '#[#]', o, name); +setProperty(o, name, value) => JS('', '#[#]=#', o, name, value); + +callMethod(o, String method, List args) => + JS('Object', '#[#].apply(#, #)', o, method, o, args); + +instanceof(o, Function type) => JS('bool', '# instanceof #', o, type); +callConstructor(Function constr, List arguments) { + if (arguments == null) { + return JS('Object', 'new #()', constr); + } + + if (JS('bool', '# instanceof Array', arguments)) { + int argumentCount = JS('int', '#.length', arguments); + switch (argumentCount) { + case 0: + return JS('Object', 'new #()', constr); + + case 1: + var arg0 = JS('', '#[0]', arguments); + return JS('Object', 'new #(#)', constr, arg0); + + case 2: + var arg0 = JS('', '#[0]', arguments); + var arg1 = JS('', '#[1]', arguments); + return JS('Object', 'new #(#, #)', constr, arg0, arg1); + + case 3: + var arg0 = JS('', '#[0]', arguments); + var arg1 = JS('', '#[1]', arguments); + var arg2 = JS('', '#[2]', arguments); + return JS('Object', 'new #(#, #, #)', constr, arg0, arg1, arg2); + + case 4: + var arg0 = JS('', '#[0]', arguments); + var arg1 = JS('', '#[1]', arguments); + var arg2 = JS('', '#[2]', arguments); + var arg3 = JS('', '#[3]', arguments); + return JS( + 'Object', 'new #(#, #, #, #)', constr, arg0, arg1, arg2, arg3); + } + } + + // The following code solves the problem of invoking a JavaScript + // constructor with an unknown number arguments. + // First bind the constructor to the argument list using bind.apply(). + // The first argument to bind() is the binding of 't', so add 'null' to + // the arguments list passed to apply(). + // After that, use the JavaScript 'new' operator which overrides any binding + // of 'this' with the new instance. + var args = [null]..addAll(arguments); + var factoryFunction = JS('', '#.bind.apply(#, #)', constr, constr, args); + // Without this line, calling factoryFunction as a constructor throws + JS('String', 'String(#)', factoryFunction); + // This could return an UnknownJavaScriptObject, or a native + // object for which there is an interceptor + return JS('Object', 'new #()', factoryFunction); + + // TODO(sra): Investigate: + // + // var jsObj = JS('', 'Object.create(#.prototype)', constr); + // JS('', '#.apply(#, #)', constr, jsObj, + // []..addAll(arguments.map(_convertToJS))); + // return _wrapToDart(jsObj); +}