548 lines
24 KiB
JavaScript
548 lines
24 KiB
JavaScript
"use strict";
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
exports.LeakImplementation = undefined;
|
|
|
|
var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
|
|
|
|
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); /**
|
|
* Copyright (c) 2017-present, Facebook, Inc.
|
|
* All rights reserved.
|
|
*
|
|
* This source code is licensed under the BSD-style license found in the
|
|
* LICENSE file in the root directory of this source tree. An additional grant
|
|
* of patent rights can be found in the PATENTS file in the same directory.
|
|
*/
|
|
|
|
var _errors = require("../errors.js");
|
|
|
|
var _environment = require("../environment.js");
|
|
|
|
var _index = require("../values/index.js");
|
|
|
|
var _index2 = require("../methods/index.js");
|
|
|
|
var _babelTypes = require("babel-types");
|
|
|
|
var t = _interopRequireWildcard(_babelTypes);
|
|
|
|
var _babelTraverse = require("babel-traverse");
|
|
|
|
var _babelTraverse2 = _interopRequireDefault(_babelTraverse);
|
|
|
|
var _invariant = require("../invariant.js");
|
|
|
|
var _invariant2 = _interopRequireDefault(_invariant);
|
|
|
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
|
|
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
|
|
|
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
|
|
function visitName(path, state, name, read, write) {
|
|
// Is the name bound to some local identifier? If so, we don't need to do anything
|
|
if (path.scope.hasBinding(name, /*noGlobals*/true)) return;
|
|
|
|
// Otherwise, let's record that there's an unbound identifier
|
|
if (read) state.unboundReads.add(name);
|
|
if (write) state.unboundWrites.add(name);
|
|
}
|
|
|
|
function ignorePath(path) {
|
|
var parent = path.parent;
|
|
return t.isLabeledStatement(parent) || t.isBreakStatement(parent) || t.isContinueStatement(parent);
|
|
}
|
|
|
|
var LeakedClosureRefVisitor = {
|
|
ReferencedIdentifier: function ReferencedIdentifier(path, state) {
|
|
if (ignorePath(path)) return;
|
|
|
|
var innerName = path.node.name;
|
|
if (innerName === "arguments") {
|
|
return;
|
|
}
|
|
visitName(path, state, innerName, true, false);
|
|
},
|
|
"AssignmentExpression|UpdateExpression": function AssignmentExpressionUpdateExpression(path, state) {
|
|
var doesRead = path.node.operator !== "=";
|
|
for (var name in path.getBindingIdentifiers()) {
|
|
visitName(path, state, name, doesRead, true);
|
|
}
|
|
}
|
|
};
|
|
|
|
function getLeakedFunctionInfo(value) {
|
|
// TODO: This should really be cached on a per AST basis in case we have
|
|
// many uses of the same closure. It should ideally share this cache
|
|
// and data with ResidualHeapVisitor.
|
|
(0, _invariant2.default)(value instanceof _index.ECMAScriptSourceFunctionValue);
|
|
(0, _invariant2.default)(value.constructor === _index.ECMAScriptSourceFunctionValue);
|
|
var functionInfo = {
|
|
unboundReads: new Set(),
|
|
unboundWrites: new Set()
|
|
};
|
|
var formalParameters = value.$FormalParameters;
|
|
(0, _invariant2.default)(formalParameters != null);
|
|
var code = value.$ECMAScriptCode;
|
|
(0, _invariant2.default)(code != null);
|
|
|
|
(0, _babelTraverse2.default)(t.file(t.program([t.expressionStatement(t.functionExpression(null, formalParameters, code))])), LeakedClosureRefVisitor, null, functionInfo);
|
|
return functionInfo;
|
|
}
|
|
|
|
var ObjectValueLeakingVisitor = function () {
|
|
// ObjectValues to visit if they're reachable.
|
|
function ObjectValueLeakingVisitor(objectsTrackedForLeaks) {
|
|
_classCallCheck(this, ObjectValueLeakingVisitor);
|
|
|
|
this.objectsTrackedForLeaks = objectsTrackedForLeaks;
|
|
this.visitedValues = new Set();
|
|
}
|
|
// Values that has been visited.
|
|
|
|
|
|
_createClass(ObjectValueLeakingVisitor, [{
|
|
key: "mustVisit",
|
|
value: function mustVisit(val) {
|
|
if (val instanceof _index.ObjectValue) {
|
|
// For Objects we only need to visit it if it is tracked
|
|
// as a newly created object that might still be mutated.
|
|
// Abstract values gets their arguments visited.
|
|
if (!this.objectsTrackedForLeaks.has(val)) return false;
|
|
}
|
|
if (this.visitedValues.has(val)) return false;
|
|
this.visitedValues.add(val);
|
|
return true;
|
|
}
|
|
}, {
|
|
key: "visitObjectProperty",
|
|
value: function visitObjectProperty(binding) {
|
|
var desc = binding.descriptor;
|
|
if (desc === undefined) return; //deleted
|
|
this.visitDescriptor(desc);
|
|
}
|
|
}, {
|
|
key: "visitObjectProperties",
|
|
value: function visitObjectProperties(obj, kind) {
|
|
// visit symbol properties
|
|
var _iteratorNormalCompletion = true;
|
|
var _didIteratorError = false;
|
|
var _iteratorError = undefined;
|
|
|
|
try {
|
|
for (var _iterator = obj.symbols[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
|
|
var _ref = _step.value;
|
|
|
|
var _ref2 = _slicedToArray(_ref, 2);
|
|
|
|
var propertyBindingValue = _ref2[1];
|
|
|
|
(0, _invariant2.default)(propertyBindingValue);
|
|
this.visitObjectProperty(propertyBindingValue);
|
|
}
|
|
|
|
// visit string properties
|
|
} catch (err) {
|
|
_didIteratorError = true;
|
|
_iteratorError = err;
|
|
} finally {
|
|
try {
|
|
if (!_iteratorNormalCompletion && _iterator.return) {
|
|
_iterator.return();
|
|
}
|
|
} finally {
|
|
if (_didIteratorError) {
|
|
throw _iteratorError;
|
|
}
|
|
}
|
|
}
|
|
|
|
var _iteratorNormalCompletion2 = true;
|
|
var _didIteratorError2 = false;
|
|
var _iteratorError2 = undefined;
|
|
|
|
try {
|
|
for (var _iterator2 = obj.properties[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
|
|
var _ref3 = _step2.value;
|
|
|
|
var _ref4 = _slicedToArray(_ref3, 2);
|
|
|
|
var _propertyBindingValue = _ref4[1];
|
|
|
|
(0, _invariant2.default)(_propertyBindingValue);
|
|
this.visitObjectProperty(_propertyBindingValue);
|
|
}
|
|
|
|
// inject properties with computed names
|
|
} catch (err) {
|
|
_didIteratorError2 = true;
|
|
_iteratorError2 = err;
|
|
} finally {
|
|
try {
|
|
if (!_iteratorNormalCompletion2 && _iterator2.return) {
|
|
_iterator2.return();
|
|
}
|
|
} finally {
|
|
if (_didIteratorError2) {
|
|
throw _iteratorError2;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (obj.unknownProperty !== undefined) {
|
|
var desc = obj.unknownProperty.descriptor;
|
|
if (desc !== undefined) {
|
|
var val = desc.value;
|
|
(0, _invariant2.default)(val instanceof _index.AbstractValue);
|
|
this.visitObjectPropertiesWithComputedNames(val);
|
|
}
|
|
}
|
|
|
|
// prototype
|
|
this.visitObjectPrototype(obj);
|
|
|
|
// if this object wasn't already leaked, we need mark it as leaked
|
|
// so that any mutation and property access get tracked after this.
|
|
if (!obj.isLeakedObject()) {
|
|
obj.leak();
|
|
}
|
|
}
|
|
}, {
|
|
key: "visitObjectPrototype",
|
|
value: function visitObjectPrototype(obj) {
|
|
var proto = obj.$Prototype;
|
|
this.visitValue(proto);
|
|
}
|
|
}, {
|
|
key: "visitObjectPropertiesWithComputedNames",
|
|
value: function visitObjectPropertiesWithComputedNames(absVal) {
|
|
(0, _invariant2.default)(absVal.args.length === 3);
|
|
var cond = absVal.args[0];
|
|
(0, _invariant2.default)(cond instanceof _index.AbstractValue);
|
|
if (cond.kind === "template for property name condition") {
|
|
var P = cond.args[0];
|
|
(0, _invariant2.default)(P instanceof _index.AbstractValue);
|
|
var V = absVal.args[1];
|
|
var earlier_props = absVal.args[2];
|
|
if (earlier_props instanceof _index.AbstractValue) this.visitObjectPropertiesWithComputedNames(earlier_props);
|
|
this.visitValue(P);
|
|
this.visitValue(V);
|
|
} else {
|
|
// conditional assignment
|
|
this.visitValue(cond);
|
|
var consequent = absVal.args[1];
|
|
(0, _invariant2.default)(consequent instanceof _index.AbstractValue);
|
|
var alternate = absVal.args[2];
|
|
(0, _invariant2.default)(alternate instanceof _index.AbstractValue);
|
|
this.visitObjectPropertiesWithComputedNames(consequent);
|
|
this.visitObjectPropertiesWithComputedNames(alternate);
|
|
}
|
|
}
|
|
}, {
|
|
key: "visitDescriptor",
|
|
value: function visitDescriptor(desc) {
|
|
(0, _invariant2.default)(desc.value === undefined || desc.value instanceof _index.Value);
|
|
if (desc.value !== undefined) this.visitValue(desc.value);
|
|
if (desc.get !== undefined) this.visitValue(desc.get);
|
|
if (desc.set !== undefined) this.visitValue(desc.set);
|
|
}
|
|
}, {
|
|
key: "visitDeclarativeEnvironmentRecordBinding",
|
|
value: function visitDeclarativeEnvironmentRecordBinding(record, remainingLeakedBindings) {
|
|
var bindings = record.bindings;
|
|
var _iteratorNormalCompletion3 = true;
|
|
var _didIteratorError3 = false;
|
|
var _iteratorError3 = undefined;
|
|
|
|
try {
|
|
for (var _iterator3 = Object.keys(bindings)[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
|
|
var bindingName = _step3.value;
|
|
|
|
var binding = bindings[bindingName];
|
|
// Check if this binding is referenced, and if so delete it from the set.
|
|
var isRead = remainingLeakedBindings.unboundReads.delete(bindingName);
|
|
var isWritten = remainingLeakedBindings.unboundWrites.delete(bindingName);
|
|
if (isRead) {
|
|
// If this binding can be read from the closure, its value has now leaked.
|
|
var value = binding.value;
|
|
if (value) {
|
|
this.visitValue(value);
|
|
}
|
|
}
|
|
if (isWritten || isRead) {
|
|
// If this binding could have been mutated from the closure, then the
|
|
// binding itself has now leaked, but not necessarily the value in it.
|
|
// TODO: We could tag a leaked binding as read and/or write. That way
|
|
// we don't have to leak values written to this binding if only writes
|
|
// have leaked. We also don't have to leak reads from this binding
|
|
// if it is only read from.
|
|
(0, _environment.leakBinding)(binding);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
_didIteratorError3 = true;
|
|
_iteratorError3 = err;
|
|
} finally {
|
|
try {
|
|
if (!_iteratorNormalCompletion3 && _iterator3.return) {
|
|
_iterator3.return();
|
|
}
|
|
} finally {
|
|
if (_didIteratorError3) {
|
|
throw _iteratorError3;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}, {
|
|
key: "visitValueMap",
|
|
value: function visitValueMap(val) {
|
|
var kind = val.getKind();
|
|
|
|
var entries = void 0;
|
|
if (kind === "Map") {
|
|
entries = val.$MapData;
|
|
} else {
|
|
(0, _invariant2.default)(kind === "WeakMap");
|
|
entries = val.$WeakMapData;
|
|
}
|
|
(0, _invariant2.default)(entries !== undefined);
|
|
var len = entries.length;
|
|
|
|
for (var i = 0; i < len; i++) {
|
|
var entry = entries[i];
|
|
var key = entry.$Key;
|
|
var value = entry.$Value;
|
|
if (key === undefined || value === undefined) continue;
|
|
this.visitValue(key);
|
|
this.visitValue(value);
|
|
}
|
|
}
|
|
}, {
|
|
key: "visitValueSet",
|
|
value: function visitValueSet(val) {
|
|
var kind = val.getKind();
|
|
|
|
var entries = void 0;
|
|
if (kind === "Set") {
|
|
entries = val.$SetData;
|
|
} else {
|
|
(0, _invariant2.default)(kind === "WeakSet");
|
|
entries = val.$WeakSetData;
|
|
}
|
|
(0, _invariant2.default)(entries !== undefined);
|
|
var len = entries.length;
|
|
|
|
for (var i = 0; i < len; i++) {
|
|
var entry = entries[i];
|
|
if (entry === undefined) continue;
|
|
this.visitValue(entry);
|
|
}
|
|
}
|
|
}, {
|
|
key: "visitValueFunction",
|
|
value: function visitValueFunction(val) {
|
|
if (val.isLeakedObject()) {
|
|
return;
|
|
}
|
|
this.visitObjectProperties(val);
|
|
|
|
if (val instanceof _index.BoundFunctionValue) {
|
|
this.visitValue(val.$BoundTargetFunction);
|
|
this.visitValue(val.$BoundThis);
|
|
var _iteratorNormalCompletion4 = true;
|
|
var _didIteratorError4 = false;
|
|
var _iteratorError4 = undefined;
|
|
|
|
try {
|
|
for (var _iterator4 = val.$BoundArguments[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) {
|
|
var boundArg = _step4.value;
|
|
this.visitValue(boundArg);
|
|
}
|
|
} catch (err) {
|
|
_didIteratorError4 = true;
|
|
_iteratorError4 = err;
|
|
} finally {
|
|
try {
|
|
if (!_iteratorNormalCompletion4 && _iterator4.return) {
|
|
_iterator4.return();
|
|
}
|
|
} finally {
|
|
if (_didIteratorError4) {
|
|
throw _iteratorError4;
|
|
}
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
(0, _invariant2.default)(!(val instanceof _index.NativeFunctionValue), "all native function values should have already been created outside this pure function");
|
|
|
|
var remainingLeakedBindings = getLeakedFunctionInfo(val);
|
|
|
|
var environment = val.$Environment.parent;
|
|
while (environment) {
|
|
var record = environment.environmentRecord;
|
|
if (record instanceof _environment.ObjectEnvironmentRecord) {
|
|
this.visitValue(record.object);
|
|
continue;
|
|
}
|
|
|
|
(0, _invariant2.default)(!(record instanceof _environment.GlobalEnvironmentRecord), "we should never reach the global scope because it is never newly created in a pure function.");
|
|
(0, _invariant2.default)(record instanceof _environment.DeclarativeEnvironmentRecord);
|
|
|
|
this.visitDeclarativeEnvironmentRecordBinding(record, remainingLeakedBindings);
|
|
|
|
if (record instanceof _environment.FunctionEnvironmentRecord) {
|
|
// If this is a function environment, which is not tracked for leaks,
|
|
// we can bail out because its bindings should not be mutated in a
|
|
// pure function.
|
|
var fn = record.$FunctionObject;
|
|
if (!this.objectsTrackedForLeaks.has(fn)) {
|
|
break;
|
|
}
|
|
}
|
|
environment = environment.parent;
|
|
}
|
|
}
|
|
}, {
|
|
key: "visitValueObject",
|
|
value: function visitValueObject(val) {
|
|
if (val.isLeakedObject()) {
|
|
return;
|
|
}
|
|
|
|
var kind = val.getKind();
|
|
this.visitObjectProperties(val, kind);
|
|
|
|
switch (kind) {
|
|
case "RegExp":
|
|
case "Number":
|
|
case "String":
|
|
case "Boolean":
|
|
case "ReactElement":
|
|
case "ArrayBuffer":
|
|
return;
|
|
case "Date":
|
|
var dateValue = val.$DateValue;
|
|
(0, _invariant2.default)(dateValue !== undefined);
|
|
this.visitValue(dateValue);
|
|
return;
|
|
case "Float32Array":
|
|
case "Float64Array":
|
|
case "Int8Array":
|
|
case "Int16Array":
|
|
case "Int32Array":
|
|
case "Uint8Array":
|
|
case "Uint16Array":
|
|
case "Uint32Array":
|
|
case "Uint8ClampedArray":
|
|
case "DataView":
|
|
var buf = val.$ViewedArrayBuffer;
|
|
(0, _invariant2.default)(buf !== undefined);
|
|
this.visitValue(buf);
|
|
return;
|
|
case "Map":
|
|
case "WeakMap":
|
|
this.visitValueMap(val);
|
|
return;
|
|
case "Set":
|
|
case "WeakSet":
|
|
this.visitValueSet(val);
|
|
return;
|
|
default:
|
|
(0, _invariant2.default)(kind === "Object", "Object of kind " + kind + " is not supported in calls to abstract functions.");
|
|
(0, _invariant2.default)(this.$ParameterMap === undefined, "Arguments object is not supported in calls to abstract functions.");
|
|
return;
|
|
}
|
|
}
|
|
}, {
|
|
key: "visitValueProxy",
|
|
value: function visitValueProxy(val) {
|
|
this.visitValue(val.$ProxyTarget);
|
|
this.visitValue(val.$ProxyHandler);
|
|
}
|
|
}, {
|
|
key: "visitAbstractValue",
|
|
value: function visitAbstractValue(val) {
|
|
for (var i = 0, n = val.args.length; i < n; i++) {
|
|
this.visitValue(val.args[i]);
|
|
}
|
|
}
|
|
}, {
|
|
key: "visitValue",
|
|
value: function visitValue(val) {
|
|
if (val instanceof _index.AbstractValue) {
|
|
if (this.mustVisit(val)) this.visitAbstractValue(val);
|
|
} else if (val.isIntrinsic()) {
|
|
// All intrinsic values exist from the beginning of time...
|
|
// ...except for a few that come into existance as templates for abstract objects (TODO #882).
|
|
this.mustVisit(val);
|
|
} else if (val instanceof _index.EmptyValue) {
|
|
this.mustVisit(val);
|
|
} else if (val instanceof _index.PrimitiveValue) {
|
|
this.mustVisit(val);
|
|
} else if (val instanceof _index.ProxyValue) {
|
|
if (this.mustVisit(val)) this.visitValueProxy(val);
|
|
} else if (val instanceof _index.FunctionValue) {
|
|
(0, _invariant2.default)(val instanceof _index.FunctionValue);
|
|
if (this.mustVisit(val)) this.visitValueFunction(val);
|
|
} else {
|
|
(0, _invariant2.default)(val instanceof _index.ObjectValue);
|
|
if (val.originalConstructor !== undefined) {
|
|
(0, _invariant2.default)(val instanceof _index.ObjectValue);
|
|
if (this.mustVisit(val)) this.visitValueObject(val);
|
|
} else {
|
|
if (this.mustVisit(val)) this.visitValueObject(val);
|
|
}
|
|
}
|
|
}
|
|
}]);
|
|
|
|
return ObjectValueLeakingVisitor;
|
|
}();
|
|
|
|
function ensureFrozenValue(realm, value, loc) {
|
|
// TODO: This should really check if it is recursively immutability.
|
|
if (value instanceof _index.ObjectValue && !(0, _index2.TestIntegrityLevel)(realm, value, "frozen")) {
|
|
var diag = new _errors.CompilerDiagnostic("Unfrozen object leaked before end of global code", loc || realm.currentLocation, "PP0017", "RecoverableError");
|
|
if (realm.handleError(diag) !== "Recover") throw new _errors.FatalError();
|
|
}
|
|
}
|
|
|
|
// Ensure that a value is immutable. If it is not, set all its properties to abstract values
|
|
// and all reachable bindings to abstract values.
|
|
|
|
var LeakImplementation = exports.LeakImplementation = function () {
|
|
function LeakImplementation() {
|
|
_classCallCheck(this, LeakImplementation);
|
|
}
|
|
|
|
_createClass(LeakImplementation, [{
|
|
key: "leakValue",
|
|
value: function leakValue(realm, value, loc) {
|
|
var objectsTrackedForLeaks = realm.createdObjectsTrackedForLeaks;
|
|
if (objectsTrackedForLeaks === undefined) {
|
|
// We're not tracking a pure function. That means that we would track
|
|
// everything as leaked. We'll assume that any object argument
|
|
// is invalid unless it's frozen.
|
|
ensureFrozenValue(realm, value, loc);
|
|
} else {
|
|
// If we're tracking a pure function, we can assume that only newly
|
|
// created objects and bindings, within it, are mutable. Any other
|
|
// object can safely be assumed to be deeply immutable as far as this
|
|
// pure function is concerned. However, any mutable object needs to
|
|
// be tainted as possibly having changed to anything.
|
|
var visitor = new ObjectValueLeakingVisitor(objectsTrackedForLeaks);
|
|
visitor.visitValue(value);
|
|
}
|
|
}
|
|
}]);
|
|
|
|
return LeakImplementation;
|
|
}();
|
|
//# sourceMappingURL=leak.js.map
|