lively.ast
Version:
Parsing JS code into ASTs and tools to query and transform these trees.
353 lines (301 loc) • 12.7 kB
JavaScript
/*global process, global, exports*/
import { arr, string, chain, Path } from "lively.lang";
import { helpers, scopes, topLevelDeclsAndRefs, topLevelFuncDecls } from "./query.js";
import { parse, fuzzyParse } from "./parser.js";
import objectSpreadTransform from "./object-spread-transform.js";
import {
varDecl,
memberChain,
id,
exprStmt,
funcCall,
returnStmt,
tryStmt,
program,
block,
funcExpr
} from "./nodes.js";
import stringify from "./stringify.js";
var helper = {
// currently this is used by the replacement functions below but
// I don't wan't to make it part of our AST API
_node2string: function(node) {
return node.source || stringify(node)
},
_findIndentAt: function(s, pos) {
var bol = string.peekLeft(s, pos, /\s+$/),
indent = typeof bol === 'number' ? s.slice(bol, pos) : '';
if (indent[0] === '\n') indent = indent.slice(1);
return indent;
},
_applyChanges: function(changes, source) {
return changes.reduce(function(source, change) {
if (change.type === 'del') {
return source.slice(0, change.pos) + source.slice(change.pos + change.length);
} else if (change.type === 'add') {
return source.slice(0, change.pos) + change.string + source.slice(change.pos);
}
throw new Error('Uexpected change ' + Objects.inspect(change));
}, source);
},
_compareNodesForReplacement: function(nodeA, nodeB) {
// equals
if (nodeA.start === nodeB.start && nodeA.end === nodeB.end) return 0;
// a "left" of b
if (nodeA.end <= nodeB.start) return -1;
// a "right" of b
if (nodeA.start >= nodeB.end) return 1;
// a contains b
if (nodeA.start <= nodeB.start && nodeA.end >= nodeB.end) return 1;
// b contains a
if (nodeB.start <= nodeA.start && nodeB.end >= nodeA.end) return -1;
throw new Error('Comparing nodes');
},
replaceNode: function(target, replacementFunc, sourceOrChanges) {
// parameters:
// - target: ast node
// - replacementFunc that gets this node and its source snippet
// handed and should produce a new ast node.
// - sourceOrChanges: If its a string -- the source code to rewrite
// If its and object -- {changes: ARRAY, source: STRING}
var sourceChanges = typeof sourceOrChanges === 'object' ?
sourceOrChanges : {changes: [], source: sourceOrChanges},
insideChangedBefore = false,
pos = sourceChanges.changes.reduce(function(pos, change) {
// fixup the start and end indices of target using the del/add
// changes already applied
if (pos.end < change.pos) return pos;
var isInFront = change.pos < pos.start;
insideChangedBefore = insideChangedBefore
|| change.pos >= pos.start && change.pos <= pos.end;
if (change.type === 'add') return {
start: isInFront ? pos.start + change.string.length : pos.start,
end: pos.end + change.string.length
};
if (change.type === 'del') return {
start: isInFront ? pos.start - change.length : pos.start,
end: pos.end - change.length
};
throw new Error('Cannot deal with change ' + Objects.inspect(change));
}, {start: target.start, end: target.end});
var source = sourceChanges.source,
replacement = replacementFunc(target, source.slice(pos.start, pos.end), insideChangedBefore),
replacementSource = Array.isArray(replacement) ?
replacement.map(helper._node2string).join('\n' + helper._findIndentAt(source, pos.start)):
replacementSource = helper._node2string(replacement);
var changes = [{type: 'del', pos: pos.start, length: pos.end - pos.start},
{type: 'add', pos: pos.start, string: replacementSource}];
return {
changes: sourceChanges.changes.concat(changes),
source: this._applyChanges(changes, source)
};
},
replaceNodes: function(targetAndReplacementFuncs, sourceOrChanges) {
// replace multiple AST nodes, order rewriting from inside out and
// top to bottom so that nodes to rewrite can overlap or be contained
// in each other
return targetAndReplacementFuncs.sort(function(a, b) {
return helper._compareNodesForReplacement(a.target, b.target);
}).reduce(function(sourceChanges, ea) {
return helper.replaceNode(ea.target, ea.replacementFunc, sourceChanges);
}, typeof sourceOrChanges === 'object' ?
sourceOrChanges : {changes: [], source: sourceOrChanges});
}
}
function replace(astOrSource, targetNode, replacementFunc, options) {
// replaces targetNode in astOrSource with what replacementFunc returns
// (one or multiple ast nodes)
// Example:
// var ast = exports.parse('foo.bar("hello");')
// exports.transform.replace(
// ast, ast.body[0].expression,
// function(node, source) {
// return {type: "CallExpression",
// callee: {name: node.arguments[0].value, type: "Identifier"},
// arguments: [{value: "world", type: "Literal"}]
// }
// });
// => {
// source: "hello('world');",
// changes: [{pos: 0,length: 16,type: "del"},{pos: 0,string: "hello('world')",type: "add"}]
// }
var parsed = typeof astOrSource === 'object' ? astOrSource : null,
source = typeof astOrSource === 'string' ?
astOrSource : (parsed.source || helper._node2string(parsed)),
result = helper.replaceNode(targetNode, replacementFunc, source);
return result;
}
function oneDeclaratorPerVarDecl(astOrSource) {
// exports.transform.oneDeclaratorPerVarDecl(
// "var x = 3, y = (function() { var y = 3, x = 2; })(); ").source
var parsed = typeof astOrSource === 'object' ?
astOrSource : parse(astOrSource),
source = typeof astOrSource === 'string' ?
astOrSource : (parsed.source || helper._node2string(parsed)),
scope = scopes(parsed),
varDecls = (function findVarDecls(scope) {
return arr.flatten(scope.varDecls.concat(scope.subScopes.map(findVarDecls)));
})(scope);
var targetsAndReplacements = varDecls.map(function(decl) {
return {
target: decl,
replacementFunc: function(declNode, s, wasChanged) {
if (wasChanged) {
// reparse node if necessary, e.g. if init was changed before like in
// var x = (function() { var y = ... })();
declNode = parse(s).body[0];
}
return declNode.declarations.map(function(ea) {
return {
type: "VariableDeclaration",
kind: "var", declarations: [ea]
}
});
}
}
});
return helper.replaceNodes(targetsAndReplacements, source);
}
function oneDeclaratorForVarsInDestructoring(astOrSource) {
var parsed = typeof astOrSource === 'object' ?
astOrSource : parse(astOrSource),
source = typeof astOrSource === 'string' ?
astOrSource : (parsed.source || helper._node2string(parsed)),
scope = scopes(parsed),
varDecls = (function findVarDecls(scope) {
return arr.flatten(scope.varDecls
.concat(scope.subScopes.map(findVarDecls)));
})(scope);
var targetsAndReplacements = varDecls.map(function(decl) {
return {
target: decl,
replacementFunc: function(declNode, s, wasChanged) {
if (wasChanged) {
// reparse node if necessary, e.g. if init was changed before like in
// var x = (function() { var y = ... })();
declNode = parse(s).body[0];
}
return arr.flatmap(declNode.declarations, function(declNode) {
var extractedId = {type: "Identifier", name: "__temp"},
extractedInit = {
type: "VariableDeclaration", kind: "var",
declarations: [{type: "VariableDeclarator", id: extractedId, init: declNode.init}]
}
var propDecls = helpers.objPropertiesAsList(declNode.id, [], false).map(ea => ea.key)
.map(keyPath => varDecl(arr.last(keyPath), memberChain(extractedId.name, ...keyPath), "var"));
return [extractedInit].concat(propDecls);
});
}
}
});
return helper.replaceNodes(targetsAndReplacements, source);
}
function returnLastStatement(source, opts) {
opts = opts || {};
var parsed = parse(source, opts),
last = arr.last(parsed.body);
if (last.type === "ExpressionStatement") {
parsed.body.splice(
parsed.body.length-1, 1,
returnStmt(last.expression))
return opts.asAST ? parsed : stringify(parsed);
} else {
return opts.asAST ? parsed : source;
}
}
function wrapInFunction(code, opts) {
opts = opts || {};
var transformed = returnLastStatement(code, opts);
return opts.asAST ?
program(funcExpr({id: opts.id || undefined}, [], ...transformed.body)) :
`function${opts.id ? " " + opts.id : ""}() {\n${transformed}\n}`;
}
function wrapInStartEndCall(parsed, options) {
// Wraps a piece of code into two function calls: One before the first
// statement and one after the last. Also wraps the entire thing into a try /
// catch block. The end call gets the result of the last statement (if it is
// something that returns a value, i.e. an expression) passed as the second
// argument. If an error occurs the end function is called with an error as
// first parameter
// Why? This allows to easily track execution of code, especially for
// asynchronus / await code!
// Example:
// stringify(wrapInStartEndCall("var y = x + 23; y"))
// // generates code
// try {
// __start_execution();
// __lvVarRecorder.y = x + 23;
// return __end_execution(null, __lvVarRecorder.y);
// } catch (err) {
// return __end_execution(err, undefined);
// }
if (typeof parsed === "string") parsed = parse(parsed);
options = options || {};
var isProgram = parsed.type === "Program",
startFuncNode = options.startFuncNode || id("__start_execution"),
endFuncNode = options.endFuncNode || id("__end_execution"),
funcDecls = topLevelFuncDecls(parsed),
innerBody = parsed.body,
outerBody = [];
// 1. Hoist func decls outside the actual eval start - end code. The async /
// generator transforms require this!
funcDecls.forEach(({node, path}) => {
Path(path).set(parsed, exprStmt(node.id));
outerBody.push(node);
});
// 2. add start-eval call
innerBody.unshift(exprStmt(funcCall(startFuncNode)));
// 3. if last statement is an expression, transform it so we can pass it to
// the end-eval call, replacing the original expression. If it's a
// non-expression we record undefined as the eval result
var last = arr.last(innerBody);
if (last.type === "ExpressionStatement") {
innerBody.pop();
innerBody.push(exprStmt(funcCall(endFuncNode, id("null"), last.expression)));
} else if (last.type === "VariableDeclaration" && arr.last(last.declarations).id.type === "Identifier") {
innerBody.push(exprStmt(funcCall(endFuncNode, id("null"), arr.last(last.declarations).id)));
} else {
innerBody.push(exprStmt(funcCall(endFuncNode, id("null"), id("undefined"))));
}
// 4. Wrap that stuff in a try stmt
outerBody.push(
tryStmt("err",
[exprStmt(funcCall(endFuncNode, id("err"), id("undefined")))],
...innerBody));
return isProgram ? program(...outerBody) : block(...outerBody);
}
const isProbablySingleExpressionRe = /^\s*(\{|function\s*\()/;
function transformSingleExpression(code) {
// evaling certain expressions such as single functions or object
// literals will fail or not work as intended. When the code being
// evaluated consists just out of a single expression we will wrap it in
// parens to allow for those cases
// Example:
// transformSingleExpression("{foo: 23}") // => "({foo: 23})"
if (!isProbablySingleExpressionRe.test(code) || code.split("\n").length > 30) return code;
try {
var parsed = fuzzyParse(code);
if (parsed.body.length === 1 &&
(parsed.body[0].type === 'FunctionDeclaration'
|| (parsed.body[0].type === 'BlockStatement'
&& parsed.body[0].body[0].type === 'LabeledStatement'))) {
code = '(' + code.replace(/;\s*$/, '') + ')';
}
} catch(e) {
if (typeof lively && lively.Config && lively.Config.showImprovedJavaScriptEvalErrors) $world.logError(e)
else console.error("Eval preprocess error: %s", e.stack || e);
}
return code;
}
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
export {
helper,
replace,
oneDeclaratorPerVarDecl,
oneDeclaratorForVarsInDestructoring,
returnLastStatement,
wrapInFunction,
wrapInStartEndCall,
transformSingleExpression,
objectSpreadTransform
};