blob: 8f96d0733f0d996ad6eaefaf8c26575212e7165b [file] [log] [blame]
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
part of _js_helper;
// TODO(ngeoffray): stop using this method once our optimizers can
// change str1.contains(str2) into str1.indexOf(str2) != -1.
bool contains(String userAgent, String name) {
return JS('int', '#.indexOf(#)', userAgent, name) != -1;
}
int arrayLength(List array) {
return JS('int', '#.length', array);
}
arrayGet(List array, int index) {
return JS('var', '#[#]', array, index);
}
void arraySet(List array, int index, var value) {
JS('var', '#[#] = #', array, index, value);
}
propertyGet(var object, String property) {
return JS('var', '#[#]', object, property);
}
bool callHasOwnProperty(var function, var object, String property) {
return JS('bool', '#.call(#, #)', function, object, property);
}
void propertySet(var object, String property, var value) {
JS('var', '#[#] = #', object, property, value);
}
getPropertyFromPrototype(var object, String name) {
return JS('var', 'Object.getPrototypeOf(#)[#]', object, name);
}
newJsObject() {
return JS('var', '{}');
}
/**
* Returns a String tag identifying the type of the native object, or `null`.
* The tag is not the name of the type, but usually the name of the JavaScript
* constructor function. Initialized by [initHooks].
*/
Function getTagFunction;
/**
* If a lookup via [getTagFunction] on an object [object] that has [tag] fails,
* this function is called to provide an alternate tag. This allows us to fail
* gracefully if we can make a good guess, for example, when browsers add novel
* kinds of HTMLElement that we have never heard of. Initialized by
* [initHooks].
*/
Function alternateTagFunction;
/**
* Returns the prototype for the JavaScript constructor named by an input tag.
* Returns `null` if there is no such constructor, or if pre-patching of the
* constructor is to be avoided. Initialized by [initHooks].
*/
Function prototypeForTagFunction;
String toStringForNativeObject(var obj) {
// TODO(sra): Is this code dead?
// [getTagFunction] might be uninitialized, but in usual usage, toString has
// been called via an interceptor and initialized it.
String name = getTagFunction == null
? '<Unknown>'
: JS('String', '#', getTagFunction(obj));
return 'Instance of $name';
}
int hashCodeForNativeObject(object) => Primitives.objectHashCode(object);
/**
* Sets a JavaScript property on an object.
*/
void defineProperty(var obj, String property, var value) {
JS('void',
'Object.defineProperty(#, #, '
'{value: #, enumerable: false, writable: true, configurable: true})',
obj,
property,
value);
}
// Is [obj] an instance of a Dart-defined class?
bool isDartObject(obj) {
// Some of the extra parens here are necessary.
return JS('bool', '((#) instanceof (#))', obj, JS_DART_OBJECT_CONSTRUCTOR());
}
/**
* A JavaScript object mapping tags to the constructors of interceptors.
* This is a JavaScript object with no prototype.
*
* Example: 'HTMLImageElement' maps to the ImageElement class constructor.
*/
get interceptorsByTag => JS('=Object', 'init.interceptorsByTag');
/**
* A JavaScript object mapping tags to `true` or `false`.
*
* Example: 'HTMLImageElement' maps to `true` since, as there are no subclasses
* of ImageElement, it is a leaf class in the native class hierarchy.
*/
get leafTags => JS('=Object', 'init.leafTags');
String findDispatchTagForInterceptorClass(interceptorClassConstructor) {
return JS('', r'#.$nativeSuperclassTag', interceptorClassConstructor);
}
/**
* Cache of dispatch records for instances. This is a JavaScript object used as
* a map. Keys are instance tags, e.g. "!SomeThing". The cache permits the
* sharing of one dispatch record between multiple instances.
*/
var dispatchRecordsForInstanceTags;
/**
* Cache of interceptors indexed by uncacheable tags, e.g. "~SomeThing".
* This is a JavaScript object used as a map.
*/
var interceptorsForUncacheableTags;
lookupInterceptor(String tag) {
return propertyGet(interceptorsByTag, tag);
}
// Dispatch tag marks are optional prefixes for a dispatch tag that direct how
// the interceptor for the tag may be cached.
/// No caching permitted.
const UNCACHED_MARK = '~';
/// Dispatch record must be cached per instance
const INSTANCE_CACHED_MARK = '!';
/// Dispatch record is cached on immediate prototype.
const LEAF_MARK = '-';
/// Dispatch record is cached on immediate prototype with a prototype
/// verification to prevent the interceptor being associated with a subclass
/// before a dispatch record is cached on the subclass.
const INTERIOR_MARK = '+';
/// A 'discriminator' function is to be used. TBD.
const DISCRIMINATED_MARK = '*';
/**
* Returns the interceptor for a native object, or returns `null` if not found.
*
* A dispatch record is cached according to the specification of the dispatch
* tag for [obj].
*/
lookupAndCacheInterceptor(obj) {
assert(!isDartObject(obj));
String tag = getTagFunction(obj);
// Fast path for instance (and uncached) tags because the lookup is repeated
// for each instance (or getInterceptor call).
var record = propertyGet(dispatchRecordsForInstanceTags, tag);
if (record != null) return patchInstance(obj, record);
var interceptor = propertyGet(interceptorsForUncacheableTags, tag);
if (interceptor != null) return interceptor;
// This lookup works for derived dispatch tags because we add them all in
// [initNativeDispatch].
var interceptorClass = lookupInterceptor(tag);
if (interceptorClass == null) {
tag = alternateTagFunction(obj, tag);
if (tag != null) {
// Fast path for instance and uncached tags again.
record = propertyGet(dispatchRecordsForInstanceTags, tag);
if (record != null) return patchInstance(obj, record);
interceptor = propertyGet(interceptorsForUncacheableTags, tag);
if (interceptor != null) return interceptor;
interceptorClass = lookupInterceptor(tag);
}
}
if (interceptorClass == null) {
// This object is not known to Dart. There could be several reasons for
// that, including (but not limited to):
//
// * A bug in native code (hopefully this is caught during development).
// * An unknown DOM object encountered.
// * JavaScript code running in an unexpected context. For example, on
// node.js.
return null;
}
interceptor = JS('', '#.prototype', interceptorClass);
var mark = JS('String|Null', '#[0]', tag);
if (mark == INSTANCE_CACHED_MARK) {
record = makeLeafDispatchRecord(interceptor);
propertySet(dispatchRecordsForInstanceTags, tag, record);
return patchInstance(obj, record);
}
if (mark == UNCACHED_MARK) {
propertySet(interceptorsForUncacheableTags, tag, interceptor);
return interceptor;
}
if (mark == LEAF_MARK) {
return patchProto(obj, makeLeafDispatchRecord(interceptor));
}
if (mark == INTERIOR_MARK) {
return patchInteriorProto(obj, interceptor);
}
if (mark == DISCRIMINATED_MARK) {
// TODO(sra): Use discriminator of tag.
throw new UnimplementedError(tag);
}
// [tag] was not explicitly an interior or leaf tag, so
var isLeaf = JS('bool', '(#[#]) === true', leafTags, tag);
if (isLeaf) {
return patchProto(obj, makeLeafDispatchRecord(interceptor));
} else {
return patchInteriorProto(obj, interceptor);
}
}
patchInstance(obj, record) {
setDispatchProperty(obj, record);
return dispatchRecordInterceptor(record);
}
patchProto(obj, record) {
setDispatchProperty(JS('', 'Object.getPrototypeOf(#)', obj), record);
return dispatchRecordInterceptor(record);
}
patchInteriorProto(obj, interceptor) {
var proto = JS('', 'Object.getPrototypeOf(#)', obj);
var record = makeDispatchRecord(interceptor, proto, null, null);
setDispatchProperty(proto, record);
return interceptor;
}
makeLeafDispatchRecord(interceptor) {
var fieldName = JS_IS_INDEXABLE_FIELD_NAME();
bool indexability = JS('bool', r'!!#[#]', interceptor, fieldName);
return makeDispatchRecord(interceptor, false, null, indexability);
}
makeDefaultDispatchRecord(tag, interceptorClass, proto) {
var interceptor = JS('', '#.prototype', interceptorClass);
var isLeaf = JS('bool', '(#[#]) === true', leafTags, tag);
if (isLeaf) {
return makeLeafDispatchRecord(interceptor);
} else {
return makeDispatchRecord(interceptor, proto, null, null);
}
}
/**
* [proto] should have no shadowing prototypes that are not also assigned a
* dispatch rescord.
*/
setNativeSubclassDispatchRecord(proto, interceptor) {
setDispatchProperty(proto, makeLeafDispatchRecord(interceptor));
}
String constructorNameFallback(object) {
return JS('String', '#(#)', _constructorNameFallback, object);
}
var initNativeDispatchFlag; // null or true
void initNativeDispatch() {
if (true == initNativeDispatchFlag) return;
initNativeDispatchFlag = true;
initNativeDispatchContinue();
}
void initNativeDispatchContinue() {
dispatchRecordsForInstanceTags = JS('', 'Object.create(null)');
interceptorsForUncacheableTags = JS('', 'Object.create(null)');
initHooks();
// Try to pro-actively patch prototypes of DOM objects. For each of our known
// tags `TAG`, if `window.TAG` is a (constructor) function, set the dispatch
// property if the function's prototype to a dispatch record.
var map = interceptorsByTag;
var tags = JS('JSMutableArray', 'Object.getOwnPropertyNames(#)', map);
if (JS('bool', 'typeof window != "undefined"')) {
var context = JS('=Object', 'window');
var fun = JS('=Object', 'function () {}');
for (int i = 0; i < tags.length; i++) {
var tag = tags[i];
var proto = prototypeForTagFunction(tag);
if (proto != null) {
var interceptorClass = JS('', '#[#]', map, tag);
var record = makeDefaultDispatchRecord(tag, interceptorClass, proto);
if (record != null) {
setDispatchProperty(proto, record);
// Ensure the modified prototype is still fast by assigning it to
// the prototype property of a function object.
JS('', '#.prototype = #', fun, proto);
}
}
}
}
// [interceptorsByTag] maps 'plain' dispatch tags. Add all the derived
// dispatch tags to simplify lookup of derived tags.
for (int i = 0; i < tags.length; i++) {
var tag = JS('String', '#[#]', tags, i);
if (JS('bool', '/^[A-Za-z_]/.test(#)', tag)) {
var interceptorClass = propertyGet(map, tag);
propertySet(map, INSTANCE_CACHED_MARK + tag, interceptorClass);
propertySet(map, UNCACHED_MARK + tag, interceptorClass);
propertySet(map, LEAF_MARK + tag, interceptorClass);
propertySet(map, INTERIOR_MARK + tag, interceptorClass);
propertySet(map, DISCRIMINATED_MARK + tag, interceptorClass);
}
}
}
/**
* Initializes [getTagFunction] and [alternateTagFunction].
*
* These functions are 'hook functions', collectively 'hooks'. They initialized
* by applying a series of hooks transformers. Built-in hooks transformers deal
* with various known browser behaviours.
*
* Each hook tranformer takes a 'hooks' input which is a JavaScript object
* containing the hook functions, and returns the same or a new object with
* replacements. The replacements can wrap the originals to provide alternate
* or modified behaviour.
*
* { getTag: function(obj) {...},
* getUnknownTag: function(obj, tag) {...},
* prototypeForTag: function(tag) {...},
* discriminator: function(tag) {...},
* }
*
* * getTag(obj) returns the dispatch tag, or `null`.
* * getUnknownTag(obj, tag) returns a tag when [getTag] fails.
* * prototypeForTag(tag) returns the prototype of the constructor for tag,
* or `null` if not available or prepatching is undesirable.
* * discriminator(tag) returns a function TBD.
*
* The web site can adapt a dart2js application by loading code ahead of the
* dart2js application that defines hook transformers to be after the built in
* ones. Code defining a transformer HT should use the following pattern to
* ensure multiple transformers can be composed:
*
* (dartNativeDispatchHooksTransformer =
* window.dartNativeDispatchHooksTransformer || []).push(HT);
*
*
* TODO: Implement and describe dispatch tags and their caching methods.
*/
void initHooks() {
// The initial simple hooks:
var hooks = JS('', '#()', _baseHooks);
// Customize for browsers where `object.constructor.name` fails:
var _fallbackConstructorHooksTransformer =
JS('', '#(#)', _fallbackConstructorHooksTransformerGenerator,
_constructorNameFallback);
hooks = applyHooksTransformer(_fallbackConstructorHooksTransformer, hooks);
// Customize for browsers:
hooks = applyHooksTransformer(_firefoxHooksTransformer, hooks);
hooks = applyHooksTransformer(_ieHooksTransformer, hooks);
hooks = applyHooksTransformer(_operaHooksTransformer, hooks);
hooks = applyHooksTransformer(_safariHooksTransformer, hooks);
hooks = applyHooksTransformer(_fixDocumentHooksTransformer, hooks);
// TODO(sra): Update ShadowDOM polyfil to use
// [dartNativeDispatchHooksTransformer] and remove this hook.
hooks = applyHooksTransformer(_dartExperimentalFixupGetTagHooksTransformer,
hooks);
// Apply global hooks.
//
// If defined, dartNativeDispatchHookdTransformer should be a single function
// of a JavaScript Array of functions.
if (JS('bool', 'typeof dartNativeDispatchHooksTransformer != "undefined"')) {
var transformers = JS('', 'dartNativeDispatchHooksTransformer');
if (JS('bool', 'typeof # == "function"', transformers)) {
transformers = [transformers];
}
if (JS('bool', '#.constructor == Array', transformers)) {
for (int i = 0; i < JS('int', '#.length', transformers); i++) {
var transformer = JS('', '#[#]', transformers, i);
if (JS('bool', 'typeof # == "function"', transformer)) {
hooks = applyHooksTransformer(transformer, hooks);
}
}
}
}
var getTag = JS('', '#.getTag', hooks);
var getUnknownTag = JS('', '#.getUnknownTag', hooks);
var prototypeForTag = JS('', '#.prototypeForTag', hooks);
getTagFunction = (o) => JS('String|Null', '#(#)', getTag, o);
alternateTagFunction =
(o, String tag) => JS('String|Null', '#(#, #)', getUnknownTag, o, tag);
prototypeForTagFunction =
(String tag) => JS('', '#(#)', prototypeForTag, tag);
}
applyHooksTransformer(transformer, hooks) {
var newHooks = JS('=Object|Null', '#(#)', transformer, hooks);
return JS('', '# || #', newHooks, hooks);
}
// JavaScript code fragments.
//
// This is a temporary place for the JavaScript code.
//
// TODO(sra): These code fragments are not minified. They could be generated by
// the code emitter, or JS_CONST could be improved to parse entire functions and
// take care of the minification.
const _baseHooks = const JS_CONST(r'''
function() {
function typeNameInChrome(o) {
var name = o.constructor.name;
if (name) return name;
var s = Object.prototype.toString.call(o);
return s.substring(8, s.length - 1);
}
function getUnknownTag(object, tag) {
// This code really belongs in [getUnknownTagGenericBrowser] but having it
// here allows [getUnknownTag] to be tested on d8.
if (/^HTML[A-Z].*Element$/.test(tag)) {
// Check that it is not a simple JavaScript object.
var name = Object.prototype.toString.call(object);
if (name == "[object Object]") return null;
return "HTMLElement";
}
}
function getUnknownTagGenericBrowser(object, tag) {
if (object instanceof HTMLElement) return "HTMLElement";
return getUnknownTag(object, tag);
}
function prototypeForTag(tag) {
if (typeof window == "undefined") return null;
if (typeof window[tag] == "undefined") return null;
var constructor = window[tag];
if (typeof constructor != "function") return null;
return constructor.prototype;
}
function discriminator(tag) { return null; }
var isBrowser = typeof navigator == "object";
return {
getTag: typeNameInChrome,
getUnknownTag: isBrowser ? getUnknownTagGenericBrowser : getUnknownTag,
prototypeForTag: prototypeForTag,
discriminator: discriminator };
}''');
/**
* Returns the name of the constructor function for browsers where
* `object.constructor.name` is not reliable.
*
* This function is split out of [_fallbackConstructorHooksTransformerGenerator]
* as it is called from both the dispatch hooks and via
* [constructorNameFallback] from objectToString.
*/
const _constructorNameFallback = const JS_CONST(r'''
function getTagFallback(o) {
var constructor = o.constructor;
if (typeof constructor == "function") {
var name = constructor.name;
// If the name is a non-empty string, we use that as the type name of this
// object. There are various cases where that does not work, so we have to
// detect them and fall through to the toString() based implementation.
if (typeof name == "string" &&
// Sometimes the string is empty. This test also catches minified
// shadow dom polyfil wrapper for Window on Firefox where the faked
// constructor name does not 'stick'. The shortest real DOM object
// names have three characters (e.g. URL, CSS).
name.length > 2 &&
// On Firefox we often get "Object" as the constructor name, even for
// more specialized DOM objects.
name !== "Object" &&
// This can happen in Opera.
name !== "Function.prototype") {
return name;
}
}
var s = Object.prototype.toString.call(o);
return s.substring(8, s.length - 1);
}''');
const _fallbackConstructorHooksTransformerGenerator = const JS_CONST(r'''
function(getTagFallback) {
return function(hooks) {
// If we are not in a browser, assume we are in d8.
// TODO(sra): Recognize jsshell.
if (typeof navigator != "object") return hooks;
var ua = navigator.userAgent;
// TODO(antonm): remove a reference to DumpRenderTree.
if (ua.indexOf("DumpRenderTree") >= 0) return hooks;
if (ua.indexOf("Chrome") >= 0) {
// Confirm constructor name is usable for dispatch.
function confirm(p) {
return typeof window == "object" && window[p] && window[p].name == p;
}
if (confirm("Window") && confirm("HTMLElement")) return hooks;
}
hooks.getTag = getTagFallback;
};
}''');
const _ieHooksTransformer = const JS_CONST(r'''
function(hooks) {
var userAgent = typeof navigator == "object" ? navigator.userAgent : "";
if (userAgent.indexOf("Trident/") == -1) return hooks;
var getTag = hooks.getTag;
var quickMap = {
"BeforeUnloadEvent": "Event",
"DataTransfer": "Clipboard",
"HTMLDDElement": "HTMLElement",
"HTMLDTElement": "HTMLElement",
"HTMLPhraseElement": "HTMLElement",
"Position": "Geoposition"
};
function getTagIE(o) {
var tag = getTag(o);
var newTag = quickMap[tag];
if (newTag) return newTag;
// Patches for types which report themselves as Objects.
if (tag == "Object") {
if (window.DataView && (o instanceof window.DataView)) return "DataView";
}
return tag;
}
function prototypeForTagIE(tag) {
var constructor = window[tag];
if (constructor == null) return null;
return constructor.prototype;
}
hooks.getTag = getTagIE;
hooks.prototypeForTag = prototypeForTagIE;
}''');
const _fixDocumentHooksTransformer = const JS_CONST(r'''
function(hooks) {
var getTag = hooks.getTag;
var prototypeForTag = hooks.prototypeForTag;
function getTagFixed(o) {
var tag = getTag(o);
if (tag == "Document") {
// Some browsers and the polymer polyfill call both HTML and XML documents
// "Document", so we check for the xmlVersion property, which is the empty
// string on HTML documents. Since both dart:html classes Document and
// HtmlDocument share the same type, we must patch the instances and not
// the prototype.
if (!!o.xmlVersion) return "!Document";
return "!HTMLDocument";
}
return tag;
}
function prototypeForTagFixed(tag) {
if (tag == "Document") return null; // Do not pre-patch Document.
return prototypeForTag(tag);
}
hooks.getTag = getTagFixed;
hooks.prototypeForTag = prototypeForTagFixed;
}''');
const _firefoxHooksTransformer = const JS_CONST(r'''
function(hooks) {
var userAgent = typeof navigator == "object" ? navigator.userAgent : "";
if (userAgent.indexOf("Firefox") == -1) return hooks;
var getTag = hooks.getTag;
var quickMap = {
"BeforeUnloadEvent": "Event",
"DataTransfer": "Clipboard",
"GeoGeolocation": "Geolocation",
"Location": "!Location", // Fixes issue 18151
"WorkerMessageEvent": "MessageEvent",
"XMLDocument": "!Document"};
function getTagFirefox(o) {
var tag = getTag(o);
return quickMap[tag] || tag;
}
hooks.getTag = getTagFirefox;
}''');
const _operaHooksTransformer = const JS_CONST(r'''
function(hooks) { return hooks; }
''');
const _safariHooksTransformer = const JS_CONST(r'''
function(hooks) { return hooks; }
''');
const _dartExperimentalFixupGetTagHooksTransformer = const JS_CONST(r'''
function(hooks) {
if (typeof dartExperimentalFixupGetTag != "function") return hooks;
hooks.getTag = dartExperimentalFixupGetTag(hooks.getTag);
}''');