From 119d5c58716624d0162061ae6d14a51c3f4a025c Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Thu, 8 Jun 2017 03:39:10 -0400 Subject: [PATCH 1/2] Hoist several closures --- .../src/index.js | 19 +- .../src/index.js | 17 +- .../babel-traverse/src/path/evaluation.js | 653 +++++++++--------- packages/babel-traverse/src/path/family.js | 23 +- 4 files changed, 360 insertions(+), 352 deletions(-) diff --git a/packages/babel-helper-builder-react-jsx/src/index.js b/packages/babel-helper-builder-react-jsx/src/index.js index fb495d1c88..370affcbca 100644 --- a/packages/babel-helper-builder-react-jsx/src/index.js +++ b/packages/babel-helper-builder-react-jsx/src/index.js @@ -120,6 +120,14 @@ export default function (opts) { return state.call || t.callExpression(state.callee, args); } + function pushProps(_props, objs) { + if (!_props.length) return _props; + + objs.push(t.objectExpression(_props)); + return []; + } + + /** * The logic for this is quite terse. It's because we need to * support spread elements. We loop over all attributes, @@ -137,24 +145,17 @@ export default function (opts) { "useBuiltIns (defaults to false)"); } - function pushProps() { - if (!_props.length) return; - - objs.push(t.objectExpression(_props)); - _props = []; - } - while (attribs.length) { const prop = attribs.shift(); if (t.isJSXSpreadAttribute(prop)) { - pushProps(); + _props = pushProps(_props, objs); objs.push(prop.argument); } else { _props.push(convertAttribute(prop)); } } - pushProps(); + pushProps(_props, objs); if (objs.length === 1) { // only one object diff --git a/packages/babel-plugin-transform-es2015-spread/src/index.js b/packages/babel-plugin-transform-es2015-spread/src/index.js index d533d484b5..d1c8697ab3 100644 --- a/packages/babel-plugin-transform-es2015-spread/src/index.js +++ b/packages/babel-plugin-transform-es2015-spread/src/index.js @@ -16,27 +16,26 @@ export default function ({ types: t }) { return false; } + function push(_props, nodes) { + if (!_props.length) return _props; + nodes.push(t.arrayExpression(_props)); + return []; + } + function build(props: Array, scope, state) { const nodes = []; - let _props = []; - function push() { - if (!_props.length) return; - nodes.push(t.arrayExpression(_props)); - _props = []; - } - for (const prop of props) { if (t.isSpreadElement(prop)) { - push(); + _props = push(_props, nodes); nodes.push(getSpreadLiteral(prop, scope, state)); } else { _props.push(prop); } } - push(); + push(_props, nodes); return nodes; } diff --git a/packages/babel-traverse/src/path/evaluation.js b/packages/babel-traverse/src/path/evaluation.js index f28a1e148e..8fd8497926 100644 --- a/packages/babel-traverse/src/path/evaluation.js +++ b/packages/babel-traverse/src/path/evaluation.js @@ -28,6 +28,327 @@ export function evaluateTruthy(): boolean { if (res.confident) return !!res.value; } +/** + * Deopts the evaluation + */ +function deopt(path, state) { + if (!state.confident) return; + state.deoptPath = path; + state.confident = false; +} + +/** + * We wrap the _evaluate method so we can track `seen` nodes, we push an item + * to the map before we actually evaluate it so we can deopt on self recursive + * nodes such as: + * + * var g = a ? 1 : 2, + * a = g * this.foo + */ +function evaluateCached(path, state) { + const { node } = path; + const { seen } = state; + + if (seen.has(node)) { + const existing = seen.get(node); + if (existing.resolved) { + return existing.value; + } else { + deopt(path, state); + return; + } + } else { + const item = { resolved: false }; + seen.set(node, item); + + const val = _evaluate(path, state); + if (state.confident) { + item.resolved = true; + item.value = val; + } + return val; + } +} + +function _evaluate(path, state) { + if (!state.confident) return; + + const { node } = path; + + if (path.isSequenceExpression()) { + const exprs = path.get("expressions"); + return evaluateCached(exprs[exprs.length - 1], state); + } + + if (path.isStringLiteral() || path.isNumericLiteral() || path.isBooleanLiteral()) { + return node.value; + } + + if (path.isNullLiteral()) { + return null; + } + + if (path.isTemplateLiteral()) { + let str = ""; + + let i = 0; + const exprs = path.get("expressions"); + + for (const elem of (node.quasis: Array)) { + // not confident, evaluated an expression we don't like + if (!state.confident) break; + + // add on cooked element + str += elem.value.cooked; + + // add on interpolated expression if it's present + const expr = exprs[i++]; + if (expr) str += String(evaluateCached(expr, state)); + } + + if (!state.confident) return; + return str; + } + + if (path.isConditionalExpression()) { + const testResult = evaluateCached(path.get("test"), state); + if (!state.confident) return; + if (testResult) { + return evaluateCached(path.get("consequent"), state); + } else { + return evaluateCached(path.get("alternate"), state); + } + } + + if (path.isExpressionWrapper()) { // TypeCastExpression, ExpressionStatement etc + return evaluateCached(path.get("expression"), state); + } + + // "foo".length + if (path.isMemberExpression() && !path.parentPath.isCallExpression({ callee: node })) { + const property = path.get("property"); + const object = path.get("object"); + + if (object.isLiteral() && property.isIdentifier()) { + const value = object.node.value; + const type = typeof value; + if (type === "number" || type === "string") { + return value[property.node.name]; + } + } + } + + if (path.isReferencedIdentifier()) { + const binding = path.scope.getBinding(node.name); + + if (binding && binding.constantViolations.length > 0) { + return deopt(binding.path, state); + } + + if (binding && path.node.start < binding.path.node.end) { + return deopt(binding.path, state); + } + + if (binding && binding.hasValue) { + return binding.value; + } else { + if (node.name === "undefined") { + return binding ? deopt(binding.path, state) : undefined; + } else if (node.name === "Infinity") { + return binding ? deopt(binding.path, state) : Infinity; + } else if (node.name === "NaN") { + return binding ? deopt(binding.path, state) : NaN; + } + + const resolved = path.resolve(); + if (resolved === path) { + return deopt(path, state); + } else { + return evaluateCached(resolved, state); + } + } + } + + if (path.isUnaryExpression({ prefix: true })) { + if (node.operator === "void") { + // we don't need to evaluate the argument to know what this will return + return undefined; + } + + const argument = path.get("argument"); + if (node.operator === "typeof" && (argument.isFunction() || argument.isClass())) { + return "function"; + } + + const arg = evaluateCached(argument, state); + if (!state.confident) return; + switch (node.operator) { + case "!": return !arg; + case "+": return +arg; + case "-": return -arg; + case "~": return ~arg; + case "typeof": return typeof arg; + } + } + + if (path.isArrayExpression()) { + const arr = []; + const elems: Array = path.get("elements"); + for (let elem of elems) { + elem = elem.evaluate(); + + if (elem.confident) { + arr.push(elem.value); + } else { + return deopt(elem, state); + } + } + return arr; + } + + if (path.isObjectExpression()) { + const obj = {}; + const props: Array = path.get("properties"); + for (const prop of props) { + if (prop.isObjectMethod() || prop.isSpreadElement()) { + return deopt(prop, state); + } + const keyPath = prop.get("key"); + let key = keyPath; + if (prop.node.computed) { + key = key.evaluate(); + if (!key.confident) { + return deopt(keyPath, state); + } + key = key.value; + } else if (key.isIdentifier()) { + key = key.node.name; + } else { + key = key.node.value; + } + const valuePath = prop.get("value"); + let value = valuePath.evaluate(); + if (!value.confident) { + return deopt(valuePath, state); + } + value = value.value; + obj[key] = value; + } + return obj; + } + + if (path.isLogicalExpression()) { + // If we are confident that one side of an && is false, or the left + // side of an || is true, we can be confident about the entire expression + const wasConfident = state.confident; + const left = evaluateCached(path.get("left"), state); + const leftConfident = state.confident; + state.confident = wasConfident; + const right = evaluateCached(path.get("right"), state); + const rightConfident = state.confident; + state.confident = leftConfident && rightConfident; + + switch (node.operator) { + case "||": + // TODO consider having a "truthy type" that doesn't bail on + // left uncertainity but can still evaluate to truthy. + if (left && leftConfident) { + state.confident = true; + return left; + } + + if (!state.confident) return; + + return left || right; + case "&&": + if ((!left && leftConfident) || (!right && rightConfident)) { + state.confident = true; + } + + if (!state.confident) return; + + return left && right; + } + } + + if (path.isBinaryExpression()) { + const left = evaluateCached(path.get("left"), state); + if (!state.confident) return; + const right = evaluateCached(path.get("right"), state); + if (!state.confident) return; + + switch (node.operator) { + case "-": return left - right; + case "+": return left + right; + case "/": return left / right; + case "*": return left * right; + case "%": return left % right; + case "**": return left ** right; + case "<": return left < right; + case ">": return left > right; + case "<=": return left <= right; + case ">=": return left >= right; + case "==": return left == right; // eslint-disable-line eqeqeq + case "!=": return left != right; + case "===": return left === right; + case "!==": return left !== right; + case "|": return left | right; + case "&": return left & right; + case "^": return left ^ right; + case "<<": return left << right; + case ">>": return left >> right; + case ">>>": return left >>> right; + } + } + + if (path.isCallExpression()) { + const callee = path.get("callee"); + let context; + let func; + + // Number(1); + if ( + callee.isIdentifier() && !path.scope.getBinding(callee.node.name, true) && + VALID_CALLEES.indexOf(callee.node.name) >= 0 + ) { + func = global[node.callee.name]; + } + + if (callee.isMemberExpression()) { + const object = callee.get("object"); + const property = callee.get("property"); + + // Math.min(1, 2) + if ( + object.isIdentifier() && property.isIdentifier() && + VALID_CALLEES.indexOf(object.node.name) >= 0 && + INVALID_METHODS.indexOf(property.node.name) < 0 + ) { + context = global[object.node.name]; + func = context[property.node.name]; + } + + // "abc".charCodeAt(4) + if (object.isLiteral() && property.isIdentifier()) { + const type = typeof object.node.value; + if (type === "string" || type === "number") { + context = object.node.value; + func = context[property.node.name]; + } + } + } + + if (func) { + const args = path.get("arguments").map((arg) => evaluateCached(arg, state)); + if (!state.confident) return; + + return func.apply(context, args); + } + } + + deopt(path, state); +} + /** * Walk the input `node` and statically evaluate it. * @@ -44,331 +365,17 @@ export function evaluateTruthy(): boolean { */ export function evaluate(): { confident: boolean; value: any } { - let confident = true; - let deoptPath: ?NodePath; - const seen = new Map; + const state = { + confident: true, + deoptPath: null, + seen: new Map, + }; + let value = evaluateCached(this, state); + if (!state.confident) value = undefined; - function deopt(path) { - if (!confident) return; - deoptPath = path; - confident = false; - } - - let value = evaluate(this); - if (!confident) value = undefined; return { - confident: confident, - deopt: deoptPath, + confident: state.confident, + deopt: state.deoptPath, value: value, }; - - // we wrap the _evaluate method so we can track `seen` nodes, we push an item - // to the map before we actually evaluate it so we can deopt on self recursive - // nodes such as: - // - // var g = a ? 1 : 2, - // a = g * this.foo - // - function evaluate(path) { - const { node } = path; - - if (seen.has(node)) { - const existing = seen.get(node); - if (existing.resolved) { - return existing.value; - } else { - deopt(path); - return; - } - } else { - const item = { resolved: false }; - seen.set(node, item); - - const val = _evaluate(path); - if (confident) { - item.resolved = true; - item.value = val; - } - return val; - } - } - - function _evaluate(path) { - if (!confident) return; - - const { node } = path; - - if (path.isSequenceExpression()) { - const exprs = path.get("expressions"); - return evaluate(exprs[exprs.length - 1]); - } - - if (path.isStringLiteral() || path.isNumericLiteral() || path.isBooleanLiteral()) { - return node.value; - } - - if (path.isNullLiteral()) { - return null; - } - - if (path.isTemplateLiteral()) { - let str = ""; - - let i = 0; - const exprs = path.get("expressions"); - - for (const elem of (node.quasis: Array)) { - // not confident, evaluated an expression we don't like - if (!confident) break; - - // add on cooked element - str += elem.value.cooked; - - // add on interpolated expression if it's present - const expr = exprs[i++]; - if (expr) str += String(evaluate(expr)); - } - - if (!confident) return; - return str; - } - - if (path.isConditionalExpression()) { - const testResult = evaluate(path.get("test")); - if (!confident) return; - if (testResult) { - return evaluate(path.get("consequent")); - } else { - return evaluate(path.get("alternate")); - } - } - - if (path.isExpressionWrapper()) { // TypeCastExpression, ExpressionStatement etc - return evaluate(path.get("expression")); - } - - // "foo".length - if (path.isMemberExpression() && !path.parentPath.isCallExpression({ callee: node })) { - const property = path.get("property"); - const object = path.get("object"); - - if (object.isLiteral() && property.isIdentifier()) { - const value = object.node.value; - const type = typeof value; - if (type === "number" || type === "string") { - return value[property.node.name]; - } - } - } - - if (path.isReferencedIdentifier()) { - const binding = path.scope.getBinding(node.name); - - if (binding && binding.constantViolations.length > 0) { - return deopt(binding.path); - } - - if (binding && path.node.start < binding.path.node.end) { - return deopt(binding.path); - } - - if (binding && binding.hasValue) { - return binding.value; - } else { - if (node.name === "undefined") { - return binding ? deopt(binding.path) : undefined; - } else if (node.name === "Infinity") { - return binding ? deopt(binding.path) : Infinity; - } else if (node.name === "NaN") { - return binding ? deopt(binding.path) : NaN; - } - - const resolved = path.resolve(); - if (resolved === path) { - return deopt(path); - } else { - return evaluate(resolved); - } - } - } - - if (path.isUnaryExpression({ prefix: true })) { - if (node.operator === "void") { - // we don't need to evaluate the argument to know what this will return - return undefined; - } - - const argument = path.get("argument"); - if (node.operator === "typeof" && (argument.isFunction() || argument.isClass())) { - return "function"; - } - - const arg = evaluate(argument); - if (!confident) return; - switch (node.operator) { - case "!": return !arg; - case "+": return +arg; - case "-": return -arg; - case "~": return ~arg; - case "typeof": return typeof arg; - } - } - - if (path.isArrayExpression()) { - const arr = []; - const elems: Array = path.get("elements"); - for (let elem of elems) { - elem = elem.evaluate(); - - if (elem.confident) { - arr.push(elem.value); - } else { - return deopt(elem); - } - } - return arr; - } - - if (path.isObjectExpression()) { - const obj = {}; - const props: Array = path.get("properties"); - for (const prop of props) { - if (prop.isObjectMethod() || prop.isSpreadElement()) { - return deopt(prop); - } - const keyPath = prop.get("key"); - let key = keyPath; - if (prop.node.computed) { - key = key.evaluate(); - if (!key.confident) { - return deopt(keyPath); - } - key = key.value; - } else if (key.isIdentifier()) { - key = key.node.name; - } else { - key = key.node.value; - } - const valuePath = prop.get("value"); - let value = valuePath.evaluate(); - if (!value.confident) { - return deopt(valuePath); - } - value = value.value; - obj[key] = value; - } - return obj; - } - - if (path.isLogicalExpression()) { - // If we are confident that one side of an && is false, or the left - // side of an || is true, we can be confident about the entire expression - const wasConfident = confident; - const left = evaluate(path.get("left")); - const leftConfident = confident; - confident = wasConfident; - const right = evaluate(path.get("right")); - const rightConfident = confident; - confident = leftConfident && rightConfident; - - switch (node.operator) { - case "||": - // TODO consider having a "truthy type" that doesn't bail on - // left uncertainity but can still evaluate to truthy. - if (left && leftConfident) { - confident = true; - return left; - } - - if (!confident) return; - - return left || right; - case "&&": - if ((!left && leftConfident) || (!right && rightConfident)) { - confident = true; - } - - if (!confident) return; - - return left && right; - } - } - - if (path.isBinaryExpression()) { - const left = evaluate(path.get("left")); - if (!confident) return; - const right = evaluate(path.get("right")); - if (!confident) return; - - switch (node.operator) { - case "-": return left - right; - case "+": return left + right; - case "/": return left / right; - case "*": return left * right; - case "%": return left % right; - case "**": return left ** right; - case "<": return left < right; - case ">": return left > right; - case "<=": return left <= right; - case ">=": return left >= right; - case "==": return left == right; // eslint-disable-line eqeqeq - case "!=": return left != right; - case "===": return left === right; - case "!==": return left !== right; - case "|": return left | right; - case "&": return left & right; - case "^": return left ^ right; - case "<<": return left << right; - case ">>": return left >> right; - case ">>>": return left >>> right; - } - } - - if (path.isCallExpression()) { - const callee = path.get("callee"); - let context; - let func; - - // Number(1); - if ( - callee.isIdentifier() && !path.scope.getBinding(callee.node.name, true) && - VALID_CALLEES.indexOf(callee.node.name) >= 0 - ) { - func = global[node.callee.name]; - } - - if (callee.isMemberExpression()) { - const object = callee.get("object"); - const property = callee.get("property"); - - // Math.min(1, 2) - if ( - object.isIdentifier() && property.isIdentifier() && - VALID_CALLEES.indexOf(object.node.name) >= 0 && - INVALID_METHODS.indexOf(property.node.name) < 0 - ) { - context = global[object.node.name]; - func = context[property.node.name]; - } - - // "abc".charCodeAt(4) - if (object.isLiteral() && property.isIdentifier()) { - const type = typeof object.node.value; - if (type === "string" || type === "number") { - context = object.node.value; - func = context[property.node.name]; - } - } - } - - if (func) { - const args = path.get("arguments").map(evaluate); - if (!confident) return; - - return func.apply(context, args); - } - } - - deopt(path); - } } diff --git a/packages/babel-traverse/src/path/family.js b/packages/babel-traverse/src/path/family.js index de7e12a0c3..612dfd7c1f 100644 --- a/packages/babel-traverse/src/path/family.js +++ b/packages/babel-traverse/src/path/family.js @@ -12,26 +12,27 @@ export function getOpposite() : ?NodePath { } } +function addCompletionRecords(path, paths) { + if (path) return paths.concat(path.getCompletionRecords()); + return paths; +} + export function getCompletionRecords(): Array { let paths = []; - const add = function (path) { - if (path) paths = paths.concat(path.getCompletionRecords()); - }; - if (this.isIfStatement()) { - add(this.get("consequent")); - add(this.get("alternate")); + paths = addCompletionRecords(this.get("consequent"), paths); + paths = addCompletionRecords(this.get("alternate"), paths); } else if (this.isDoExpression() || this.isFor() || this.isWhile()) { - add(this.get("body")); + paths = addCompletionRecords(this.get("body"), paths); } else if (this.isProgram() || this.isBlockStatement()) { - add(this.get("body").pop()); + paths = addCompletionRecords(this.get("body"), paths.pop()); } else if (this.isFunction()) { return this.get("body").getCompletionRecords(); } else if (this.isTryStatement()) { - add(this.get("block")); - add(this.get("handler")); - add(this.get("finalizer")); + paths = addCompletionRecords(this.get("block"), paths); + paths = addCompletionRecords(this.get("handler"), paths); + paths = addCompletionRecords(this.get("finalizer"), paths); } else { paths.push(this); } From ac33b1be2700784fb566fbde5e69f5f5eafa9873 Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Fri, 9 Jun 2017 00:12:16 -0400 Subject: [PATCH 2/2] Fix addCompletionRecords --- packages/babel-traverse/src/path/family.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/babel-traverse/src/path/family.js b/packages/babel-traverse/src/path/family.js index 612dfd7c1f..e2ab999829 100644 --- a/packages/babel-traverse/src/path/family.js +++ b/packages/babel-traverse/src/path/family.js @@ -26,7 +26,7 @@ export function getCompletionRecords(): Array { } else if (this.isDoExpression() || this.isFor() || this.isWhile()) { paths = addCompletionRecords(this.get("body"), paths); } else if (this.isProgram() || this.isBlockStatement()) { - paths = addCompletionRecords(this.get("body"), paths.pop()); + paths = addCompletionRecords(this.get("body").pop(), paths); } else if (this.isFunction()) { return this.get("body").getCompletionRecords(); } else if (this.isTryStatement()) {