You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
409 lines
9.1 KiB
409 lines
9.1 KiB
"use strict"; |
|
|
|
Object.defineProperty(exports, "__esModule", { |
|
value: true |
|
}); |
|
exports.evaluate = evaluate; |
|
exports.evaluateTruthy = evaluateTruthy; |
|
const VALID_CALLEES = ["String", "Number", "Math"]; |
|
const INVALID_METHODS = ["random"]; |
|
|
|
function isValidCallee(val) { |
|
return VALID_CALLEES.includes(val); |
|
} |
|
|
|
function isInvalidMethod(val) { |
|
return INVALID_METHODS.includes(val); |
|
} |
|
|
|
function evaluateTruthy() { |
|
const res = this.evaluate(); |
|
if (res.confident) return !!res.value; |
|
} |
|
|
|
function deopt(path, state) { |
|
if (!state.confident) return; |
|
state.deoptPath = path; |
|
state.confident = false; |
|
} |
|
|
|
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; |
|
|
|
if (path.isSequenceExpression()) { |
|
const exprs = path.get("expressions"); |
|
return evaluateCached(exprs[exprs.length - 1], state); |
|
} |
|
|
|
if (path.isStringLiteral() || path.isNumericLiteral() || path.isBooleanLiteral()) { |
|
return path.node.value; |
|
} |
|
|
|
if (path.isNullLiteral()) { |
|
return null; |
|
} |
|
|
|
if (path.isTemplateLiteral()) { |
|
return evaluateQuasis(path, path.node.quasis, state); |
|
} |
|
|
|
if (path.isTaggedTemplateExpression() && path.get("tag").isMemberExpression()) { |
|
const object = path.get("tag.object"); |
|
const { |
|
node: { |
|
name |
|
} |
|
} = object; |
|
const property = path.get("tag.property"); |
|
|
|
if (object.isIdentifier() && name === "String" && !path.scope.getBinding(name) && property.isIdentifier() && property.node.name === "raw") { |
|
return evaluateQuasis(path, path.node.quasi.quasis, state, true); |
|
} |
|
} |
|
|
|
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()) { |
|
return evaluateCached(path.get("expression"), state); |
|
} |
|
|
|
if (path.isMemberExpression() && !path.parentPath.isCallExpression({ |
|
callee: path.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(path.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 != null && binding.hasValue) { |
|
return binding.value; |
|
} else { |
|
if (path.node.name === "undefined") { |
|
return binding ? deopt(binding.path, state) : undefined; |
|
} else if (path.node.name === "Infinity") { |
|
return binding ? deopt(binding.path, state) : Infinity; |
|
} else if (path.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 (path.node.operator === "void") { |
|
return undefined; |
|
} |
|
|
|
const argument = path.get("argument"); |
|
|
|
if (path.node.operator === "typeof" && (argument.isFunction() || argument.isClass())) { |
|
return "function"; |
|
} |
|
|
|
const arg = evaluateCached(argument, state); |
|
if (!state.confident) return; |
|
|
|
switch (path.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 = path.get("elements"); |
|
|
|
for (const elem of elems) { |
|
const elemValue = elem.evaluate(); |
|
|
|
if (elemValue.confident) { |
|
arr.push(elemValue.value); |
|
} else { |
|
return deopt(elemValue.deopt, state); |
|
} |
|
} |
|
|
|
return arr; |
|
} |
|
|
|
if (path.isObjectExpression()) { |
|
const obj = {}; |
|
const props = 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(key.deopt, 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(value.deopt, state); |
|
} |
|
|
|
value = value.value; |
|
obj[key] = value; |
|
} |
|
|
|
return obj; |
|
} |
|
|
|
if (path.isLogicalExpression()) { |
|
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; |
|
|
|
switch (path.node.operator) { |
|
case "||": |
|
state.confident = leftConfident && (!!left || rightConfident); |
|
if (!state.confident) return; |
|
return left || right; |
|
|
|
case "&&": |
|
state.confident = leftConfident && (!left || rightConfident); |
|
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 (path.node.operator) { |
|
case "-": |
|
return left - right; |
|
|
|
case "+": |
|
return left + right; |
|
|
|
case "/": |
|
return left / right; |
|
|
|
case "*": |
|
return left * right; |
|
|
|
case "%": |
|
return left % right; |
|
|
|
case "**": |
|
return Math.pow(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; |
|
|
|
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; |
|
|
|
if (callee.isIdentifier() && !path.scope.getBinding(callee.node.name) && isValidCallee(callee.node.name)) { |
|
func = global[callee.node.name]; |
|
} |
|
|
|
if (callee.isMemberExpression()) { |
|
const object = callee.get("object"); |
|
const property = callee.get("property"); |
|
|
|
if (object.isIdentifier() && property.isIdentifier() && isValidCallee(object.node.name) && !isInvalidMethod(property.node.name)) { |
|
context = global[object.node.name]; |
|
func = context[property.node.name]; |
|
} |
|
|
|
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); |
|
} |
|
|
|
function evaluateQuasis(path, quasis, state, raw = false) { |
|
let str = ""; |
|
let i = 0; |
|
const exprs = path.get("expressions"); |
|
|
|
for (const elem of quasis) { |
|
if (!state.confident) break; |
|
str += raw ? elem.value.raw : elem.value.cooked; |
|
const expr = exprs[i++]; |
|
if (expr) str += String(evaluateCached(expr, state)); |
|
} |
|
|
|
if (!state.confident) return; |
|
return str; |
|
} |
|
|
|
function evaluate() { |
|
const state = { |
|
confident: true, |
|
deoptPath: null, |
|
seen: new Map() |
|
}; |
|
let value = evaluateCached(this, state); |
|
if (!state.confident) value = undefined; |
|
return { |
|
confident: state.confident, |
|
deopt: state.deoptPath, |
|
value: value |
|
}; |
|
} |