Skip to content

Commit

Permalink
Add WeakMap and WeakSet polyfills.
Browse files Browse the repository at this point in the history
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=126762367
  • Loading branch information
shicks authored and blickly committed Jul 7, 2016
1 parent 7febc35 commit d5f6f30
Show file tree
Hide file tree
Showing 12 changed files with 561 additions and 51 deletions.
52 changes: 18 additions & 34 deletions src/com/google/javascript/jscomp/js/es6/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@
* limitations under the License.
*/

'require es6/symbol es6/util/makeiterator util/defineproperty';
'require util/owns util/polyfill';
'require es6/symbol es6/util/makeiterator es6/weakmap util/owns util/polyfill';

/**
* Whether to skip the conformance check and simply use the polyfill always.
Expand Down Expand Up @@ -64,6 +63,9 @@ $jscomp.polyfill('Map', function(NativeMap) {
$jscomp.initSymbolIterator();


/** @const {!WeakMap<!Object, string>} */
var idMap = new WeakMap();


/**
* Internal record type for entries.
Expand Down Expand Up @@ -322,13 +324,6 @@ $jscomp.polyfill('Map', function(NativeMap) {
};


/**
* Counter for generating IDs.
* @private {number}
*/
var mapIndex = 0;


/**
* Makes a new "head" element.
* @return {!MapEntry<KEY, VALUE>}
Expand All @@ -343,40 +338,29 @@ $jscomp.polyfill('Map', function(NativeMap) {


/**
* Fixed key used for storing generated object IDs.
* @const {symbol}
* Counter for generating IDs.
* @private {number}
*/
var idKey = Symbol('map-id-key');
var mapIndex = 0;


/**
* @param {*} obj An extensible object.
* @return {string} A unique ID.
*/
var getId = function(obj) {
// TODO(sdh): could use goog.getUid for this if it exists.
// (This might work better with goog.defineClass)
if (!(obj instanceof Object)) {
// Prepend primitives with 'p_', which will avoid potentially dangerous
// names like '__proto__', as well as anything from Object.prototype.
return 'p_' + obj;
}
if (!(idKey in obj)) {
/** @preserveTry */
try {
$jscomp.defineProperty(obj, idKey, {value: ++mapIndex});
} catch (ignored) {}
}
if (!(idKey in obj)) {
// String representation is best we can do, though it's not stricty
// guaranteed to be consistent (i.e. for mutable objects). But for
// non-extensible objects, there's nothing better we could possibly
// use for bucketing. We prepend 'o_' (for object) for two reasons:
// (1) to distinguish generated IDs (which are digits) and primitives,
// and (2) to prevent illegal or dangerous keys (see above).
return 'o_ ' + obj;
var type = obj && typeof obj;
if (type == 'object' || type == 'function') {
obj = /** @type {!Object} */ (obj);
if (!idMap.has(obj)) {
var id = '' + (++mapIndex);
idMap.set(obj, id);
return id;
}
return idMap.get(obj);
}
return obj[idKey];
// Add a prefix since obj could be '__proto__';
return 'p_' + obj;
};


Expand Down
2 changes: 0 additions & 2 deletions src/com/google/javascript/jscomp/js/es6/nopolyfill.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@

$jscomp.polyfill('Proxy', null, 'es6', 'es6');

$jscomp.polyfill('WeakMap', null, 'es6-impl', 'es6-impl');
$jscomp.polyfill('WeakSet', null, 'es6-impl', 'es6-impl');
$jscomp.polyfill('Reflect', null, 'es6-impl', 'es6-impl');
$jscomp.polyfill('Object.setPrototypeOf', null, 'es6-impl', 'es6-impl');
$jscomp.polyfill('String.raw', null, 'es6-impl', 'es6-impl');
Expand Down
188 changes: 188 additions & 0 deletions src/com/google/javascript/jscomp/js/es6/weakmap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/*
* Copyright 2016 The Closure Compiler Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

'require es6/symbol es6/util/makeiterator util/defineproperty';
'require util/owns util/patch util/polyfill';

$jscomp.polyfill('WeakMap', function(NativeWeakMap) {
/**
* Checks conformance of the existing WeakMap.
* @return {boolean} True if the browser's implementation conforms.
*/
function isConformant() {
if (!NativeWeakMap || !Object.seal) return false;
var x = Object.seal({});
var y = Object.seal({});
var map = new /** @type {function(new: WeakMap, !Array)} */ (NativeWeakMap)(
[[x, 2], [y, 3]]);
if (map.get(x) != 2 || map.get(y) != 3) return false;
map.delete(x);
map.set(y, 4);
return !map.has(x) && map.get(y) == 4;
}
if (isConformant()) return NativeWeakMap;

var prop = '$jscomp_hidden_' + Math.random().toString().substring(2);
var hidden = {}; // tag used to indicate a hidden object.

/**
* Inserts the hidden property into the target.
* @param {!Object} target
*/
function insert(target) {
if (!$jscomp.owns(target, prop)) {
var obj = {};
// TODO(sdh): This property will be enumerated in IE8. If this becomes
// a problem, we could avoid it by copying an infrequently-used non-enum
// method (like toLocaleString) onto the object itself and encoding the
// property on the copy instead. This codepath must be easily removable
// if IE8 support is not needed.
obj[prop] = hidden;
$jscomp.defineProperty(target, prop, {value: obj});
}
}

/**
* Monkey-patches the key-enumeration methods to ensure that the hidden
* property is not returned.
* @param {function(!Object): !Array<string>} prev
* @return {function(!Object): !Array<string>}
*/
function fixList(prev) {
return function(target) {
var result = prev(target);
if ($jscomp.owns(target, prop) &&
$jscomp.owns(target[prop], prop) &&
target[prop][prop] === hidden) {
for (var i = 0; i < result.length; i++) {
if (result[i] == prop) {
result.splice(i, 1);
break;
}
}
}
return result;
};
}
$jscomp.patch('Object.getOwnPropertyNames', fixList);
$jscomp.patch('Object.keys', fixList);
$jscomp.patch('Reflect.enumerate', fixList);
$jscomp.patch('Reflect.ownKeys', fixList);

/**
* Monkey-patches the freezing methods to ensure that the hidden
* property is added before any freezing happens.
* @param {function(!Object): !Object} prev
* @return {function(!Object): !Object}
*/
function preInsert(prev) {
return function(target) {
insert(target);
return prev(target);
};
}
$jscomp.patch('Object.freeze', preInsert);
$jscomp.patch('Object.preventExtensions', preInsert);
$jscomp.patch('Object.seal', preInsert);
// Note: no need to patch Reflect.preventExtensions since the polyfill
// just calls Object.preventExtensions anyway (and if it's not polyfilled
// then neither is WeakMap).

var index = 0;

/**
* Polyfill for WeakMap:
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap
*
* This implementation is as non-leaky as possible, due to patching
* the freezing and sealing operations. It does not include any logic
* to handle cases where a key was somehow made non-extensible without
* the special hidden property being added. It takes some care to ensure
* the hidden property is not enumerated over nor discoverable, though
* it's not completely secure (particularly in IE8).
*
* @constructor
* @template KEY, VALUE
* @param {!Iterator<!Array<KEY|VALUE>>|!Array<!Array<KEY|VALUE>>|null=}
* opt_iterable Optional initial data.
*/
var PolyfillWeakMap = function(opt_iterable) {
/** @private @const {string} */
this.id_ = (index += (Math.random() + 1)).toString();

if (opt_iterable) {
$jscomp.initSymbol();
$jscomp.initSymbolIterator();
var iter = $jscomp.makeIterator(opt_iterable);
var entry;
while (!(entry = iter.next()).done) {
var item = entry.value;
this.set(/** @type {KEY} */ (item[0]), /** @type {VALUE} */ (item[1]));
}
}
};

/**
* @param {KEY} key
* @param {VALUE} value
* @return {!PolyfillWeakMap}
*/
PolyfillWeakMap.prototype.set = function(key, value) {
insert(key);
if (!$jscomp.owns(key, prop)) {
// NOTE: If the insert() call fails on the key, but the property
// has previously successfully been added higher up the prototype
// chain, then we'll silently misbehave. Instead, throw immediately
// before doing something bad. If this becomes a problem (e.g. due
// to some rogue frozen objects) then we may need to add a slow and
// leaky fallback array to each WeakMap instance, as well as extra
// logic in each accessor to use it (*only*) when necessary.
throw new Error('WeakMap key fail: ' + key);
}
key[prop][this.id_] = value;
return this;
};

/**
* @param {KEY} key
* @return {VALUE}
*/
PolyfillWeakMap.prototype.get = function(key) {
return $jscomp.owns(key, prop) ? key[prop][this.id_] : undefined;
};

/**
* @param {KEY} key
* @return {boolean}
*/
PolyfillWeakMap.prototype.has = function(key) {
return $jscomp.owns(key, prop) && $jscomp.owns(key[prop], this.id_);
};

/**
* @param {KEY} key
* @return {boolean}
*/
PolyfillWeakMap.prototype.delete = function(key) {
if (!$jscomp.owns(key, prop) ||
!$jscomp.owns(key[prop], this.id_)) {
return false;
}
return delete key[prop][this.id_];
};

return PolyfillWeakMap;
}, 'es6-impl', 'es3');
84 changes: 84 additions & 0 deletions src/com/google/javascript/jscomp/js/es6/weakset.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright 2016 The Closure Compiler Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

'require es6/symbol es6/util/makeiterator util/polyfill es6/weakmap';

$jscomp.polyfill('WeakSet', function(NativeWeakSet) {
/**
* Checks conformance of the existing WeakSet.
* @return {boolean} True if the browser's implementation conforms.
*/
function isConformant() {
if (!NativeWeakSet || !Object.seal) return false;
var x = Object.seal({});
var y = Object.seal({});
var set = new /** @type {function(new: WeakSet, !Array)} */ (NativeWeakSet)(
[x]);
if (!set.has(x) || set.has(y)) return false;
set.delete(x);
set.add(y);
return !set.has(x) && set.has(y);
}
if (isConformant()) return NativeWeakSet;

/**
* @constructor
* @template TYPE
* @param {!Iterator<TYPE>|!Array<TYPE>|null=} opt_iterable
*/
var PolyfillWeakSet = function(opt_iterable) {
/** @private @const {!WeakMap<TYPE, boolean>} */
this.map_ = new WeakMap();

if (opt_iterable) {
$jscomp.initSymbol();
$jscomp.initSymbolIterator();
var iter = $jscomp.makeIterator(opt_iterable);
var entry;
while (!(entry = iter.next()).done) {
var item = entry.value;
this.add(item);
}
}
};

/**
* @param {TYPE} elem
* @return {!PolyfillWeakSet<TYPE>}
*/
PolyfillWeakSet.prototype.add = function(elem) {
this.map_.set(elem, true);
return this;
};

/**
* @param {TYPE} elem
* @return {boolean}
*/
PolyfillWeakSet.prototype.has = function(elem) {
return this.map_.has(elem);
};

/**
* @param {TYPE} elem
* @return {boolean}
*/
PolyfillWeakSet.prototype.delete = function(elem) {
return this.map_.delete(elem);
};

return PolyfillWeakSet;
}, 'es6-impl', 'es3');
1 change: 1 addition & 0 deletions src/com/google/javascript/jscomp/js/es6_runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@
'require es6/set es6/string es6/symbol es6/util/arrayfromiterable';
'require es6/util/arrayfromiterator es6/util/inherits';
'require es6/util/iteratorfromarray es6/util/makeiterator';
'require es6/weakmap es6/weakset';
4 changes: 2 additions & 2 deletions src/com/google/javascript/jscomp/js/polyfills.txt
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,5 @@ String.prototype.normalize es6-impl es6-impl
String.prototype.repeat es6-impl es3 es6/string/repeat
String.prototype.startsWith es6-impl es3 es6/string/startswith
String.raw es6-impl es6-impl
WeakMap es6-impl es6-impl
WeakSet es6-impl es6-impl
WeakMap es6-impl es3 es6/weakmap
WeakSet es6-impl es3 es6/weakset
2 changes: 1 addition & 1 deletion src/com/google/javascript/jscomp/resources.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,6 @@ delete String.prototype.startsWith;
delete String.prototype.endsWith;

delete Symbol;

delete WeakMap;
delete WeakSet;

0 comments on commit d5f6f30

Please sign in to comment.