Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(298)

Unified Diff: sdk/lib/js/dartium/js_dartium.dart

Issue 1194643002: Enhance dart:js interop in a backwards compatible manner. List objects can now be passed back and f… (Closed) Base URL: git@github.com:dart-lang/sdk.git@master
Patch Set: Created 5 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « sdk/lib/html/dartium/html_dartium.dart ('k') | tests/html/html.status » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: sdk/lib/js/dartium/js_dartium.dart
diff --git a/sdk/lib/js/dartium/js_dartium.dart b/sdk/lib/js/dartium/js_dartium.dart
index e7f3004c686d8efbc0b5bdc038bbb28178f2f5d2..57243c5349bb2bdcdf98185cf55bc865e4897d5a 100644
--- a/sdk/lib/js/dartium/js_dartium.dart
+++ b/sdk/lib/js/dartium/js_dartium.dart
@@ -89,11 +89,386 @@ library dart.js;
import 'dart:collection' show ListMixin;
import 'dart:nativewrappers';
+import 'dart:math' as math;
+import 'dart:mirrors' as mirrors;
+
+// Pretend we are always in checked mode as we aren't interested in users
+// running Dartium code outside of checked mode.
+final bool CHECK_JS_INVOCATIONS = true;
+
+final _allowedMethods = new Map<Symbol, _DeclarationSet>();
+final _allowedGetters = new Map<Symbol, _DeclarationSet>();
+final _allowedSetters = new Map<Symbol, _DeclarationSet>();
+
+final _jsInterfaceTypes = new Set<Type>();
+Iterable<Type> get jsInterfaceTypes => _jsInterfaceTypes;
+
+/// A collection of methods where all methods have the same name.
+/// This class is intended to optimize whether a specific invocation is
+/// appropritate for at least some of the methods in the collection.
+class _DeclarationSet {
+ _DeclarationSet() : _members = <mirrors.DeclarationMirror>[];
+
+ static bool _checkType(obj, mirrors.TypeMirror type) {
+ if (obj == null) return true;
+ return mirrors.reflectType(obj.runtimeType).isSubtypeOf(type);
+ }
+
+ /// Returns whether the return [value] has a type is consistent with the
+ /// return type from at least one of the members matching the DeclarationSet.
+ bool _checkReturnType(value) {
+ if (value == null) return true;
+ var valueMirror = mirrors.reflectType(value.runtimeType);
+ for (var member in _members) {
+ if (member is mirrors.VariableMirror || member.isGetter) {
+ // TODO(jacobr): actually check return types for getters that return
+ // function types.
+ return true;
+ } else {
+ if (valueMirror.isSubtypeOf(member.returnType)) return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Check whether the [invocation] is consistent with the [member] mirror.
+ */
+ bool _checkDeclaration(
+ Invocation invocation, mirrors.DeclarationMirror member) {
+ if (member is mirrors.VariableMirror || member.isGetter) {
+ // TODO(jacobr): actually check method types against the function type
+ // returned by the getter or field.
+ return true;
+ }
+ var parameters = member.parameters;
+ var positionalArguments = invocation.positionalArguments;
+ // Too many arguments
+ if (parameters.length < positionalArguments.length) return false;
+ // Too few required arguments.
+ if (parameters.length > positionalArguments.length &&
+ !parameters[positionalArguments.length].isOptional) return false;
+ for (var i = 0; i < positionalArguments.length; i++) {
+ if (parameters[i].isNamed) {
+ // Not enough positional arguments.
+ return false;
+ }
+ if (!_checkType(
+ invocation.positionalArguments[i], parameters[i].type)) return false;
+ }
+ if (invocation.namedArguments.isNotEmpty) {
+ var startNamed;
+ for (startNamed = parameters.length - 1; startNamed >= 0; startNamed--) {
+ if (!parameters[startNamed].isNamed) break;
+ }
+ startNamed++;
+
+ // TODO(jacobr): we are unneccessarily using an O(n^2) algorithm here.
+ // If we have JS APIs with a lange number of named parameters we should
+ // optimize this. Either use a HashSet or invert this, walking over
+ // parameters, querying invocation, and making sure we match
+ //invocation.namedArguments.size keys.
+ for (var name in invocation.namedArguments.keys) {
+ bool match = false;
+ for (var j = startNamed; j < parameters.length; j++) {
+ var p = parameters[j];
+ if (p.simpleName == name) {
+ if (!_checkType(invocation.namedArguments[name],
+ parameters[j].type)) return false;
+ match = true;
+ break;
+ }
+ }
+ if (match == false) return false;
+ }
+ }
+ return true;
+ }
+
+ bool checkInvocation(Invocation invocation) {
+ for (var member in _members) {
+ if (_checkDeclaration(invocation, member)) return true;
+ }
+ return false;
+ }
+
+ void add(mirrors.DeclarationMirror mirror) {
+ _members.add(mirror);
+ }
+
+ final List<mirrors.DeclarationMirror> _members;
+}
+
+/**
+ * Temporary method that we hope to remove at some point. This method should
+ * generally only be called by machine generated code.
+ */
+void registerJsInterfaces(List<Type> classes) {
+ if (_finalized == true) {
+ throw 'JSInterop class registration already finalized';
+ }
+ for (Type type in classes) {
+ if (!_jsInterfaceTypes.add(type)) continue; // Already registered.
+ mirrors.ClassMirror typeMirror = mirrors.reflectType(type);
+ typeMirror.declarations.forEach((symbol, declaration) {
+ if (declaration is mirrors.MethodMirror ||
+ declaration is mirrors.VariableMirror && !declaration.isStatic) {
+ bool treatAsGetter = false;
+ bool treatAsSetter = false;
+ if (declaration is mirrors.VariableMirror) {
+ treatAsGetter = true;
+ if (!declaration.isConst && !declaration.isFinal) {
+ treatAsSetter = true;
+ }
+ } else {
+ if (declaration.isGetter) {
+ treatAsGetter = true;
+ } else if (declaration.isSetter) {
+ treatAsSetter = true;
+ } else if (!declaration.isConstructor) {
+ _allowedMethods
+ .putIfAbsent(symbol, () => new _DeclarationSet())
+ .add(declaration);
+ }
+ }
+ if (treatAsGetter) {
+ _allowedGetters
+ .putIfAbsent(symbol, () => new _DeclarationSet())
+ .add(declaration);
+ _allowedMethods
+ .putIfAbsent(symbol, () => new _DeclarationSet())
+ .add(declaration);
+ }
+ if (treatAsSetter) {
+ _allowedSetters
+ .putIfAbsent(symbol, () => new _DeclarationSet())
+ .add(declaration);
+ }
+ }
+ });
+ }
+}
+
+_finalizeJsInterfaces() native "Js_finalizeJsInterfaces";
+
+/**
+ * Generates a part file defining source code for JsObjectImpl and related
+ * classes. This calass is needed so that type checks for all registered JavaScript
+ * interop classes pass.
+ */
+String _generateJsObjectImplPart() {
+ Iterable<Type> types = jsInterfaceTypes;
+ var libraryPrefixes = new Map<mirrors.LibraryMirror, String>();
+ var prefixNames = new Set<String>();
+ var sb = new StringBuffer();
+
+ var implements = <String>[];
+ for (var type in types) {
+ mirrors.ClassMirror typeMirror = mirrors.reflectType(type);
+ mirrors.LibraryMirror libraryMirror = typeMirror.owner;
+ var prefixName;
+ if (libraryPrefixes.containsKey(libraryMirror)) {
+ prefixName = libraryPrefixes[libraryMirror];
+ } else {
+ var basePrefixName =
+ mirrors.MirrorSystem.getName(libraryMirror.simpleName);
+ basePrefixName = basePrefixName.replaceAll('.', '_');
+ if (basePrefixName.isEmpty) basePrefixName = "lib";
+ prefixName = basePrefixName;
+ var i = 1;
+ while (prefixNames.contains(prefixName)) {
+ prefixName = '$basePrefixName$i';
+ i++;
+ }
+ prefixNames.add(prefixName);
+ libraryPrefixes[libraryMirror] = prefixName;
+ }
+ implements.add(
+ '${prefixName}.${mirrors.MirrorSystem.getName(typeMirror.simpleName)}');
+ }
+ libraryPrefixes.forEach((libraryMirror, prefix) {
+ sb.writeln('import "${libraryMirror.uri}" as $prefix;');
+ });
+ var implementsClause =
+ implements.isEmpty ? "" : "implements ${implements.join(', ')}";
+ // TODO(jacobr): only certain classes need to be implemented by
+ // Function and Array.
+ sb.write('''
+class JsObjectImpl extends JsObject $implementsClause {
+ JsObjectImpl.internal() : super.internal();
+}
+
+class JsFunctionImpl extends JsFunction $implementsClause {
+ JsFunctionImpl.internal() : super.internal();
+}
+
+class JsArrayImpl<E> extends JsArray<E> $implementsClause {
+ JsArrayImpl.internal() : super.internal();
+}
+''');
+ return sb.toString();
+}
+
+// Start of block of helper methods facilitating emulating JavaScript Array
+// methods on Dart List objects passed to JavaScript via JS interop.
+// TODO(jacobr): match JS more closely.
+String _toStringJs(obj) => '$obj';
+
+// TODO(jacobr): this might not exactly match JS semantics but should be
+// adequate for now.
+int _toIntJs(obj) {
+ if (obj is int) return obj;
+ if (obj is num) return obj.toInt();
+ return num.parse('$obj'.trim(), (_) => 0).toInt();
+}
+
+// TODO(jacobr): this might not exactly match JS semantics but should be
+// adequate for now.
+num _toNumJs(obj) {
+ return obj is num ? obj : num.parse('$obj'.trim(), (_) => 0);
+}
+
+/// Match the behavior of setting List length in JavaScript with the exception
+/// that Dart does not distinguish undefined and null.
+_setListLength(List list, rawlen) {
+ num len = _toNumJs(rawlen);
+ if (len is! int || len < 0) {
+ throw new RangeError("Invalid array length");
+ }
+ if (len > list.length) {
+ _arrayExtend(list, len);
+ } else if (len < list.length) {
+ list.removeRange(len, list.length);
+ }
+ return rawlen;
+}
+
+// TODO(jacobr): should we really bother with this method instead of just
+// shallow copying to a JS array and calling the JavaScript join method?
+String _arrayJoin(List list, sep) {
+ if (sep == null) {
+ sep = ",";
+ }
+ return list.map((e) => e == null ? "" : e.toString()).join(sep.toString());
+}
+
+// TODO(jacobr): should we really bother with this method instead of just
+// shallow copying to a JS array and using the toString method?
+String _arrayToString(List list) => _arrayJoin(list, ",");
+
+int _arrayPush(List list, e) {
+ list.add(e);
+ return list.length;
+}
+
+_arrayPop(List list) {
+ if (list.length > 0) return list.removeLast();
+}
+
+// TODO(jacobr): would it be better to just copy input to a JS List
+// and call Array.concat?
+List _arrayConcat(List input, List args) {
+ var ret = new List.from(input);
+ for (var e in args) {
+ // TODO(jacobr): technically in ES6 we should use
+ // Symbol.isConcatSpreadable to determine whether call addAll. Once v8
+ // supports it, we can make all Dart classes implementing Iterable
+ // specify isConcatSpreadable and tweak this behavior to allow Iterable.
+ if (e is List) {
+ ret.addAll(e);
+ } else {
+ ret.add(e);
+ }
+ }
+ return ret;
+}
+
+List _arraySplice(List input, List args) {
+ int start = 0;
+ if (args.length > 0) {
+ var rawStart = _toIntJs(args[0]);
+ if (rawStart < 0) {
+ start = math.max(0, input.length - rawStart);
+ } else {
+ start = math.min(input.length, rawStart);
+ }
+ }
+ var end = start;
+ if (args.length > 1) {
+ var rawDeleteCount = _toIntJs(args[1]);
+ if (rawDeleteCount < 0) rawDeleteCount = 0;
+ end = math.min(input.length, start + rawDeleteCount);
+ }
+ var replacement = [];
+ var removedElements = input.getRange(start, end).toList();
+ if (args.length > 2) {
+ replacement = args.getRange(2, args.length);
+ }
+ input.replaceRange(start, end, replacement);
+ return removedElements;
+}
+
+List _arrayReverse(List l) {
+ for (var i = 0, j = l.length - 1; i < j; i++, j--) {
+ var tmp = l[i];
+ l[i] = l[j];
+ l[j] = tmp;
+ }
+ return l;
+}
+
+_arrayShift(List l) {
+ if (l.isEmpty) return null; // Technically we should return undefined.
+ return l.removeAt(0);
+}
+
+int _arrayUnshift(List l, List args) {
+ l.insertAll(0, args);
+ return l.length;
+}
+
+_arrayExtend(List l, int newLength) {
+ for (var i = l.length; i < newLength; i++) {
+ // TODO(jacobr): we'd really like to add undefined to better match
+ // JavaScript semantics.
+ l.add(null);
+ }
+}
+
+List _arraySort(List l, rawCompare) {
+ // TODO(jacobr): alternately we could just copy the Array to JavaScript,
+ // invoke the JS sort method and then copy the result back to Dart.
+ Comparator compare;
+ if (rawCompare == null) {
+ compare = (a, b) => _toStringJs(a).compareTo(_toStringJs(b));
+ } else if (rawCompare is JsFunction) {
+ compare = (a, b) => rawCompare.apply([a, b]);
+ } else {
+ compare = rawCompare;
+ }
+ l.sort(compare);
+ return l;
+}
+// End of block of helper methods to emulate JavaScript Array methods on Dart List.
+
+/**
+ * Can be called to provide a predictable point where no more JS interfaces can
+ * be added. Creating an instance of JsObject will also automatically trigger
+ * all JsObjects to be finalized.
+ */
+void finalizeJsInterfaces() {
+ if (_finalized == true) {
+ throw 'JSInterop class registration already finalized';
+ }
+ _finalizeJsInterfaces();
+}
JsObject _cachedContext;
JsObject get _context native "Js_context_Callback";
+bool get _finalized native "Js_interfacesFinalized_Callback";
+
JsObject get context {
if (_cachedContext == null) {
_cachedContext = _context;
@@ -114,9 +489,24 @@ class JsObject extends NativeFieldWrapperClass2 {
* Constructs a new JavaScript object from [constructor] and returns a proxy
* to it.
*/
- factory JsObject(JsFunction constructor, [List arguments]) => _create(constructor, arguments);
-
- static JsObject _create(JsFunction constructor, arguments) native "JsObject_constructorCallback";
+ factory JsObject(JsFunction constructor, [List arguments]) =>
+ _create(constructor, arguments);
+
+ static JsObject _create(
+ JsFunction constructor, arguments) native "JsObject_constructorCallback";
+
+ _buildArgs(Invocation invocation) {
+ if (invocation.namedArguments.isEmpty) {
+ return invocation.positionalArguments;
+ } else {
+ var varArgs = new Map<String, Object>();
+ invocation.namedArguments.forEach((symbol, val) {
+ varArgs[mirrors.MirrorSystem.getName(symbol)] = val;
+ });
+ return invocation.positionalArguments.toList()
+ ..add(new JsObject.jsify(varArgs));
+ }
+ }
/**
* Constructs a [JsObject] that proxies a native Dart object; _for expert use
@@ -131,8 +521,7 @@ class JsObject extends NativeFieldWrapperClass2 {
*/
factory JsObject.fromBrowserObject(object) {
if (object is num || object is String || object is bool || object == null) {
- throw new ArgumentError(
- "object cannot be a num, string, bool, or null");
+ throw new ArgumentError("object cannot be a num, string, bool, or null");
}
return _fromBrowserObject(object);
}
@@ -155,7 +544,8 @@ class JsObject extends NativeFieldWrapperClass2 {
static JsObject _jsify(object) native "JsObject_jsify";
- static JsObject _fromBrowserObject(object) native "JsObject_fromBrowserObject";
+ static JsObject _fromBrowserObject(
+ object) native "JsObject_fromBrowserObject";
/**
* Returns the value associated with [property] from the proxied JavaScript
@@ -163,7 +553,7 @@ class JsObject extends NativeFieldWrapperClass2 {
*
* The type of [property] must be either [String] or [num].
*/
- operator[](property) native "JsObject_[]";
+ operator [](property) native "JsObject_[]";
/**
* Sets the value associated with [property] on the proxied JavaScript
@@ -171,13 +561,14 @@ class JsObject extends NativeFieldWrapperClass2 {
*
* The type of [property] must be either [String] or [num].
*/
- operator[]=(property, value) native "JsObject_[]=";
+ operator []=(property, value) native "JsObject_[]=";
int get hashCode native "JsObject_hashCode";
- operator==(other) => other is JsObject && _identityEquality(this, other);
+ operator ==(other) => other is JsObject && _identityEquality(this, other);
- static bool _identityEquality(JsObject a, JsObject b) native "JsObject_identityEquality";
+ static bool _identityEquality(
+ JsObject a, JsObject b) native "JsObject_identityEquality";
/**
* Returns `true` if the JavaScript object contains the specified property
@@ -207,7 +598,7 @@ class JsObject extends NativeFieldWrapperClass2 {
String toString() {
try {
return _toString();
- } catch(e) {
+ } catch (e) {
return super.toString();
}
}
@@ -223,7 +614,7 @@ class JsObject extends NativeFieldWrapperClass2 {
callMethod(String method, [List args]) {
try {
return _callMethod(method, args);
- } catch(e) {
+ } catch (e) {
if (hasProperty(method)) {
rethrow;
} else {
@@ -232,13 +623,63 @@ class JsObject extends NativeFieldWrapperClass2 {
}
}
+ noSuchMethod(Invocation invocation) {
+ throwError() {
+ throw new NoSuchMethodError(this, invocation.memberName,
+ invocation.positionalArguments, invocation.namedArguments);
+ }
+
+ String name = mirrors.MirrorSystem.getName(invocation.memberName);
+ if (invocation.isGetter) {
+ if (CHECK_JS_INVOCATIONS) {
+ var matches = _allowedGetters[invocation.memberName];
+ if (matches == null &&
+ !_allowedMethods.containsKey(invocation.memberName)) {
+ throwError();
+ }
+ var ret = this[name];
+ if (matches != null && matches._checkReturnType(ret)) return ret;
+ if (ret is Function ||
+ (ret is JsFunction /* shouldn't be needed in the future*/) &&
+ _allowedMethods.containsKey(
+ invocation.memberName)) return ret; // Warning: we have not bound "this"... we could type check on the Function but that is of little value in Dart.
+ throwError();
+ } else {
+ // TODO(jacobr): should we throw if the JavaScript object doesn't have the property?
+ return this[name];
+ }
+ } else if (invocation.isSetter) {
+ if (CHECK_JS_INVOCATIONS) {
+ var matches = _allowedSetters[invocation.memberName];
+ if (matches == null ||
+ !matches.checkInvocation(invocation)) throwError();
+ }
+ assert(name.endsWith("="));
+ name = name.substring(0, name.length - 1);
+ return this[name] = invocation.positionalArguments.first;
+ } else {
+ // TODO(jacobr): also allow calling getters that look like functions.
+ var matches;
+ if (CHECK_JS_INVOCATIONS) {
+ matches = _allowedMethods[invocation.memberName];
+ if (matches == null ||
+ !matches.checkInvocation(invocation)) throwError();
+ }
+ var ret = this.callMethod(name, _buildArgs(invocation));
+ if (CHECK_JS_INVOCATIONS) {
+ if (!matches._checkReturnType(ret)) throwError();
+ }
+ return ret;
+ }
+ }
+
_callMethod(String name, List args) native "JsObject_callMethod";
}
/**
* Proxies a JavaScript Function object.
*/
-class JsFunction extends JsObject {
+class JsFunction extends JsObject implements Function {
JsFunction.internal() : super.internal();
/**
@@ -253,13 +694,21 @@ class JsFunction extends JsObject {
*/
dynamic apply(List args, {thisArg}) native "JsFunction_apply";
+ noSuchMethod(Invocation invocation) {
+ if (invocation.isMethod && invocation.memberName == #call) {
+ return apply(_buildArgs(invocation));
+ }
+ return super.noSuchMethod(invocation);
+ }
+
/**
* Internal only version of apply which uses debugger proxies of Dart objects
* rather than opaque handles. This method is private because it cannot be
* efficiently implemented in Dart2Js so should only be used by internal
* tools.
*/
- _applyDebuggerOnly(List args, {thisArg}) native "JsFunction_applyDebuggerOnly";
+ _applyDebuggerOnly(List args,
+ {thisArg}) native "JsFunction_applyDebuggerOnly";
static JsFunction _withThis(Function f) native "JsFunction_withThis";
}
@@ -268,14 +717,17 @@ class JsFunction extends JsObject {
* A [List] proxying a JavaScript Array.
*/
class JsArray<E> extends JsObject with ListMixin<E> {
+ JsArray.internal() : super.internal();
factory JsArray() => _newJsArray();
static JsArray _newJsArray() native "JsArray_newJsArray";
- factory JsArray.from(Iterable<E> other) => _newJsArrayFromSafeList(new List.from(other));
+ factory JsArray.from(Iterable<E> other) =>
+ _newJsArrayFromSafeList(new List.from(other));
- static JsArray _newJsArrayFromSafeList(List list) native "JsArray_newJsArrayFromSafeList";
+ static JsArray _newJsArrayFromSafeList(
+ List list) native "JsArray_newJsArrayFromSafeList";
_checkIndex(int index, {bool insert: false}) {
int length = insert ? this.length + 1 : this.length;
@@ -304,7 +756,7 @@ class JsArray<E> extends JsObject with ListMixin<E> {
}
void operator []=(index, E value) {
- if(index is int) {
+ if (index is int) {
_checkIndex(index);
}
super[index] = value;
@@ -312,7 +764,9 @@ class JsArray<E> extends JsObject with ListMixin<E> {
int get length native "JsArray_length";
- void set length(int length) { super['length'] = length; }
+ void set length(int length) {
+ super['length'] = length;
+ }
// Methods overriden for better performance
@@ -326,7 +780,7 @@ class JsArray<E> extends JsObject with ListMixin<E> {
}
void insert(int index, E element) {
- _checkIndex(index, insert:true);
+ _checkIndex(index, insert: true);
callMethod('splice', [index, 0, element]);
}
@@ -365,7 +819,7 @@ class JsArray<E> extends JsObject with ListMixin<E> {
*/
const _UNDEFINED = const Object();
-// FIXME(jacobr): this method is a hack to work around the lack of proper dart
+// TODO(jacobr): this method is a hack to work around the lack of proper dart
// support for varargs methods.
List _stripUndefinedArgs(List args) =>
args.takeWhile((i) => i != _UNDEFINED).toList();
@@ -375,8 +829,7 @@ List _stripUndefinedArgs(List args) =>
* than 11) of arguments without violating Dart type checks.
*/
Function _wrapAsDebuggerVarArgsFunction(JsFunction jsFunction) =>
- ([a1=_UNDEFINED, a2=_UNDEFINED, a3=_UNDEFINED, a4=_UNDEFINED,
- a5=_UNDEFINED, a6=_UNDEFINED, a7=_UNDEFINED, a8=_UNDEFINED,
- a9=_UNDEFINED, a10=_UNDEFINED]) =>
- jsFunction._applyDebuggerOnly(_stripUndefinedArgs(
- [a1,a2,a3,a4,a5,a6,a7,a8,a9,a10]));
+ ([a1 = _UNDEFINED, a2 = _UNDEFINED, a3 = _UNDEFINED, a4 = _UNDEFINED,
+ a5 = _UNDEFINED, a6 = _UNDEFINED, a7 = _UNDEFINED, a8 = _UNDEFINED,
+ a9 = _UNDEFINED, a10 = _UNDEFINED]) => jsFunction._applyDebuggerOnly(
+ _stripUndefinedArgs([a1, a2, a3, a4, a5, a6, a7, a8, a9, a10]));
« no previous file with comments | « sdk/lib/html/dartium/html_dartium.dart ('k') | tests/html/html.status » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698