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..9a6f54d211aa70233cf9fa42b3e913d2e14550ce 100644 |
--- a/sdk/lib/js/dartium/js_dartium.dart |
+++ b/sdk/lib/js/dartium/js_dartium.dart |
@@ -89,6 +89,322 @@ library dart.js; |
import 'dart:collection' show ListMixin; |
import 'dart:nativewrappers'; |
+import 'dart:math' as math; |
+import 'dart:mirrors' as mirrors; |
+ |
+// TODO(jacobr): if we care about unchecked mode in Dartium we need to set this |
+// to false in unchecked 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 Set<Type> _jsInterfaceTypes = new Set<Type>(); |
+Iterable<Type> get jsInterfaceTypes => _jsInterfaceTypes.toList(); |
+ |
+bool _finalized = false; |
+ |
+class _DeclarationSet { |
+ _DeclarationSet() : _methods = <mirrors.MethodMirror>[]; |
+ |
+ bool checkType(obj, type) { |
+ if (obj == null) return true; |
+ return mirrors.reflectType(obj.runtimeType).isSubtypeOf(type); |
+ } |
+ |
+ bool checkReturnType(value) { |
+ if (value == null) return true; |
+ var valueMirror = mirrors.reflectType(value.runtimeType); |
+ for (var method in _methods) { |
+ if (method.isGetter) { |
+ // FIXME: actually check return types for getters that return function |
+ // types. |
+ return true; |
+ } else { |
+ if (valueMirror.isSubtypeOf(method.returnType)) return true; |
+ } |
+ } |
+ return false; |
+ } |
+ |
+ bool _checkDeclaration(Invocation invocation, mirrors.MethodMirror method) { |
+ if (method.isGetter) { |
+ // FIXME: actually check method types against the function type returned |
+ // by the getter. |
+ return true; |
+ } |
+ var parameters = method.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; |
+ var endPositional = math.min(parameters.length, positionalArguments.length); |
+ for (var i = 0; i < endPositional; 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 startPositional; |
+ for (startPositional = parameters.length -1 ; startPositional >= 0; startPositional--) { |
+ if (!parameters[startPositional].isNamed) break; |
+ } |
+ startPositional++; |
+ |
+ for (var name in invocation.namedArguments.keys) { |
+ bool match = false; |
+ for (var j = startPositional; 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 method in _methods) { |
+ if (_checkDeclaration(invocation, method)) return true; |
+ } |
+ return false; |
+ } |
+ |
+ void add(mirrors.MethodMirror mirror) { |
+ _methods.add(mirror); |
+ } |
+ |
+ final List<mirrors.MethodMirror> _methods; |
+} |
+ |
+/** |
+ * Temporary method that we hope to remove at some point. Should only be called by codegen. |
+ */ |
+void registerJsInterfaces(List<Type> classes) { |
+ if (_finalized == true) { |
+ throw 'JSInterop class registration already finalized'; |
+ } |
+ Map<Symbol, List<mirrors.DeclarationMirror>> allMembers; |
+ 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.isStatic) { |
+ if (declaration.isGetter) { |
+ _allowedGetters.putIfAbsent(symbol, () => new _DeclarationSet()).add(declaration); |
+ _allowedMethods.putIfAbsent(symbol, () => new _DeclarationSet()).add(declaration); |
+ } else if (declaration.isSetter) { |
+ _allowedSetters.putIfAbsent(symbol, () => new _DeclarationSet()).add(declaration); |
+ } else if (!declaration.isConstructor) { |
+ _allowedMethods.putIfAbsent(symbol, () => new _DeclarationSet()).add(declaration); |
+ } |
+ } |
+ }); |
+ } |
+} |
+ |
+_finalizeJsInterfaces() native "Js_finalizeJsInterfaces"; |
+ |
+/** |
+ * Generates a part file defining source code for JsObjectImpl and related |
+ * classes. This 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 to emulate JavaScript Array methods on Dart List. |
+// TODO(jacobr): match JS more closely. |
+String _toStringJs(obj) => '$obj'; |
+ |
+// TODO(jacobr): this might not exactly match JS semantics. |
+int _toIntJs(obj) { |
+ if (obj is int) return obj; |
+ if (obj is num) return obj.toInt(); |
+ return num.parse('$obj'.trim(), (_) => 0).toInt(); |
+} |
+ |
+// Helper to match the behavior of setting List length in JavaScript. |
+int _setListLength(List list, int len) { |
+ for (var i = list.length; i < len; i++) { |
+ list.add(null); |
+ } |
+ return len; |
+} |
+ |
+// TODO(jacobr): should we really bother with this method instead of just |
+// shallow copying to a JS array and using the 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) => 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): should we use Iterable ? |
+ if (e is List) { // FIXME use Symbol.isConcatSpreadable |
+ 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; // Really want to return JS undefined. |
+ return l.removeAt(0); |
+} |
+ |
+int _arrayUnshift(List l, List args) { |
+ l.insertAll(0, args); |
+ return l.length; |
+} |
+ |
+_arrayExtend(List l, int newLength) { |
+ for (int 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. |
+ |
+/** |
+ * Must be called before JS Interfaces can be used safely used and cross cast. |
+ */ |
+void finalizeJsInterfaces() { |
+ if (_finalized == true) { |
+ throw 'JSInterop class registration already finalized'; |
+ } |
+ _finalized = true; |
+ _finalizeJsInterfaces(); |
+} |
JsObject _cachedContext; |
@@ -118,6 +434,18 @@ class JsObject extends NativeFieldWrapperClass2 { |
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 |
* only_. |
@@ -232,13 +560,55 @@ 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.checkInvocation(invocation)) throwError(); |
+ } |
+ 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,6 +623,13 @@ 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 |
@@ -268,6 +645,7 @@ 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(); |