lively.ast
Version:
Parsing JS code into ASTs and tools to query and transform these trees.
563 lines (540 loc) • 19 kB
JavaScript
/*global acorn*/
import { obj, arr, string, Path } from "lively.lang";
import { withMozillaAstDo } from "./mozilla-ast-visitor-interface.js";
var GLOBAL = typeof window!=="undefined" ?
window : typeof global!=="undefined" ?
global : typeof self!=="undefined" ?
self : this;
import "acorn";
export { acorn }
var walk = acorn.walk;
var loose = acorn.loose;
export { walk, loose }
// rk 2016-05-17 FIXME: the current version of acorn.walk doesn't support async
// await. We patch the walker here until it does
if (!walk.base.AwaitExpression) {
walk.base.AwaitExpression = function (node, st, c) {
if (node.argument)
c(node.argument, st, 'Expression');
}
}
// FIXME, don't add to walk object, that's our own stuff!
walk.forEachNode = forEachNode;
walk.matchNodes = matchNodes;
walk.findNodesIncluding = findNodesIncluding;
walk.withParentInfo = withParentInfo;
walk.copy = copy;
walk.findSiblings = findSiblings;
walk.findNodeByAstIndex = findNodeByAstIndex;
walk.findStatementOfNode = findStatementOfNode;
walk.addAstIndex = addAstIndex;
export {
findStatementOfNode,
addAstIndex,
findNodesIncluding,
findSiblings,
findNodeByAstIndex,
copy
}
// -=-=-=-=-=-=-=-=-=-=-=-
// from lively.ast.acorn
// -=-=-=-=-=-=-=-=-=-=-=-
function forEachNode(parsed, func, state, options) {
// note: func can get called with the same node for different
// visitor callbacks!
// func args: node, state, depth, type
options = options || {};
var traversal = options.traversal || 'preorder'; // also: postorder
var visitors = obj.clone(options.visitors ? options.visitors : walk.make(walk.visitors.withMemberExpression));
var iterator = traversal === 'preorder' ?
function(orig, type, node, depth, cont) { func(node, state, depth, type); return orig(node, depth+1, cont); } :
function(orig, type, node, depth, cont) { var result = orig(node, depth+1, cont); func(node, state, depth, type); return result; };
Object.keys(visitors).forEach(function(type) {
var orig = visitors[type];
visitors[type] = function(node, depth, cont) { return iterator(orig, type, node, depth, cont); };
});
walk.recursive(parsed, 0, null, visitors);
return parsed;
};
function matchNodes(parsed, visitor, state, options) {
function visit(node, state, depth, type) {
if (visitor[node.type]) visitor[node.type](node, state, depth, type);
}
return forEachNode(parsed, visit, state, options);
};
// findNodesIncluding: function(ast, pos, test, base) {
// var nodes = [];
// base = base || acorn.walk.make({});
// Object.keys(base).forEach(function(name) {
// var orig = base[name];
// base[name] = function(node, state, cont) {
// nodes.pushIfNotIncluded(node);
// return orig(node, state, cont);
// }
// });
// acorn.walk.findNodeAround(ast, pos, test, base);
// return nodes;
// }
function findNodesIncluding(parsed, pos, test, base) {
var nodes = [];
base = base || walk.make(walk.visitors.withMemberExpression);
Object.keys(walk.base).forEach(function (name) {
var orig = base[name];
base[name] = function(node, state, cont) {
arr.pushIfNotIncluded(nodes, node);
return orig(node, state, cont);
}
});
base["Property"] = function (node, st, c) {
arr.pushIfNotIncluded(nodes, node);
c(node.key, st, "Expression");
c(node.value, st, "Expression");
}
base["LabeledStatement"] = function (node, st, c) {
node.label && c(node.label, st, "Expression");
c(node.body, st, "Statement");
}
walk.findNodeAround(parsed, pos, test, base);
return nodes;
};
function withParentInfo(parsed, iterator, options) {
// options = {visitAllNodes: BOOL}
options = options || {};
function makeScope(parentScope) {
var scope = {id: string.newUUID(), parentScope: parentScope, containingScopes: []};
parentScope && parentScope.containingScopes.push(scope);
return scope;
}
var visitors = walk.make({
Function: function(node, st, c) {
if (st && st.scope) st.scope = makeScope(st.scope);
c(node.body, st, "ScopeBody");
},
VariableDeclarator: function(node, st, c) {
// node.id && c(node.id, st, 'Identifier');
node.init && c(node.init, st, 'Expression');
},
VariableDeclaration: function(node, st, c) {
for (var i = 0; i < node.declarations.length; ++i) {
var decl = node.declarations[i];
if (decl) c(decl, st, "VariableDeclarator");
}
},
ObjectExpression: function(node, st, c) {
for (var i = 0; i < node.properties.length; ++i) {
var prop = node.properties[i];
c(prop.key, st, "Expression");
c(prop.value, st, "Expression");
}
},
MemberExpression: function(node, st, c) {
c(node.object, st, "Expression");
c(node.property, st, "Expression");
}
}, walk.base);
var lastActiveProp, getters = [];
forEachNode(parsed, function(node) {
arr.withoutAll(Object.keys(node), ['end', 'start', 'type', 'source', 'raw']).forEach(function(propName) {
if (node.__lookupGetter__(propName)) return; // already defined
var val = node[propName];
node.__defineGetter__(propName, function() { lastActiveProp = propName; return val; });
getters.push([node, propName, node[propName]]);
});
}, null, {visitors: visitors});
var result = [];
Object.keys(visitors).forEach(function(type) {
var orig = visitors[type];
visitors[type] = function(node, state, cont) {
if (type === node.type || options.visitAllNodes) {
result.push(iterator.call(null, node, {scope: state.scope, depth: state.depth, parent: state.parent, type: type, propertyInParent: lastActiveProp}));
return orig(node, {scope: state.scope, parent: node, depth: state.depth+1}, cont);
} else {
return orig(node, state, cont);
}
}
});
walk.recursive(parsed, {scope: makeScope(), parent: null, propertyInParent: '', depth: 0}, null, visitors);
getters.forEach(function(nodeNameVal) {
delete nodeNameVal[0][nodeNameVal[1]];
nodeNameVal[0][nodeNameVal[1]] = nodeNameVal[2];
});
return result;
};
function copy(ast, override) {
var visitors = obj.extend({
Program: function(n, c) {
return {
start: n.start, end: n.end, type: 'Program',
body: n.body.map(c),
source: n.source, astIndex: n.astIndex
};
},
FunctionDeclaration: function(n, c) {
return {
start: n.start, end: n.end, type: 'FunctionDeclaration',
id: c(n.id), params: n.params.map(c), body: c(n.body),
source: n.source, astIndex: n.astIndex
};
},
BlockStatement: function(n, c) {
return {
start: n.start, end: n.end, type: 'BlockStatement',
body: n.body.map(c),
source: n.source, astIndex: n.astIndex
};
},
ExpressionStatement: function(n, c) {
return {
start: n.start, end: n.end, type: 'ExpressionStatement',
expression: c(n.expression),
source: n.source, astIndex: n.astIndex
};
},
CallExpression: function(n, c) {
return {
start: n.start, end: n.end, type: 'CallExpression',
callee: c(n.callee), arguments: n.arguments.map(c),
source: n.source, astIndex: n.astIndex
};
},
MemberExpression: function(n, c) {
return {
start: n.start, end: n.end, type: 'MemberExpression',
object: c(n.object), property: c(n.property), computed: n.computed,
source: n.source, astIndex: n.astIndex
};
},
NewExpression: function(n, c) {
return {
start: n.start, end: n.end, type: 'NewExpression',
callee: c(n.callee), arguments: n.arguments.map(c),
source: n.source, astIndex: n.astIndex
};
},
VariableDeclaration: function(n, c) {
return {
start: n.start, end: n.end, type: 'VariableDeclaration',
declarations: n.declarations.map(c), kind: n.kind,
source: n.source, astIndex: n.astIndex
};
},
VariableDeclarator: function(n, c) {
return {
start: n.start, end: n.end, type: 'VariableDeclarator',
id: c(n.id), init: c(n.init),
source: n.source, astIndex: n.astIndex
};
},
FunctionExpression: function(n, c) {
return {
start: n.start, end: n.end, type: 'FunctionExpression',
id: c(n.id), params: n.params.map(c), body: c(n.body),
source: n.source, astIndex: n.astIndex
};
},
IfStatement: function(n, c) {
return {
start: n.start, end: n.end, type: 'IfStatement',
test: c(n.test), consequent: c(n.consequent),
alternate: c(n.alternate),
source: n.source, astIndex: n.astIndex
};
},
ConditionalExpression: function(n, c) {
return {
start: n.start, end: n.end, type: 'ConditionalExpression',
test: c(n.test), consequent: c(n.consequent),
alternate: c(n.alternate),
source: n.source, astIndex: n.astIndex
};
},
SwitchStatement: function(n, c) {
return {
start: n.start, end: n.end, type: 'SwitchStatement',
discriminant: c(n.discriminant), cases: n.cases.map(c),
source: n.source, astIndex: n.astIndex
};
},
SwitchCase: function(n, c) {
return {
start: n.start, end: n.end, type: 'SwitchCase',
test: c(n.test), consequent: n.consequent.map(c),
source: n.source, astIndex: n.astIndex
};
},
BreakStatement: function(n, c) {
return {
start: n.start, end: n.end, type: 'BreakStatement',
label: n.label,
source: n.source, astIndex: n.astIndex
};
},
ContinueStatement: function(n, c) {
return {
start: n.start, end: n.end, type: 'ContinueStatement',
label: n.label,
source: n.source, astIndex: n.astIndex
};
},
TryStatement: function(n, c) {
return {
start: n.start, end: n.end, type: 'TryStatement',
block: c(n.block), handler: c(n.handler), finalizer: c(n.finalizer),
guardedHandlers: n.guardedHandlers.map(c),
source: n.source, astIndex: n.astIndex
};
},
CatchClause: function(n, c) {
return {
start: n.start, end: n.end, type: 'CatchClause',
param: c(n.param), guard: c(n.guard), body: c(n.body),
source: n.source, astIndex: n.astIndex
};
},
ThrowStatement: function(n, c) {
return {
start: n.start, end: n.end, type: 'ThrowStatement',
argument: c(n.argument),
source: n.source, astIndex: n.astIndex
};
},
ForStatement: function(n, c) {
return {
start: n.start, end: n.end, type: 'ForStatement',
init: c(n.init), test: c(n.test), update: c(n.update),
body: c(n.body),
source: n.source, astIndex: n.astIndex
};
},
ForInStatement: function(n, c) {
return {
start: n.start, end: n.end, type: 'ForInStatement',
left: c(n.left), right: c(n.right), body: c(n.body),
source: n.source, astIndex: n.astIndex
};
},
WhileStatement: function(n, c) {
return {
start: n.start, end: n.end, type: 'WhileStatement',
test: c(n.test), body: c(n.body),
source: n.source, astIndex: n.astIndex
};
},
DoWhileStatement: function(n, c) {
return {
start: n.start, end: n.end, type: 'DoWhileStatement',
test: c(n.test), body: c(n.body),
source: n.source, astIndex: n.astIndex
};
},
WithStatement: function(n ,c) {
return {
start: n.start, end: n.end, type: 'WithStatement',
object: c(n.object), body: c(n.body),
source: n.source, astIndex: n.astIndex
};
},
UnaryExpression: function(n, c) {
return {
start: n.start, end: n.end, type: 'UnaryExpression',
argument: c(n.argument), operator: n.operator, prefix: n.prefix,
source: n.source, astIndex: n.astIndex
};
},
BinaryExpression: function(n, c) {
return {
start: n.start, end: n.end, type: 'BinaryExpression',
left: c(n.left), operator: n.operator, right: c(n.right),
source: n.source, astIndex: n.astIndex
};
},
LogicalExpression: function(n, c) {
return {
start: n.start, end: n.end, type: 'LogicalExpression',
left: c(n.left), operator: n.operator, right: c(n.right),
source: n.source, astIndex: n.astIndex
};
},
AssignmentExpression: function(n, c) {
return {
start: n.start, end: n.end, type: 'AssignmentExpression',
left: c(n.left), operator: n.operator, right: c(n.right),
source: n.source, astIndex: n.astIndex
};
},
UpdateExpression: function(n, c) {
return {
start: n.start, end: n.end, type: 'UpdateExpression',
argument: c(n.argument), operator: n.operator, prefix: n.prefix,
source: n.source, astIndex: n.astIndex
};
},
ReturnStatement: function(n, c) {
return {
start: n.start, end: n.end, type: 'ReturnStatement',
argument: c(n.argument),
source: n.source, astIndex: n.astIndex
};
},
Identifier: function(n, c) {
return {
start: n.start, end: n.end, type: 'Identifier',
name: n.name,
source: n.source, astIndex: n.astIndex
};
},
Literal: function(n, c) {
return {
start: n.start, end: n.end, type: 'Literal',
value: n.value, raw: n.raw /* Acorn-specific */,
source: n.source, astIndex: n.astIndex
};
},
ObjectExpression: function(n, c) {
return {
start: n.start, end: n.end, type: 'ObjectExpression',
properties: n.properties.map(function(prop) {
return {
key: c(prop.key), value: c(prop.value), kind: prop.kind
};
}),
source: n.source, astIndex: n.astIndex
};
},
ArrayExpression: function(n, c) {
return {
start: n.start, end: n.end, type: 'ArrayExpression',
elements: n.elements.map(c),
source: n.source, astIndex: n.astIndex
};
},
SequenceExpression: function(n, c) {
return {
start: n.start, end: n.end, type: 'SequenceExpression',
expressions: n.expressions.map(c),
source: n.source, astIndex: n.astIndex
};
},
EmptyStatement: function(n, c) {
return {
start: n.start, end: n.end, type: 'EmptyStatement',
source: n.source, astIndex: n.astIndex
};
},
ThisExpression: function(n, c) {
return {
start: n.start, end: n.end, type: 'ThisExpression',
source: n.source, astIndex: n.astIndex
};
},
DebuggerStatement: function(n, c) {
return {
start: n.start, end: n.end, type: 'DebuggerStatement',
source: n.source, astIndex: n.astIndex
};
},
LabeledStatement: function(n, c) {
return {
start: n.start, end: n.end, type: 'LabeledStatement',
label: n.label, body: c(n.body),
source: n.source, astIndex: n.astIndex
};
}
}, override || {});
function c(node) {
if (node === null) return null;
return visitors[node.type](node, c);
}
return c(ast);
}
function findSiblings(parsed, node, beforeOrAfter) {
if (!node) return [];
var nodes = findNodesIncluding(parsed, node.start),
idx = nodes.indexOf(node),
parents = nodes.slice(0, idx),
parentWithBody = arr.detect(parents.reverse(), function(p) { return Array.isArray(p.body); }),
siblingsWithNode = parentWithBody.body;
if (!beforeOrAfter) return arr.without(siblingsWithNode, node);
var nodeIdxInSiblings = siblingsWithNode.indexOf(node);
return beforeOrAfter === 'before' ?
siblingsWithNode.slice(0, nodeIdxInSiblings) :
siblingsWithNode.slice(nodeIdxInSiblings + 1);
}
// // cached visitors that are used often
walk.visitors = {
stopAtFunctions: walk.make({
'Function': function() { /* stop descent */ }
}, walk.base),
withMemberExpression: walk.make({
MemberExpression: function(node, st, c) {
c(node.object, st, "Expression");
c(node.property, st, "Expression");
}
}, walk.base)
}
// -=-=-=-=-=-=-=-=-=-=-=-=-=-
// from lively.ast.AstHelper
// -=-=-=-=-=-=-=-=-=-=-=-=-=-
function findNodeByAstIndex(parsed, astIndexToFind, addIndex) {
addIndex = addIndex == null ? true : !!addIndex;
if (!parsed.astIndex && addIndex) addAstIndex(parsed);
// we need to visit every node, forEachNode is highly
// inefficient, the compilled Mozilla visitors are a better fit
var found = null;
withMozillaAstDo(parsed, null, function(next, node, state) {
if (found) return;
var idx = node.astIndex;
if (idx < astIndexToFind) return;
if (node.astIndex === astIndexToFind) { found = node; return; }
next();
});
return found;
};
// FIXME: global (and temporary) findNodeByAstIndex is used by __getClosure and defined in Rewriting.js
// Global.findNodeByAstIndex = findNodeByAstIndex;
function findStatementOfNode(options, parsed, target) {
// DEPRECATED in favor of query.statementOf(parsed, node)
// Can also be called with just ast and target. options can be {asPath: BOOLEAN}.
// Find the statement that a target node is in. Example:
// let source be "var x = 1; x + 1;" and we are looking for the
// Identifier "x" in "x+1;". The second statement is what will be found.
if (!target) { target = parsed; parsed = options; options = null }
if (!options) options = {}
if (!parsed.astIndex) addAstIndex(parsed);
var found, targetReached = false;
var statements = [
// ES5
'EmptyStatement', 'BlockStatement', 'ExpressionStatement', 'IfStatement',
'LabeledStatement', 'BreakStatement', 'ContinueStatement', 'WithStatement', 'SwitchStatement',
'ReturnStatement', 'ThrowStatement', 'TryStatement', 'WhileStatement', 'DoWhileStatement',
'ForStatement', 'ForInStatement', 'DebuggerStatement', 'FunctionDeclaration',
'VariableDeclaration',
// ES2015:
'ClassDeclaration'
];
withMozillaAstDo(parsed, {}, function(next, node, state, path) {
if (targetReached || node.astIndex < target.astIndex) return;
if (node === target || node.astIndex === target.astIndex) {
targetReached = true;
if (options.asPath)
found = path;
else {
var p = Path(path);
do {
found = p.get(parsed);
p = p.slice(0, p.size() - 1);
} while ((statements.indexOf(found.type) == -1) && (p.size() > 0));
}
}
!targetReached && next();
});
return found;
};
function addAstIndex(parsed) {
// we need to visit every node, forEachNode is highly
// inefficient, the compilled Mozilla visitors are a better fit
withMozillaAstDo(parsed, {index: 0}, function(next, node, state) {
next(); node.astIndex = state.index++;
});
return parsed;
};