OLD | NEW |
| (Empty) |
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 /** | |
6 * Support for interoperating with JavaScript. | |
7 * | |
8 * This library provides access to JavaScript objects from Dart, allowing | |
9 * Dart code to get and set properties, and call methods of JavaScript objects | |
10 * and invoke JavaScript functions. The library takes care of converting | |
11 * between Dart and JavaScript objects where possible, or providing proxies if | |
12 * conversion isn't possible. | |
13 * | |
14 * This library does not yet make Dart objects usable from JavaScript, their | |
15 * methods and proeprties are not accessible, though it does allow Dart | |
16 * functions to be passed into and called from JavaScript. | |
17 * | |
18 * [JsObject] is the core type and represents a proxy of a JavaScript object. | |
19 * JsObject gives access to the underlying JavaScript objects properties and | |
20 * methods. `JsObject`s can be acquired by calls to JavaScript, or they can be | |
21 * created from proxies to JavaScript constructors. | |
22 * | |
23 * The top-level getter [context] provides a [JsObject] that represents the | |
24 * global object in JavaScript, usually `window`. | |
25 * | |
26 * The following example shows an alert dialog via a JavaScript call to the | |
27 * global function `alert()`: | |
28 * | |
29 * import 'dart:js'; | |
30 * | |
31 * main() => context.callMethod('alert', ['Hello from Dart!']); | |
32 * | |
33 * This example shows how to create a [JsObject] from a JavaScript constructor | |
34 * and access its properties: | |
35 * | |
36 * import 'dart:js'; | |
37 * | |
38 * main() { | |
39 * var object = new JsObject(context['Object']); | |
40 * object['greeting'] = 'Hello'; | |
41 * object['greet'] = (name) => "${object['greeting']} $name"; | |
42 * var message = object.callMethod('greet', ['JavaScript']); | |
43 * context['console'].callMethod('log', [message]); | |
44 * } | |
45 * | |
46 * ## Proxying and automatic conversion | |
47 * | |
48 * When setting properties on a JsObject or passing arguments to a Javascript | |
49 * method or function, Dart objects are automatically converted or proxied to | |
50 * JavaScript objects. When accessing JavaScript properties, or when a Dart | |
51 * closure is invoked from JavaScript, the JavaScript objects are also | |
52 * converted to Dart. | |
53 * | |
54 * Functions and closures are proxied in such a way that they are callable. A | |
55 * Dart closure assigned to a JavaScript property is proxied by a function in | |
56 * JavaScript. A JavaScript function accessed from Dart is proxied by a | |
57 * [JsFunction], which has a [apply] method to invoke it. | |
58 * | |
59 * The following types are transferred directly and not proxied: | |
60 * | |
61 * * "Basic" types: `null`, `bool`, `num`, `String`, `DateTime` | |
62 * * `Blob` | |
63 * * `Event` | |
64 * * `HtmlCollection` | |
65 * * `ImageData` | |
66 * * `KeyRange` | |
67 * * `Node` | |
68 * * `NodeList` | |
69 * * `TypedData`, including its subclasses like `Int32List`, but _not_ | |
70 * `ByteBuffer` | |
71 * * `Window` | |
72 * | |
73 * ## Converting collections with JsObject.jsify() | |
74 * | |
75 * To create a JavaScript collection from a Dart collection use the | |
76 * [JsObject.jsify] constructor, which converts Dart [Map]s and [Iterable]s | |
77 * into JavaScript Objects and Arrays. | |
78 * | |
79 * The following expression creates a new JavaScript object with the properties | |
80 * `a` and `b` defined: | |
81 * | |
82 * var jsMap = new JsObject.jsify({'a': 1, 'b': 2}); | |
83 * | |
84 * This expression creates a JavaScript array: | |
85 * | |
86 * var jsArray = new JsObject.jsify([1, 2, 3]); | |
87 */ | |
88 library dart.js; | |
89 | |
90 import 'dart:collection' show ListMixin; | |
91 import 'dart:nativewrappers'; | |
92 import 'dart:math' as math; | |
93 import 'dart:mirrors' as mirrors; | |
94 import 'dart:html' as html; | |
95 import 'dart:_blink' as _blink; | |
96 import 'dart:html_common' as html_common; | |
97 import 'dart:indexed_db' as indexed_db; | |
98 import 'dart:typed_data'; | |
99 import 'dart:core'; | |
100 | |
101 import 'cached_patches.dart'; | |
102 | |
103 // Pretend we are always in checked mode as we aren't interested in users | |
104 // running Dartium code outside of checked mode. | |
105 @Deprecated("Internal Use Only") | |
106 final bool CHECK_JS_INVOCATIONS = true; | |
107 | |
108 final String _DART_RESERVED_NAME_PREFIX = r'JS$'; | |
109 // If a private class is defined to use @JS we need to inject a non-private | |
110 // class with a name that will not cause collisions in the library so we can | |
111 // make JSObject implement that interface even though it is in a different | |
112 // library. | |
113 final String escapePrivateClassPrefix = r'$JSImplClass23402893498'; | |
114 | |
115 // Exposed to return ArrayBufferView from a TypedArray passed to readPixels. | |
116 toArrayBufferView(TypedData data) native "Dart_TypedArray_ArrayBufferView"; | |
117 | |
118 String _stripReservedNamePrefix(String name) => | |
119 name.startsWith(_DART_RESERVED_NAME_PREFIX) | |
120 ? name.substring(_DART_RESERVED_NAME_PREFIX.length) | |
121 : name; | |
122 | |
123 _buildArgs(Invocation invocation) { | |
124 if (invocation.namedArguments.isEmpty) { | |
125 return invocation.positionalArguments; | |
126 } else { | |
127 var varArgs = new Map<String, Object>(); | |
128 invocation.namedArguments.forEach((symbol, val) { | |
129 varArgs[mirrors.MirrorSystem.getName(symbol)] = val; | |
130 }); | |
131 return invocation.positionalArguments.toList() | |
132 ..add(JsNative.jsify(varArgs)); | |
133 } | |
134 } | |
135 | |
136 final _allowedMethods = new Map<Symbol, _DeclarationSet>(); | |
137 final _allowedGetters = new Map<Symbol, _DeclarationSet>(); | |
138 final _allowedSetters = new Map<Symbol, _DeclarationSet>(); | |
139 | |
140 final _jsInterfaceTypes = new Set<mirrors.ClassMirror>(); | |
141 @Deprecated("Internal Use Only") | |
142 Iterable<mirrors.ClassMirror> get jsInterfaceTypes => _jsInterfaceTypes; | |
143 | |
144 class _StringLiteralEscape { | |
145 // Character code constants. | |
146 static const int BACKSPACE = 0x08; | |
147 static const int TAB = 0x09; | |
148 static const int NEWLINE = 0x0a; | |
149 static const int CARRIAGE_RETURN = 0x0d; | |
150 static const int FORM_FEED = 0x0c; | |
151 static const int QUOTE = 0x22; | |
152 static const int CHAR_$ = 0x24; | |
153 static const int CHAR_0 = 0x30; | |
154 static const int BACKSLASH = 0x5c; | |
155 static const int CHAR_b = 0x62; | |
156 static const int CHAR_f = 0x66; | |
157 static const int CHAR_n = 0x6e; | |
158 static const int CHAR_r = 0x72; | |
159 static const int CHAR_t = 0x74; | |
160 static const int CHAR_u = 0x75; | |
161 | |
162 final StringSink _sink; | |
163 | |
164 _StringLiteralEscape(this._sink); | |
165 | |
166 void writeString(String string) { | |
167 _sink.write(string); | |
168 } | |
169 | |
170 void writeStringSlice(String string, int start, int end) { | |
171 _sink.write(string.substring(start, end)); | |
172 } | |
173 | |
174 void writeCharCode(int charCode) { | |
175 _sink.writeCharCode(charCode); | |
176 } | |
177 | |
178 /// ('0' + x) or ('a' + x - 10) | |
179 static int hexDigit(int x) => x < 10 ? 48 + x : 87 + x; | |
180 | |
181 /// Write, and suitably escape, a string's content as a JSON string literal. | |
182 void writeStringContent(String s) { | |
183 // Identical to JSON string literal escaping except that we also escape $. | |
184 int offset = 0; | |
185 final int length = s.length; | |
186 for (int i = 0; i < length; i++) { | |
187 int charCode = s.codeUnitAt(i); | |
188 if (charCode > BACKSLASH) continue; | |
189 if (charCode < 32) { | |
190 if (i > offset) writeStringSlice(s, offset, i); | |
191 offset = i + 1; | |
192 writeCharCode(BACKSLASH); | |
193 switch (charCode) { | |
194 case BACKSPACE: | |
195 writeCharCode(CHAR_b); | |
196 break; | |
197 case TAB: | |
198 writeCharCode(CHAR_t); | |
199 break; | |
200 case NEWLINE: | |
201 writeCharCode(CHAR_n); | |
202 break; | |
203 case FORM_FEED: | |
204 writeCharCode(CHAR_f); | |
205 break; | |
206 case CARRIAGE_RETURN: | |
207 writeCharCode(CHAR_r); | |
208 break; | |
209 default: | |
210 writeCharCode(CHAR_u); | |
211 writeCharCode(CHAR_0); | |
212 writeCharCode(CHAR_0); | |
213 writeCharCode(hexDigit((charCode >> 4) & 0xf)); | |
214 writeCharCode(hexDigit(charCode & 0xf)); | |
215 break; | |
216 } | |
217 } else if (charCode == QUOTE || | |
218 charCode == BACKSLASH || | |
219 charCode == CHAR_$) { | |
220 if (i > offset) writeStringSlice(s, offset, i); | |
221 offset = i + 1; | |
222 writeCharCode(BACKSLASH); | |
223 writeCharCode(charCode); | |
224 } | |
225 } | |
226 if (offset == 0) { | |
227 writeString(s); | |
228 } else if (offset < length) { | |
229 writeStringSlice(s, offset, length); | |
230 } | |
231 } | |
232 | |
233 /** | |
234 * Serialize a [num], [String], [bool], [Null], [List] or [Map] value. | |
235 * | |
236 * Returns true if the value is one of these types, and false if not. | |
237 * If a value is both a [List] and a [Map], it's serialized as a [List]. | |
238 */ | |
239 bool writeStringLiteral(String str) { | |
240 writeString('"'); | |
241 writeStringContent(str); | |
242 writeString('"'); | |
243 } | |
244 } | |
245 | |
246 String _escapeString(String str) { | |
247 StringBuffer output = new StringBuffer(); | |
248 new _StringLiteralEscape(output)..writeStringLiteral(str); | |
249 return output.toString(); | |
250 } | |
251 | |
252 /// A collection of methods where all methods have the same name. | |
253 /// This class is intended to optimize whether a specific invocation is | |
254 /// appropriate for at least some of the methods in the collection. | |
255 class _DeclarationSet { | |
256 _DeclarationSet() : _members = <mirrors.DeclarationMirror>[]; | |
257 | |
258 static bool _checkType(obj, mirrors.TypeMirror type) { | |
259 if (obj == null) return true; | |
260 return mirrors.reflectType(obj.runtimeType).isSubtypeOf(type); | |
261 } | |
262 | |
263 /// Returns whether the return [value] has a type is consistent with the | |
264 /// return type from at least one of the members matching the DeclarationSet. | |
265 bool _checkReturnType(value) { | |
266 if (value == null) return true; | |
267 var valueMirror = mirrors.reflectType(value.runtimeType); | |
268 for (var member in _members) { | |
269 if (member is mirrors.VariableMirror || member.isGetter) { | |
270 // TODO(jacobr): actually check return types for getters that return | |
271 // function types. | |
272 return true; | |
273 } else { | |
274 if (valueMirror.isSubtypeOf(member.returnType)) return true; | |
275 } | |
276 } | |
277 return false; | |
278 } | |
279 | |
280 /** | |
281 * Check whether the [invocation] is consistent with the [member] mirror. | |
282 */ | |
283 bool _checkDeclaration( | |
284 Invocation invocation, mirrors.DeclarationMirror member) { | |
285 if (member is mirrors.VariableMirror || (member as dynamic).isGetter) { | |
286 // TODO(jacobr): actually check method types against the function type | |
287 // returned by the getter or field. | |
288 return true; | |
289 } | |
290 var parameters = (member as dynamic).parameters; | |
291 var positionalArguments = invocation.positionalArguments; | |
292 // Too many arguments | |
293 if (parameters.length < positionalArguments.length) return false; | |
294 // Too few required arguments. | |
295 if (parameters.length > positionalArguments.length && | |
296 !parameters[positionalArguments.length].isOptional) return false; | |
297 for (var i = 0; i < positionalArguments.length; i++) { | |
298 if (parameters[i].isNamed) { | |
299 // Not enough positional arguments. | |
300 return false; | |
301 } | |
302 if (!_checkType(invocation.positionalArguments[i], parameters[i].type)) | |
303 return false; | |
304 } | |
305 if (invocation.namedArguments.isNotEmpty) { | |
306 var startNamed; | |
307 for (startNamed = parameters.length - 1; startNamed >= 0; startNamed--) { | |
308 if (!parameters[startNamed].isNamed) break; | |
309 } | |
310 startNamed++; | |
311 | |
312 // TODO(jacobr): we are unnecessarily using an O(n^2) algorithm here. | |
313 // If we have JS APIs with a large number of named parameters we should | |
314 // optimize this. Either use a HashSet or invert this, walking over | |
315 // parameters, querying invocation, and making sure we match | |
316 //invocation.namedArguments.size keys. | |
317 for (var name in invocation.namedArguments.keys) { | |
318 bool match = false; | |
319 for (var j = startNamed; j < parameters.length; j++) { | |
320 var p = parameters[j]; | |
321 if (p.simpleName == name) { | |
322 if (!_checkType( | |
323 invocation.namedArguments[name], parameters[j].type)) | |
324 return false; | |
325 match = true; | |
326 break; | |
327 } | |
328 } | |
329 if (match == false) return false; | |
330 } | |
331 } | |
332 return true; | |
333 } | |
334 | |
335 bool checkInvocation(Invocation invocation) { | |
336 for (var member in _members) { | |
337 if (_checkDeclaration(invocation, member)) return true; | |
338 } | |
339 return false; | |
340 } | |
341 | |
342 void add(mirrors.DeclarationMirror mirror) { | |
343 _members.add(mirror); | |
344 } | |
345 | |
346 final List<mirrors.DeclarationMirror> _members; | |
347 } | |
348 | |
349 /** | |
350 * Temporary method that we hope to remove at some point. This method should | |
351 * generally only be called by machine generated code. | |
352 */ | |
353 @Deprecated("Internal Use Only") | |
354 void registerJsInterfaces([List<Type> classes]) { | |
355 // This method is now obsolete in Dartium. | |
356 } | |
357 | |
358 void _registerJsInterfaces(List<Type> classes) { | |
359 for (Type type in classes) { | |
360 mirrors.ClassMirror typeMirror = mirrors.reflectType(type); | |
361 typeMirror.declarations.forEach((symbol, declaration) { | |
362 if (declaration is mirrors.MethodMirror || | |
363 declaration is mirrors.VariableMirror && !declaration.isStatic) { | |
364 bool treatAsGetter = false; | |
365 bool treatAsSetter = false; | |
366 if (declaration is mirrors.VariableMirror) { | |
367 treatAsGetter = true; | |
368 if (!declaration.isConst && !declaration.isFinal) { | |
369 treatAsSetter = true; | |
370 } | |
371 } else { | |
372 if (declaration.isGetter) { | |
373 treatAsGetter = true; | |
374 } else if (declaration.isSetter) { | |
375 treatAsSetter = true; | |
376 } else if (!declaration.isConstructor) { | |
377 _allowedMethods | |
378 .putIfAbsent(symbol, () => new _DeclarationSet()) | |
379 .add(declaration); | |
380 } | |
381 } | |
382 if (treatAsGetter) { | |
383 _allowedGetters | |
384 .putIfAbsent(symbol, () => new _DeclarationSet()) | |
385 .add(declaration); | |
386 _allowedMethods | |
387 .putIfAbsent(symbol, () => new _DeclarationSet()) | |
388 .add(declaration); | |
389 } | |
390 if (treatAsSetter) { | |
391 _allowedSetters | |
392 .putIfAbsent(symbol, () => new _DeclarationSet()) | |
393 .add(declaration); | |
394 } | |
395 } | |
396 }); | |
397 } | |
398 } | |
399 | |
400 _finalizeJsInterfaces() native "Js_finalizeJsInterfaces"; | |
401 | |
402 String _getJsName(mirrors.DeclarationMirror mirror) { | |
403 if (_atJsType != null) { | |
404 for (var annotation in mirror.metadata) { | |
405 if (annotation.type.reflectedType == _atJsType) { | |
406 try { | |
407 var name = annotation.reflectee.name; | |
408 return name != null ? name : ""; | |
409 } catch (e) {} | |
410 } | |
411 } | |
412 } | |
413 return null; | |
414 } | |
415 | |
416 bool _isAnonymousClass(mirrors.ClassMirror mirror) { | |
417 for (var annotation in mirror.metadata) { | |
418 if (mirrors.MirrorSystem.getName(annotation.type.simpleName) == | |
419 "_Anonymous") { | |
420 mirrors.LibraryMirror library = annotation.type.owner; | |
421 var uri = library.uri; | |
422 // make sure the annotation is from package://js | |
423 if (uri.scheme == 'package' && uri.path == 'js/js.dart') { | |
424 return true; | |
425 } | |
426 } | |
427 } | |
428 return false; | |
429 } | |
430 | |
431 bool _hasJsName(mirrors.DeclarationMirror mirror) { | |
432 if (_atJsType != null) { | |
433 for (var annotation in mirror.metadata) { | |
434 if (annotation.type.reflectedType == _atJsType) { | |
435 return true; | |
436 } | |
437 } | |
438 } | |
439 return false; | |
440 } | |
441 | |
442 var _domNameType; | |
443 | |
444 bool hasDomName(mirrors.DeclarationMirror mirror) { | |
445 var location = mirror.location; | |
446 if (location == null || location.sourceUri.scheme != 'dart') return false; | |
447 for (var annotation in mirror.metadata) { | |
448 if (mirrors.MirrorSystem.getName(annotation.type.simpleName) == "DomName") { | |
449 // We can't make sure the annotation is in dart: as Dartium believes it | |
450 // is file://dart/sdk/lib/html/html_common/metadata.dart | |
451 // instead of a proper dart: location. | |
452 return true; | |
453 } | |
454 } | |
455 return false; | |
456 } | |
457 | |
458 _getJsMemberName(mirrors.DeclarationMirror mirror) { | |
459 var name = _getJsName(mirror); | |
460 return name == null || name.isEmpty | |
461 ? _stripReservedNamePrefix(_getDeclarationName(mirror)) | |
462 : name; | |
463 } | |
464 | |
465 // TODO(jacobr): handle setters correctyl. | |
466 String _getDeclarationName(mirrors.DeclarationMirror declaration) { | |
467 var name = mirrors.MirrorSystem.getName(declaration.simpleName); | |
468 if (declaration is mirrors.MethodMirror && declaration.isSetter) { | |
469 assert(name.endsWith("=")); | |
470 name = name.substring(0, name.length - 1); | |
471 } | |
472 return name; | |
473 } | |
474 | |
475 final _JS_LIBRARY_PREFIX = "js_library"; | |
476 final _UNDEFINED_VAR = "_UNDEFINED_JS_CONST"; | |
477 | |
478 String _accessJsPath(String path) => _accessJsPathHelper(path.split(".")); | |
479 | |
480 String _accessJsPathHelper(Iterable<String> parts) { | |
481 var sb = new StringBuffer(); | |
482 sb | |
483 ..write('${_JS_LIBRARY_PREFIX}.JsNative.getProperty(' * parts.length) | |
484 ..write("${_JS_LIBRARY_PREFIX}.context"); | |
485 for (var p in parts) { | |
486 sb.write(", ${_escapeString(p)})"); | |
487 } | |
488 return sb.toString(); | |
489 } | |
490 | |
491 // TODO(jacobr): remove these helpers and add JsNative.setPropertyDotted, | |
492 // getPropertyDotted, and callMethodDotted helpers that would be simpler | |
493 // and more efficient. | |
494 String _accessJsPathSetter(String path) { | |
495 var parts = path.split("."); | |
496 return "${_JS_LIBRARY_PREFIX}.JsNative.setProperty(${_accessJsPathHelper(parts
.getRange(0, parts.length - 1)) | |
497 }, ${_escapeString(parts.last)}, v)"; | |
498 } | |
499 | |
500 String _accessJsPathCallMethodHelper(String path) { | |
501 var parts = path.split("."); | |
502 return "${_JS_LIBRARY_PREFIX}.JsNative.callMethod(${_accessJsPathHelper(parts.
getRange(0, parts.length - 1)) | |
503 }, ${_escapeString(parts.last)},"; | |
504 } | |
505 | |
506 @Deprecated("Internal Use Only") | |
507 void addMemberHelper( | |
508 mirrors.MethodMirror declaration, String path, StringBuffer sb, | |
509 {bool isStatic: false, String memberName}) { | |
510 if (!declaration.isConstructor) { | |
511 var jsName = _getJsMemberName(declaration); | |
512 path = (path != null && path.isNotEmpty) ? "${path}.${jsName}" : jsName; | |
513 } | |
514 var name = memberName != null ? memberName : _getDeclarationName(declaration); | |
515 if (declaration.isConstructor) { | |
516 sb.write("factory"); | |
517 } else if (isStatic) { | |
518 sb.write("static"); | |
519 } else { | |
520 sb.write("@patch"); | |
521 } | |
522 sb.write(" "); | |
523 if (declaration.isGetter) { | |
524 sb.write("get $name => ${_accessJsPath(path)};"); | |
525 } else if (declaration.isSetter) { | |
526 sb.write("set $name(v) {\n" | |
527 " ${_JS_LIBRARY_PREFIX}.safeForTypedInterop(v);\n" | |
528 " return ${_accessJsPathSetter(path)};\n" | |
529 "}\n"); | |
530 } else { | |
531 sb.write("$name("); | |
532 bool hasOptional = false; | |
533 int i = 0; | |
534 var args = <String>[]; | |
535 for (var p in declaration.parameters) { | |
536 assert(!p.isNamed); // TODO(jacobr): throw. | |
537 assert(!p.hasDefaultValue); | |
538 if (i > 0) { | |
539 sb.write(", "); | |
540 } | |
541 if (p.isOptional && !hasOptional) { | |
542 sb.write("["); | |
543 hasOptional = true; | |
544 } | |
545 var arg = "p$i"; | |
546 args.add(arg); | |
547 sb.write(arg); | |
548 if (p.isOptional) { | |
549 sb.write("=${_UNDEFINED_VAR}"); | |
550 } | |
551 i++; | |
552 } | |
553 if (hasOptional) { | |
554 sb.write("]"); | |
555 } | |
556 // TODO(jacobr): | |
557 sb.write(") {\n"); | |
558 for (var arg in args) { | |
559 sb.write(" ${_JS_LIBRARY_PREFIX}.safeForTypedInterop($arg);\n"); | |
560 } | |
561 sb.write(" return "); | |
562 if (declaration.isConstructor) { | |
563 sb.write("${_JS_LIBRARY_PREFIX}.JsNative.callConstructor("); | |
564 sb..write(_accessJsPath(path))..write(","); | |
565 } else { | |
566 sb.write(_accessJsPathCallMethodHelper(path)); | |
567 } | |
568 sb.write("[${args.join(",")}]"); | |
569 | |
570 if (hasOptional) { | |
571 sb.write(".takeWhile((i) => i != ${_UNDEFINED_VAR}).toList()"); | |
572 } | |
573 sb.write(");"); | |
574 sb.write("}\n"); | |
575 } | |
576 sb.write("\n"); | |
577 } | |
578 | |
579 bool _isExternal(mirrors.MethodMirror mirror) { | |
580 // This try-catch block is a workaround for BUG:24834. | |
581 try { | |
582 return mirror.isExternal; | |
583 } catch (e) {} | |
584 return false; | |
585 } | |
586 | |
587 List<String> _generateExternalMethods( | |
588 List<String> libraryPaths, bool useCachedPatches) { | |
589 var staticCodegen = <String>[]; | |
590 | |
591 if (libraryPaths.length == 0) { | |
592 mirrors.currentMirrorSystem().libraries.forEach((uri, library) { | |
593 var library_name = "${uri.scheme}:${uri.path}"; | |
594 if (useCachedPatches && cached_patches.containsKey(library_name)) { | |
595 // Use the pre-generated patch files for DOM dart:nnnn libraries. | |
596 var patch = cached_patches[library_name]; | |
597 staticCodegen.addAll(patch); | |
598 } else if (_hasJsName(library)) { | |
599 // Library marked with @JS | |
600 _generateLibraryCodegen(uri, library, staticCodegen); | |
601 } else if (!useCachedPatches) { | |
602 // Can't use the cached patches file, instead this is a signal to genera
te | |
603 // the patches for this file. | |
604 _generateLibraryCodegen(uri, library, staticCodegen); | |
605 } | |
606 }); // End of library foreach | |
607 } else { | |
608 // Used to generate cached_patches.dart file for all IDL generated dart: | |
609 // files to the WebKit DOM. | |
610 for (var library_name in libraryPaths) { | |
611 var parts = library_name.split(':'); | |
612 var uri = new Uri(scheme: parts[0], path: parts[1]); | |
613 var library = mirrors.currentMirrorSystem().libraries[uri]; | |
614 _generateLibraryCodegen(uri, library, staticCodegen); | |
615 } | |
616 } | |
617 | |
618 return staticCodegen; | |
619 } | |
620 | |
621 _generateLibraryCodegen(uri, library, staticCodegen) { | |
622 // Is it a dart generated library? | |
623 var dartLibrary = uri.scheme == 'dart'; | |
624 | |
625 var sb = new StringBuffer(); | |
626 String jsLibraryName = _getJsName(library); | |
627 | |
628 // Sort by patch file by its declaration name. | |
629 var sortedDeclKeys = library.declarations.keys.toList(); | |
630 sortedDeclKeys.sort((a, b) => mirrors.MirrorSystem | |
631 .getName(a) | |
632 .compareTo(mirrors.MirrorSystem.getName(b))); | |
633 | |
634 sortedDeclKeys.forEach((name) { | |
635 var declaration = library.declarations[name]; | |
636 if (declaration is mirrors.MethodMirror) { | |
637 if ((_hasJsName(declaration) || jsLibraryName != null) && | |
638 _isExternal(declaration)) { | |
639 addMemberHelper(declaration, jsLibraryName, sb); | |
640 } | |
641 } else if (declaration is mirrors.ClassMirror) { | |
642 mirrors.ClassMirror clazz = declaration; | |
643 var isDom = dartLibrary ? hasDomName(clazz) : false; | |
644 var isJsInterop = _hasJsName(clazz); | |
645 if (isDom || isJsInterop) { | |
646 // TODO(jacobr): verify class implements JavaScriptObject. | |
647 var className = mirrors.MirrorSystem.getName(clazz.simpleName); | |
648 bool isPrivateUserDefinedClass = | |
649 className.startsWith('_') && !dartLibrary; | |
650 var classNameImpl = '${className}Impl'; | |
651 var sbPatch = new StringBuffer(); | |
652 if (isJsInterop) { | |
653 String jsClassName = _getJsMemberName(clazz); | |
654 | |
655 jsInterfaceTypes.add(clazz); | |
656 clazz.declarations.forEach((name, declaration) { | |
657 if (declaration is! mirrors.MethodMirror || | |
658 !_isExternal(declaration)) return; | |
659 if (declaration.isFactoryConstructor && _isAnonymousClass(clazz)) { | |
660 sbPatch.write(" factory ${className}("); | |
661 int i = 0; | |
662 var args = <String>[]; | |
663 for (var p in declaration.parameters) { | |
664 args.add(mirrors.MirrorSystem.getName(p.simpleName)); | |
665 i++; | |
666 } | |
667 if (args.isNotEmpty) { | |
668 sbPatch | |
669 ..write('{') | |
670 ..write( | |
671 args.map((name) => '$name:${_UNDEFINED_VAR}').join(", ")) | |
672 ..write('}'); | |
673 } | |
674 sbPatch.write(") {\n" | |
675 " var ret = ${_JS_LIBRARY_PREFIX}.JsNative.newObject();\n")
; | |
676 i = 0; | |
677 for (var p in declaration.parameters) { | |
678 assert(p.isNamed); // TODO(jacobr): throw. | |
679 var name = args[i]; | |
680 var jsName = _stripReservedNamePrefix( | |
681 mirrors.MirrorSystem.getName(p.simpleName)); | |
682 sbPatch.write(" if($name != ${_UNDEFINED_VAR}) {\n" | |
683 " ${_JS_LIBRARY_PREFIX}.safeForTypedInterop($name);\n" | |
684 " ${_JS_LIBRARY_PREFIX}.JsNative.setProperty(ret, ${_es
capeString(jsName)}, $name);\n" | |
685 " }\n"); | |
686 i++; | |
687 } | |
688 | |
689 sbPatch.write(" return ret;" | |
690 "}\n"); | |
691 } else if (declaration.isConstructor || | |
692 declaration.isFactoryConstructor) { | |
693 sbPatch.write(" "); | |
694 addMemberHelper( | |
695 declaration, | |
696 (jsLibraryName != null && jsLibraryName.isNotEmpty) | |
697 ? "${jsLibraryName}.${jsClassName}" | |
698 : jsClassName, | |
699 sbPatch, | |
700 isStatic: true, | |
701 memberName: className); | |
702 } | |
703 }); // End of clazz.declarations.forEach | |
704 | |
705 clazz.staticMembers.forEach((memberName, member) { | |
706 if (_isExternal(member)) { | |
707 sbPatch.write(" "); | |
708 addMemberHelper( | |
709 member, | |
710 (jsLibraryName != null && jsLibraryName.isNotEmpty) | |
711 ? "${jsLibraryName}.${jsClassName}" | |
712 : jsClassName, | |
713 sbPatch, | |
714 isStatic: true); | |
715 } | |
716 }); | |
717 } | |
718 if (isDom) { | |
719 sbPatch.write( | |
720 " static Type get instanceRuntimeType => ${classNameImpl};\n"); | |
721 } | |
722 if (isPrivateUserDefinedClass) { | |
723 sb.write(""" | |
724 class ${escapePrivateClassPrefix}${className} implements $className {} | |
725 """); | |
726 } | |
727 | |
728 if (sbPatch.isNotEmpty) { | |
729 var typeVariablesClause = ''; | |
730 if (!clazz.typeVariables.isEmpty) { | |
731 typeVariablesClause = | |
732 '<${clazz.typeVariables.map((m) => mirrors.MirrorSystem.getName(
m.simpleName)).join(',')}>'; | |
733 } | |
734 sb.write(""" | |
735 @patch class $className$typeVariablesClause { | |
736 $sbPatch | |
737 } | |
738 """); | |
739 if (isDom) { | |
740 sb.write(""" | |
741 class $classNameImpl$typeVariablesClause extends $className implements ${_JS_LIB
RARY_PREFIX}.JSObjectInterfacesDom { | |
742 ${classNameImpl}.internal_() : super.internal_(); | |
743 get runtimeType => $className; | |
744 toString() => super.toString(); | |
745 } | |
746 """); | |
747 } | |
748 } | |
749 } | |
750 } | |
751 }); | |
752 if (sb.isNotEmpty) { | |
753 staticCodegen | |
754 ..add(uri.toString()) | |
755 ..add("${uri}_js_interop_patch.dart") | |
756 ..add(""" | |
757 import 'dart:js' as ${_JS_LIBRARY_PREFIX}; | |
758 | |
759 /** | |
760 * Placeholder object for cases where we need to determine exactly how many | |
761 * args were passed to a function. | |
762 */ | |
763 const ${_UNDEFINED_VAR} = const Object(); | |
764 | |
765 ${sb} | |
766 """); | |
767 } | |
768 } | |
769 | |
770 // Remember the @JS type to compare annotation type. | |
771 var _atJsType = -1; | |
772 | |
773 void setupJsTypeCache() { | |
774 // Cache the @JS Type. | |
775 if (_atJsType == -1) { | |
776 var uri = new Uri(scheme: "package", path: "js/js.dart"); | |
777 var jsLibrary = mirrors.currentMirrorSystem().libraries[uri]; | |
778 if (jsLibrary != null) { | |
779 // @ JS used somewhere. | |
780 var jsDeclaration = jsLibrary.declarations[new Symbol("JS")]; | |
781 _atJsType = jsDeclaration.reflectedType; | |
782 } else { | |
783 // @ JS not used in any library. | |
784 _atJsType = null; | |
785 } | |
786 } | |
787 } | |
788 | |
789 /** | |
790 * Generates part files defining source code for JSObjectImpl, all DOM classes | |
791 * classes. This codegen is needed so that type checks for all registered | |
792 * JavaScript interop classes pass. | |
793 * If genCachedPatches is true then the patch files don't exist this is a specia
l | |
794 * signal to generate and emit the patches to stdout to be captured and put into | |
795 * the file sdk/lib/js/dartium/cached_patches.dart | |
796 */ | |
797 List<String> _generateInteropPatchFiles( | |
798 List<String> libraryPaths, genCachedPatches) { | |
799 // Cache the @JS Type. | |
800 if (_atJsType == -1) setupJsTypeCache(); | |
801 | |
802 var ret = | |
803 _generateExternalMethods(libraryPaths, genCachedPatches ? false : true); | |
804 var libraryPrefixes = new Map<mirrors.LibraryMirror, String>(); | |
805 var prefixNames = new Set<String>(); | |
806 var sb = new StringBuffer(); | |
807 | |
808 var implements = <String>[]; | |
809 var implementsArray = <String>[]; | |
810 var implementsDom = <String>[]; | |
811 var listMirror = mirrors.reflectType(List); | |
812 var functionMirror = mirrors.reflectType(Function); | |
813 var jsObjectMirror = mirrors.reflectType(JSObject); | |
814 | |
815 for (var typeMirror in jsInterfaceTypes) { | |
816 mirrors.LibraryMirror libraryMirror = typeMirror.owner; | |
817 var location = libraryMirror.location; | |
818 var dartLibrary = location != null && location.sourceUri.scheme == 'dart'; | |
819 | |
820 var prefixName; | |
821 if (libraryPrefixes.containsKey(libraryMirror)) { | |
822 prefixName = libraryPrefixes[libraryMirror]; | |
823 } else { | |
824 var basePrefixName = | |
825 mirrors.MirrorSystem.getName(libraryMirror.simpleName); | |
826 basePrefixName = basePrefixName.replaceAll('.', '_'); | |
827 if (basePrefixName.isEmpty) basePrefixName = "lib"; | |
828 prefixName = basePrefixName; | |
829 var i = 1; | |
830 while (prefixNames.contains(prefixName)) { | |
831 prefixName = '$basePrefixName$i'; | |
832 i++; | |
833 } | |
834 prefixNames.add(prefixName); | |
835 libraryPrefixes[libraryMirror] = prefixName; | |
836 } | |
837 var isArray = typeMirror.isSubtypeOf(listMirror); | |
838 var isFunction = typeMirror.isSubtypeOf(functionMirror); | |
839 var isJSObject = typeMirror.isSubtypeOf(jsObjectMirror); | |
840 var className = mirrors.MirrorSystem.getName(typeMirror.simpleName); | |
841 var isPrivateUserDefinedClass = className.startsWith('_') && !dartLibrary; | |
842 if (isPrivateUserDefinedClass) | |
843 className = '${escapePrivateClassPrefix}${className}'; | |
844 var fullName = '${prefixName}.${className}'; | |
845 (isArray ? implementsArray : implements).add(fullName); | |
846 if (!isArray && !isFunction && !isJSObject) { | |
847 // For DOM classes we need to be a bit more conservative at tagging them | |
848 // as implementing JS interop classes risks strange unintended | |
849 // consequences as unrleated code may have instanceof checks. Checking | |
850 // for isJSObject ensures we do not accidentally pull in existing | |
851 // dart:html classes as they all have JSObject as a base class. | |
852 // Note that methods from these classes can still be called on a | |
853 // dart:html instance but checked mode type checks will fail. This is | |
854 // not ideal but is better than causing strange breaks in existing | |
855 // code that uses dart:html. | |
856 // TODO(jacobr): consider throwing compile time errors if @JS classes | |
857 // extend JSObject as that case cannot be safely handled in Dartium. | |
858 implementsDom.add(fullName); | |
859 } | |
860 } | |
861 libraryPrefixes.forEach((libraryMirror, prefix) { | |
862 sb.writeln('import "${libraryMirror.uri}" as $prefix;'); | |
863 }); | |
864 buildImplementsClause(classes) => | |
865 classes.isEmpty ? "" : "implements ${classes.join(', ')}"; | |
866 var implementsClause = buildImplementsClause(implements); | |
867 var implementsClauseDom = buildImplementsClause(implementsDom); | |
868 // TODO(jacobr): only certain classes need to be implemented by | |
869 // JsFunctionImpl. | |
870 var allTypes = []..addAll(implements)..addAll(implementsArray); | |
871 sb.write(''' | |
872 class JSObjectImpl extends JSObject $implementsClause { | |
873 JSObjectImpl.internal() : super.internal(); | |
874 } | |
875 | |
876 class JSFunctionImpl extends JSFunction $implementsClause { | |
877 JSFunctionImpl.internal() : super.internal(); | |
878 } | |
879 | |
880 class JSArrayImpl extends JSArray ${buildImplementsClause(implementsArray)} { | |
881 JSArrayImpl.internal() : super.internal(); | |
882 } | |
883 | |
884 // Interfaces that are safe to slam on all DOM classes. | |
885 // Adding implementsClause would be risky as it could contain Function which | |
886 // is likely to break a lot of instanceof checks. | |
887 abstract class JSObjectInterfacesDom $implementsClauseDom { | |
888 } | |
889 | |
890 @patch class JSObject { | |
891 static Type get instanceRuntimeType => JSObjectImpl; | |
892 } | |
893 | |
894 @patch class JSFunction { | |
895 static Type get instanceRuntimeType => JSFunctionImpl; | |
896 } | |
897 | |
898 @patch class JSArray { | |
899 static Type get instanceRuntimeType => JSArrayImpl; | |
900 } | |
901 | |
902 _registerAllJsInterfaces() { | |
903 _registerJsInterfaces([${allTypes.join(", ")}]); | |
904 } | |
905 | |
906 '''); | |
907 ret..addAll(["dart:js", "JSInteropImpl.dart", sb.toString()]); | |
908 return ret; | |
909 } | |
910 | |
911 // Start of block of helper methods facilitating emulating JavaScript Array | |
912 // methods on Dart List objects passed to JavaScript via JS interop. | |
913 // TODO(jacobr): match JS more closely. | |
914 String _toStringJs(obj) => '$obj'; | |
915 | |
916 // TODO(jacobr): this might not exactly match JS semantics but should be | |
917 // adequate for now. | |
918 int _toIntJs(obj) { | |
919 if (obj is int) return obj; | |
920 if (obj is num) return obj.toInt(); | |
921 return num.parse('$obj'.trim(), (_) => 0).toInt(); | |
922 } | |
923 | |
924 // TODO(jacobr): this might not exactly match JS semantics but should be | |
925 // adequate for now. | |
926 num _toNumJs(obj) { | |
927 return obj is num ? obj : num.parse('$obj'.trim(), (_) => 0); | |
928 } | |
929 | |
930 /// Match the behavior of setting List length in JavaScript with the exception | |
931 /// that Dart does not distinguish undefined and null. | |
932 _setListLength(List list, rawlen) { | |
933 num len = _toNumJs(rawlen); | |
934 if (len is! int || len < 0) { | |
935 throw new RangeError("Invalid array length"); | |
936 } | |
937 if (len > list.length) { | |
938 _arrayExtend(list, len); | |
939 } else if (len < list.length) { | |
940 list.removeRange(len, list.length); | |
941 } | |
942 return rawlen; | |
943 } | |
944 | |
945 // TODO(jacobr): should we really bother with this method instead of just | |
946 // shallow copying to a JS array and calling the JavaScript join method? | |
947 String _arrayJoin(List list, sep) { | |
948 if (sep == null) { | |
949 sep = ","; | |
950 } | |
951 return list.map((e) => e == null ? "" : e.toString()).join(sep.toString()); | |
952 } | |
953 | |
954 // TODO(jacobr): should we really bother with this method instead of just | |
955 // shallow copying to a JS array and using the toString method? | |
956 String _arrayToString(List list) => _arrayJoin(list, ","); | |
957 | |
958 int _arrayPush(List list, List args) { | |
959 for (var e in args) { | |
960 list.add(e); | |
961 } | |
962 return list.length; | |
963 } | |
964 | |
965 _arrayPop(List list) { | |
966 if (list.length > 0) return list.removeLast(); | |
967 } | |
968 | |
969 // TODO(jacobr): would it be better to just copy input to a JS List | |
970 // and call Array.concat? | |
971 List _arrayConcat(List input, List args) { | |
972 var ret = new List.from(input); | |
973 for (var e in args) { | |
974 // TODO(jacobr): technically in ES6 we should use | |
975 // Symbol.isConcatSpreadable to determine whether call addAll. Once v8 | |
976 // supports it, we can make all Dart classes implementing Iterable | |
977 // specify isConcatSpreadable and tweak this behavior to allow Iterable. | |
978 if (e is List) { | |
979 ret.addAll(e); | |
980 } else { | |
981 ret.add(e); | |
982 } | |
983 } | |
984 return ret; | |
985 } | |
986 | |
987 List _arraySplice(List input, List args) { | |
988 int start = 0; | |
989 if (args.length > 0) { | |
990 var rawStart = _toIntJs(args[0]); | |
991 if (rawStart < 0) { | |
992 start = math.max(0, input.length - rawStart); | |
993 } else { | |
994 start = math.min(input.length, rawStart); | |
995 } | |
996 } | |
997 var end = start; | |
998 if (args.length > 1) { | |
999 var rawDeleteCount = _toIntJs(args[1]); | |
1000 if (rawDeleteCount < 0) rawDeleteCount = 0; | |
1001 end = math.min(input.length, start + rawDeleteCount); | |
1002 } | |
1003 var replacement = []; | |
1004 var removedElements = input.getRange(start, end).toList(); | |
1005 if (args.length > 2) { | |
1006 replacement = args.getRange(2, args.length); | |
1007 } | |
1008 input.replaceRange(start, end, replacement); | |
1009 return removedElements; | |
1010 } | |
1011 | |
1012 List _arrayReverse(List l) { | |
1013 for (var i = 0, j = l.length - 1; i < j; i++, j--) { | |
1014 var tmp = l[i]; | |
1015 l[i] = l[j]; | |
1016 l[j] = tmp; | |
1017 } | |
1018 return l; | |
1019 } | |
1020 | |
1021 _arrayShift(List l) { | |
1022 if (l.isEmpty) return null; // Technically we should return undefined. | |
1023 return l.removeAt(0); | |
1024 } | |
1025 | |
1026 int _arrayUnshift(List l, List args) { | |
1027 l.insertAll(0, args); | |
1028 return l.length; | |
1029 } | |
1030 | |
1031 _arrayExtend(List l, int newLength) { | |
1032 for (var i = l.length; i < newLength; i++) { | |
1033 // TODO(jacobr): we'd really like to add undefined to better match | |
1034 // JavaScript semantics. | |
1035 l.add(null); | |
1036 } | |
1037 } | |
1038 | |
1039 List _arraySort(List l, rawCompare) { | |
1040 // TODO(jacobr): alternately we could just copy the Array to JavaScript, | |
1041 // invoke the JS sort method and then copy the result back to Dart. | |
1042 Comparator compare; | |
1043 if (rawCompare == null) { | |
1044 compare = (a, b) => _toStringJs(a).compareTo(_toStringJs(b)); | |
1045 } else if (rawCompare is JsFunction) { | |
1046 compare = (a, b) => rawCompare.apply([a, b]); | |
1047 } else { | |
1048 compare = rawCompare; | |
1049 } | |
1050 l.sort(compare); | |
1051 return l; | |
1052 } | |
1053 // End of block of helper methods to emulate JavaScript Array methods on Dart Li
st. | |
1054 | |
1055 /** | |
1056 * Can be called to provide a predictable point where no more JS interfaces can | |
1057 * be added. Creating an instance of JsObject will also automatically trigger | |
1058 * all JsObjects to be finalized. | |
1059 */ | |
1060 @Deprecated("Internal Use Only") | |
1061 void finalizeJsInterfaces() { | |
1062 if (_finalized == true) { | |
1063 throw 'JSInterop class registration already finalized'; | |
1064 } | |
1065 _finalizeJsInterfaces(); | |
1066 } | |
1067 | |
1068 JsObject _cachedContext; | |
1069 | |
1070 JsObject get _context native "Js_context_Callback"; | |
1071 | |
1072 bool get _finalized native "Js_interfacesFinalized_Callback"; | |
1073 | |
1074 JsObject get context { | |
1075 if (_cachedContext == null) { | |
1076 _cachedContext = _context; | |
1077 } | |
1078 return _cachedContext; | |
1079 } | |
1080 | |
1081 _lookupType(o, bool isCrossFrame, bool isElement) { | |
1082 try { | |
1083 var type = html_common.lookupType(o, isElement); | |
1084 var typeMirror = mirrors.reflectType(type); | |
1085 var legacyInteropConvertToNative = | |
1086 typeMirror.isSubtypeOf(mirrors.reflectType(html.Blob)) || | |
1087 typeMirror.isSubtypeOf(mirrors.reflectType(html.Event)) || | |
1088 typeMirror.isSubtypeOf(mirrors.reflectType(indexed_db.KeyRange)) || | |
1089 typeMirror.isSubtypeOf(mirrors.reflectType(html.ImageData)) || | |
1090 typeMirror.isSubtypeOf(mirrors.reflectType(html.Node)) || | |
1091 // TypedData is removed from this list as it is converted directly | |
1092 // rather than flowing through the interceptor code path. | |
1093 // typeMirror.isSubtypeOf(mirrors.reflectType(typed_data.TypedData)) || | |
1094 typeMirror.isSubtypeOf(mirrors.reflectType(html.Window)); | |
1095 if (isCrossFrame && | |
1096 !typeMirror.isSubtypeOf(mirrors.reflectType(html.Window))) { | |
1097 // TODO(jacobr): evaluate using the true cross frame Window class, etc. | |
1098 // as well as triggering that legacy JS Interop returns raw JsObject | |
1099 // instances. | |
1100 legacyInteropConvertToNative = false; | |
1101 } | |
1102 return [type, legacyInteropConvertToNative]; | |
1103 } catch (e) {} | |
1104 return [JSObject.instanceRuntimeType, false]; | |
1105 } | |
1106 | |
1107 /** | |
1108 * Base class for both the legacy JsObject class and the modern JSObject class. | |
1109 * This allows the JsNative utility class tobehave identically whether it is | |
1110 * called on a JsObject or a JSObject. | |
1111 */ | |
1112 class _JSObjectBase extends NativeFieldWrapperClass2 { | |
1113 String _toString() native "JSObject_toString"; | |
1114 _callMethod(String name, List args) native "JSObject_callMethod"; | |
1115 _operator_getter(String property) native "JSObject_[]"; | |
1116 _operator_setter(String property, value) native "JSObject_[]="; | |
1117 bool _hasProperty(String property) native "JsObject_hasProperty"; | |
1118 bool _instanceof(/*JsFunction|JSFunction*/ type) native "JsObject_instanceof"; | |
1119 | |
1120 int get hashCode native "JSObject_hashCode"; | |
1121 } | |
1122 | |
1123 /** | |
1124 * Proxies a JavaScript object to Dart. | |
1125 * | |
1126 * The properties of the JavaScript object are accessible via the `[]` and | |
1127 * `[]=` operators. Methods are callable via [callMethod]. | |
1128 */ | |
1129 class JsObject extends _JSObjectBase { | |
1130 JsObject.internal(); | |
1131 | |
1132 /** | |
1133 * Constructs a new JavaScript object from [constructor] and returns a proxy | |
1134 * to it. | |
1135 */ | |
1136 factory JsObject(JsFunction constructor, [List arguments]) { | |
1137 try { | |
1138 return _create(constructor, arguments); | |
1139 } catch (e) { | |
1140 // Re-throw any errors (returned as a string) as a DomException. | |
1141 throw new html.DomException.jsInterop(e); | |
1142 } | |
1143 } | |
1144 | |
1145 static JsObject _create(JsFunction constructor, arguments) | |
1146 native "JsObject_constructorCallback"; | |
1147 | |
1148 /** | |
1149 * Constructs a [JsObject] that proxies a native Dart object; _for expert use | |
1150 * only_. | |
1151 * | |
1152 * Use this constructor only if you wish to get access to JavaScript | |
1153 * properties attached to a browser host object, such as a Node or Blob, that | |
1154 * is normally automatically converted into a native Dart object. | |
1155 * | |
1156 * An exception will be thrown if [object] either is `null` or has the type | |
1157 * `bool`, `num`, or `String`. | |
1158 */ | |
1159 factory JsObject.fromBrowserObject(object) { | |
1160 if (object is num || object is String || object is bool || object == null) { | |
1161 throw new ArgumentError("object cannot be a num, string, bool, or null"); | |
1162 } | |
1163 if (object is JsObject) return object; | |
1164 return _fromBrowserObject(object); | |
1165 } | |
1166 | |
1167 /** | |
1168 * Recursively converts a JSON-like collection of Dart objects to a | |
1169 * collection of JavaScript objects and returns a [JsObject] proxy to it. | |
1170 * | |
1171 * [object] must be a [Map] or [Iterable], the contents of which are also | |
1172 * converted. Maps and Iterables are copied to a new JavaScript object. | |
1173 * Primitives and other transferrable values are directly converted to their | |
1174 * JavaScript type, and all other objects are proxied. | |
1175 */ | |
1176 factory JsObject.jsify(object) { | |
1177 if ((object is! Map) && (object is! Iterable)) { | |
1178 throw new ArgumentError("object must be a Map or Iterable"); | |
1179 } | |
1180 return _jsify(object); | |
1181 } | |
1182 | |
1183 static JsObject _jsify(object) native "JsObject_jsify"; | |
1184 | |
1185 static JsObject _fromBrowserObject(object) | |
1186 native "JsObject_fromBrowserObject"; | |
1187 | |
1188 /** | |
1189 * Returns the value associated with [property] from the proxied JavaScript | |
1190 * object. | |
1191 * | |
1192 * The type of [property] must be either [String] or [num]. | |
1193 */ | |
1194 operator [](property) { | |
1195 try { | |
1196 return _operator_getterLegacy(property); | |
1197 } catch (e) { | |
1198 // Re-throw any errors (returned as a string) as a DomException. | |
1199 throw new html.DomException.jsInterop(e); | |
1200 } | |
1201 } | |
1202 | |
1203 _operator_getterLegacy(property) native "JsObject_[]Legacy"; | |
1204 | |
1205 /** | |
1206 * Sets the value associated with [property] on the proxied JavaScript | |
1207 * object. | |
1208 * | |
1209 * The type of [property] must be either [String] or [num]. | |
1210 */ | |
1211 operator []=(property, value) { | |
1212 try { | |
1213 _operator_setterLegacy(property, value); | |
1214 } catch (e) { | |
1215 // Re-throw any errors (returned as a string) as a DomException. | |
1216 throw new html.DomException.jsInterop(e); | |
1217 } | |
1218 } | |
1219 | |
1220 _operator_setterLegacy(property, value) native "JsObject_[]=Legacy"; | |
1221 | |
1222 int get hashCode native "JsObject_hashCode"; | |
1223 | |
1224 operator ==(other) { | |
1225 if (other is! JsObject && other is! JSObject) return false; | |
1226 return _identityEquality(this, other); | |
1227 } | |
1228 | |
1229 static bool _identityEquality(a, b) native "JsObject_identityEquality"; | |
1230 | |
1231 /** | |
1232 * Returns `true` if the JavaScript object contains the specified property | |
1233 * either directly or though its prototype chain. | |
1234 * | |
1235 * This is the equivalent of the `in` operator in JavaScript. | |
1236 */ | |
1237 bool hasProperty(String property) => _hasProperty(property); | |
1238 | |
1239 /** | |
1240 * Removes [property] from the JavaScript object. | |
1241 * | |
1242 * This is the equivalent of the `delete` operator in JavaScript. | |
1243 */ | |
1244 void deleteProperty(String property) native "JsObject_deleteProperty"; | |
1245 | |
1246 /** | |
1247 * Returns `true` if the JavaScript object has [type] in its prototype chain. | |
1248 * | |
1249 * This is the equivalent of the `instanceof` operator in JavaScript. | |
1250 */ | |
1251 bool instanceof(JsFunction type) => _instanceof(type); | |
1252 | |
1253 /** | |
1254 * Returns the result of the JavaScript objects `toString` method. | |
1255 */ | |
1256 String toString() { | |
1257 try { | |
1258 return _toString(); | |
1259 } catch (e) { | |
1260 return super.toString(); | |
1261 } | |
1262 } | |
1263 | |
1264 String _toString() native "JsObject_toString"; | |
1265 | |
1266 /** | |
1267 * Calls [method] on the JavaScript object with the arguments [args] and | |
1268 * returns the result. | |
1269 * | |
1270 * The type of [method] must be either [String] or [num]. | |
1271 */ | |
1272 callMethod(String method, [List args]) { | |
1273 try { | |
1274 return _callMethodLegacy(method, args); | |
1275 } catch (e) { | |
1276 if (hasProperty(method)) { | |
1277 // Return a DomException if DOM call returned an error. | |
1278 throw new html.DomException.jsInterop(e); | |
1279 } else { | |
1280 throw new NoSuchMethodError(this, new Symbol(method), args, null); | |
1281 } | |
1282 } | |
1283 } | |
1284 | |
1285 _callMethodLegacy(String name, List args) native "JsObject_callMethodLegacy"; | |
1286 } | |
1287 | |
1288 /// Base class for all JS objects used through dart:html and typed JS interop. | |
1289 @Deprecated("Internal Use Only") | |
1290 class JSObject extends _JSObjectBase { | |
1291 JSObject.internal() {} | |
1292 external static Type get instanceRuntimeType; | |
1293 | |
1294 /** | |
1295 * Returns the result of the JavaScript objects `toString` method. | |
1296 */ | |
1297 String toString() { | |
1298 try { | |
1299 return _toString(); | |
1300 } catch (e) { | |
1301 return super.toString(); | |
1302 } | |
1303 } | |
1304 | |
1305 noSuchMethod(Invocation invocation) { | |
1306 throwError() { | |
1307 super.noSuchMethod(invocation); | |
1308 } | |
1309 | |
1310 String name = _stripReservedNamePrefix( | |
1311 mirrors.MirrorSystem.getName(invocation.memberName)); | |
1312 argsSafeForTypedInterop(invocation.positionalArguments); | |
1313 if (invocation.isGetter) { | |
1314 if (CHECK_JS_INVOCATIONS) { | |
1315 var matches = _allowedGetters[invocation.memberName]; | |
1316 if (matches == null && | |
1317 !_allowedMethods.containsKey(invocation.memberName)) { | |
1318 throwError(); | |
1319 } | |
1320 var ret = _operator_getter(name); | |
1321 if (matches != null) return ret; | |
1322 if (ret is Function || | |
1323 (ret is JsFunction /* shouldn't be needed in the future*/) && | |
1324 _allowedMethods.containsKey(invocation.memberName)) | |
1325 return ret; // Warning: we have not bound "this"... we could type chec
k on the Function but that is of little value in Dart. | |
1326 throwError(); | |
1327 } else { | |
1328 // TODO(jacobr): should we throw if the JavaScript object doesn't have t
he property? | |
1329 return _operator_getter(name); | |
1330 } | |
1331 } else if (invocation.isSetter) { | |
1332 if (CHECK_JS_INVOCATIONS) { | |
1333 var matches = _allowedSetters[invocation.memberName]; | |
1334 if (matches == null || !matches.checkInvocation(invocation)) | |
1335 throwError(); | |
1336 } | |
1337 assert(name.endsWith("=")); | |
1338 name = name.substring(0, name.length - 1); | |
1339 return _operator_setter(name, invocation.positionalArguments.first); | |
1340 } else { | |
1341 // TODO(jacobr): also allow calling getters that look like functions. | |
1342 var matches; | |
1343 if (CHECK_JS_INVOCATIONS) { | |
1344 matches = _allowedMethods[invocation.memberName]; | |
1345 if (matches == null || !matches.checkInvocation(invocation)) | |
1346 throwError(); | |
1347 } | |
1348 var ret = _callMethod(name, _buildArgs(invocation)); | |
1349 if (CHECK_JS_INVOCATIONS) { | |
1350 if (!matches._checkReturnType(ret)) { | |
1351 html.window.console.error("Return value for method: ${name} is " | |
1352 "${ret.runtimeType} which is inconsistent with all typed " | |
1353 "JS interop definitions for method ${name}."); | |
1354 } | |
1355 } | |
1356 return ret; | |
1357 } | |
1358 } | |
1359 } | |
1360 | |
1361 @Deprecated("Internal Use Only") | |
1362 class JSArray extends JSObject with ListMixin { | |
1363 JSArray.internal() : super.internal(); | |
1364 external static Type get instanceRuntimeType; | |
1365 | |
1366 // Reuse JsArray_length as length behavior is unchanged. | |
1367 int get length native "JsArray_length"; | |
1368 | |
1369 set length(int length) { | |
1370 _operator_setter('length', length); | |
1371 } | |
1372 | |
1373 _checkIndex(int index, {bool insert: false}) { | |
1374 int length = insert ? this.length + 1 : this.length; | |
1375 if (index is int && (index < 0 || index >= length)) { | |
1376 throw new RangeError.range(index, 0, length); | |
1377 } | |
1378 } | |
1379 | |
1380 _checkRange(int start, int end) { | |
1381 int cachedLength = this.length; | |
1382 if (start < 0 || start > cachedLength) { | |
1383 throw new RangeError.range(start, 0, cachedLength); | |
1384 } | |
1385 if (end < start || end > cachedLength) { | |
1386 throw new RangeError.range(end, start, cachedLength); | |
1387 } | |
1388 } | |
1389 | |
1390 _indexed_getter(int index) native "JSArray_indexed_getter"; | |
1391 _indexed_setter(int index, o) native "JSArray_indexed_setter"; | |
1392 | |
1393 // Methods required by ListMixin | |
1394 | |
1395 operator [](index) { | |
1396 if (index is int) { | |
1397 _checkIndex(index); | |
1398 } | |
1399 | |
1400 return _indexed_getter(index); | |
1401 } | |
1402 | |
1403 void operator []=(int index, value) { | |
1404 _checkIndex(index); | |
1405 _indexed_setter(index, value); | |
1406 } | |
1407 } | |
1408 | |
1409 @Deprecated("Internal Use Only") | |
1410 class JSFunction extends JSObject implements Function { | |
1411 JSFunction.internal() : super.internal(); | |
1412 | |
1413 external static Type get instanceRuntimeType; | |
1414 | |
1415 call( | |
1416 [a1 = _UNDEFINED, | |
1417 a2 = _UNDEFINED, | |
1418 a3 = _UNDEFINED, | |
1419 a4 = _UNDEFINED, | |
1420 a5 = _UNDEFINED, | |
1421 a6 = _UNDEFINED, | |
1422 a7 = _UNDEFINED, | |
1423 a8 = _UNDEFINED, | |
1424 a9 = _UNDEFINED, | |
1425 a10 = _UNDEFINED]) { | |
1426 return _apply( | |
1427 _stripUndefinedArgs([a1, a2, a3, a4, a5, a6, a7, a8, a9, a10])); | |
1428 } | |
1429 | |
1430 noSuchMethod(Invocation invocation) { | |
1431 if (invocation.isMethod && invocation.memberName == #call) { | |
1432 return _apply(_buildArgs(invocation)); | |
1433 } | |
1434 return super.noSuchMethod(invocation); | |
1435 } | |
1436 | |
1437 dynamic _apply(List args, {thisArg}) native "JSFunction_apply"; | |
1438 | |
1439 static JSFunction _createWithThis(Function f) | |
1440 native "JSFunction_createWithThis"; | |
1441 static JSFunction _create(Function f) native "JSFunction_create"; | |
1442 } | |
1443 | |
1444 // JavaScript interop methods that do not automatically wrap to dart:html types. | |
1445 // Warning: this API is not exposed to dart:js. | |
1446 // TODO(jacobr): rename to JSNative and make at least part of this API public. | |
1447 @Deprecated("Internal Use Only") | |
1448 class JsNative { | |
1449 static JSObject jsify(object) native "JSObject_jsify"; | |
1450 static JSObject newObject() native "JSObject_newObject"; | |
1451 static JSArray newArray() native "JSObject_newArray"; | |
1452 | |
1453 static hasProperty(_JSObjectBase o, name) => o._hasProperty(name); | |
1454 static getProperty(_JSObjectBase o, name) => o._operator_getter(name); | |
1455 static setProperty(_JSObjectBase o, name, value) => | |
1456 o._operator_setter(name, value); | |
1457 static callMethod(_JSObjectBase o, String method, List args) => | |
1458 o._callMethod(method, args); | |
1459 static instanceof(_JSObjectBase o, /*JsFunction|JSFunction*/ type) => | |
1460 o._instanceof(type); | |
1461 static callConstructor0(_JSObjectBase constructor) | |
1462 native "JSNative_callConstructor0"; | |
1463 static callConstructor(_JSObjectBase constructor, List args) | |
1464 native "JSNative_callConstructor"; | |
1465 | |
1466 static toTypedObject(JsObject o) native "JSNative_toTypedObject"; | |
1467 | |
1468 /** | |
1469 * Same behavior as new JsFunction.withThis except that JavaScript "this" is n
ot | |
1470 * wrapped. | |
1471 */ | |
1472 static JSFunction withThis(Function f) native "JsFunction_withThisNoWrap"; | |
1473 } | |
1474 | |
1475 /** | |
1476 * Proxies a JavaScript Function object. | |
1477 */ | |
1478 class JsFunction extends JsObject { | |
1479 JsFunction.internal() : super.internal(); | |
1480 | |
1481 /** | |
1482 * Returns a [JsFunction] that captures its 'this' binding and calls [f] | |
1483 * with the value of this passed as the first argument. | |
1484 */ | |
1485 factory JsFunction.withThis(Function f) => _withThis(f); | |
1486 | |
1487 /** | |
1488 * Invokes the JavaScript function with arguments [args]. If [thisArg] is | |
1489 * supplied it is the value of `this` for the invocation. | |
1490 */ | |
1491 dynamic apply(List args, {thisArg}) => _apply(args, thisArg: thisArg); | |
1492 | |
1493 dynamic _apply(List args, {thisArg}) native "JsFunction_apply"; | |
1494 | |
1495 /** | |
1496 * Internal only version of apply which uses debugger proxies of Dart objects | |
1497 * rather than opaque handles. This method is private because it cannot be | |
1498 * efficiently implemented in Dart2Js so should only be used by internal | |
1499 * tools. | |
1500 */ | |
1501 _applyDebuggerOnly(List args, {thisArg}) | |
1502 native "JsFunction_applyDebuggerOnly"; | |
1503 | |
1504 static JsFunction _withThis(Function f) native "JsFunction_withThis"; | |
1505 } | |
1506 | |
1507 /** | |
1508 * A [List] proxying a JavaScript Array. | |
1509 */ | |
1510 class JsArray<E> extends JsObject with ListMixin<E> { | |
1511 JsArray.internal() : super.internal(); | |
1512 | |
1513 factory JsArray() => _newJsArray(); | |
1514 | |
1515 static JsArray _newJsArray() native "JsArray_newJsArray"; | |
1516 | |
1517 factory JsArray.from(Iterable<E> other) => | |
1518 _newJsArrayFromSafeList(new List.from(other)); | |
1519 | |
1520 static JsArray _newJsArrayFromSafeList(List list) | |
1521 native "JsArray_newJsArrayFromSafeList"; | |
1522 | |
1523 _checkIndex(int index, {bool insert: false}) { | |
1524 int length = insert ? this.length + 1 : this.length; | |
1525 if (index is int && (index < 0 || index >= length)) { | |
1526 throw new RangeError.range(index, 0, length); | |
1527 } | |
1528 } | |
1529 | |
1530 _checkRange(int start, int end) { | |
1531 int cachedLength = this.length; | |
1532 if (start < 0 || start > cachedLength) { | |
1533 throw new RangeError.range(start, 0, cachedLength); | |
1534 } | |
1535 if (end < start || end > cachedLength) { | |
1536 throw new RangeError.range(end, start, cachedLength); | |
1537 } | |
1538 } | |
1539 | |
1540 // Methods required by ListMixin | |
1541 | |
1542 E operator [](index) { | |
1543 if (index is int) { | |
1544 _checkIndex(index); | |
1545 } | |
1546 | |
1547 return super[index]; | |
1548 } | |
1549 | |
1550 void operator []=(index, E value) { | |
1551 if (index is int) { | |
1552 _checkIndex(index); | |
1553 } | |
1554 super[index] = value; | |
1555 } | |
1556 | |
1557 int get length native "JsArray_length"; | |
1558 | |
1559 set length(int length) { | |
1560 super['length'] = length; | |
1561 } | |
1562 | |
1563 // Methods overridden for better performance | |
1564 | |
1565 void add(E value) { | |
1566 callMethod('push', [value]); | |
1567 } | |
1568 | |
1569 void addAll(Iterable<E> iterable) { | |
1570 // TODO(jacobr): this can be optimized slightly. | |
1571 callMethod('push', new List.from(iterable)); | |
1572 } | |
1573 | |
1574 void insert(int index, E element) { | |
1575 _checkIndex(index, insert: true); | |
1576 callMethod('splice', [index, 0, element]); | |
1577 } | |
1578 | |
1579 E removeAt(int index) { | |
1580 _checkIndex(index); | |
1581 return callMethod('splice', [index, 1])[0]; | |
1582 } | |
1583 | |
1584 E removeLast() { | |
1585 if (length == 0) throw new RangeError(-1); | |
1586 return callMethod('pop'); | |
1587 } | |
1588 | |
1589 void removeRange(int start, int end) { | |
1590 _checkRange(start, end); | |
1591 callMethod('splice', [start, end - start]); | |
1592 } | |
1593 | |
1594 void setRange(int start, int end, Iterable<E> iterable, [int skipCount = 0]) { | |
1595 _checkRange(start, end); | |
1596 int length = end - start; | |
1597 if (length == 0) return; | |
1598 if (skipCount < 0) throw new ArgumentError(skipCount); | |
1599 var args = [start, length]..addAll(iterable.skip(skipCount).take(length)); | |
1600 callMethod('splice', args); | |
1601 } | |
1602 | |
1603 void sort([int compare(E a, E b)]) { | |
1604 callMethod('sort', [compare]); | |
1605 } | |
1606 } | |
1607 | |
1608 /** | |
1609 * Placeholder object for cases where we need to determine exactly how many | |
1610 * args were passed to a function. | |
1611 */ | |
1612 const _UNDEFINED = const Object(); | |
1613 | |
1614 // TODO(jacobr): this method is a hack to work around the lack of proper dart | |
1615 // support for varargs methods. | |
1616 List _stripUndefinedArgs(List args) => | |
1617 args.takeWhile((i) => i != _UNDEFINED).toList(); | |
1618 | |
1619 /** | |
1620 * Check that that if [arg] is a [Function] it is safe to pass to JavaScript. | |
1621 * To make a function safe, call [allowInterop] or [allowInteropCaptureThis]. | |
1622 */ | |
1623 @Deprecated("Internal Use Only") | |
1624 safeForTypedInterop(arg) { | |
1625 if (CHECK_JS_INVOCATIONS && arg is Function && arg is! JSFunction) { | |
1626 throw new ArgumentError( | |
1627 "Attempt to pass Function '$arg' to JavaScript via without calling allow
Interop or allowInteropCaptureThis"); | |
1628 } | |
1629 } | |
1630 | |
1631 /** | |
1632 * Check that that if any elements of [args] are [Function] it is safe to pass | |
1633 * to JavaScript. To make a function safe, call [allowInterop] or | |
1634 * [allowInteropCaptureThis]. | |
1635 */ | |
1636 @Deprecated("Internal Use Only") | |
1637 void argsSafeForTypedInterop(Iterable args) { | |
1638 for (var arg in args) { | |
1639 safeForTypedInterop(arg); | |
1640 } | |
1641 } | |
1642 | |
1643 /** | |
1644 * Returns a method that can be called with an arbitrary number (for n less | |
1645 * than 11) of arguments without violating Dart type checks. | |
1646 */ | |
1647 Function _wrapAsDebuggerVarArgsFunction(JsFunction jsFunction) => ( | |
1648 [a1 = _UNDEFINED, | |
1649 a2 = _UNDEFINED, | |
1650 a3 = _UNDEFINED, | |
1651 a4 = _UNDEFINED, | |
1652 a5 = _UNDEFINED, | |
1653 a6 = _UNDEFINED, | |
1654 a7 = _UNDEFINED, | |
1655 a8 = _UNDEFINED, | |
1656 a9 = _UNDEFINED, | |
1657 a10 = _UNDEFINED]) => | |
1658 jsFunction._applyDebuggerOnly( | |
1659 _stripUndefinedArgs([a1, a2, a3, a4, a5, a6, a7, a8, a9, a10])); | |
1660 | |
1661 /// Returns a wrapper around function [f] that can be called from JavaScript | |
1662 /// using the package:js Dart-JavaScript interop. | |
1663 /// | |
1664 /// For performance reasons in Dart2Js, by default Dart functions cannot be | |
1665 /// passed directly to JavaScript unless this method is called to create | |
1666 /// a Function compatible with both Dart and JavaScript. | |
1667 /// Calling this method repeatedly on a function will return the same function. | |
1668 /// The [Function] returned by this method can be used from both Dart and | |
1669 /// JavaScript. We may remove the need to call this method completely in the | |
1670 /// future if Dart2Js is refactored so that its function calling conventions | |
1671 /// are more compatible with JavaScript. | |
1672 Function/*=F*/ allowInterop/*<F extends Function>*/(Function/*=F*/ f) { | |
1673 if (f is JSFunction) { | |
1674 // The function is already a JSFunction... no need to do anything. | |
1675 return f; | |
1676 } else { | |
1677 return JSFunction._create(f); | |
1678 } | |
1679 } | |
1680 | |
1681 /// Cached JSFunction associated with the Dart function when "this" is | |
1682 /// captured. | |
1683 Expando<JSFunction> _interopCaptureThisExpando = new Expando<JSFunction>(); | |
1684 | |
1685 /// Returns a [Function] that when called from JavaScript captures its 'this' | |
1686 /// binding and calls [f] with the value of this passed as the first argument. | |
1687 /// When called from Dart, [null] will be passed as the first argument. | |
1688 /// | |
1689 /// See the documentation for [allowInterop]. This method should only be used | |
1690 /// with package:js Dart-JavaScript interop. | |
1691 JSFunction allowInteropCaptureThis(Function f) { | |
1692 if (f is JSFunction) { | |
1693 // Behavior when the function is already a JS function is unspecified. | |
1694 throw new ArgumentError( | |
1695 "Function is already a JS function so cannot capture this."); | |
1696 return f; | |
1697 } else { | |
1698 var ret = _interopCaptureThisExpando[f]; | |
1699 if (ret == null) { | |
1700 // TODO(jacobr): we could optimize this. | |
1701 ret = JSFunction._createWithThis(f); | |
1702 _interopCaptureThisExpando[f] = ret; | |
1703 } | |
1704 return ret; | |
1705 } | |
1706 } | |
OLD | NEW |