131 lines
3.1 KiB
JavaScript
131 lines
3.1 KiB
JavaScript
const parser = require('@babel/parser');
|
|
|
|
/**
|
|
* @param {Object} options - Options to configure parser
|
|
* @param {Object} options.parser - An object with a parse method that returns an AST
|
|
*/
|
|
module.exports = function(options = {}) {
|
|
this.parser = options.parser || parser;
|
|
|
|
if (options.parser) {
|
|
// We don't want to send that down to the actual parser
|
|
delete options.parser;
|
|
}
|
|
|
|
this.options = Object.assign({
|
|
plugins: [
|
|
'jsx',
|
|
'flow',
|
|
'doExpressions',
|
|
'objectRestSpread',
|
|
['decorators', {decoratorsBeforeExport: true}],
|
|
'classProperties',
|
|
'exportDefaultFrom',
|
|
'exportNamespaceFrom',
|
|
'asyncGenerators',
|
|
'functionBind',
|
|
'functionSent',
|
|
'dynamicImport',
|
|
'optionalChaining',
|
|
'nullishCoalescingOperator'
|
|
],
|
|
allowHashBang: true,
|
|
sourceType: 'module'
|
|
}, options);
|
|
|
|
// We use global state to stop the recursive traversal of the AST
|
|
this.shouldStop = false;
|
|
};
|
|
|
|
/**
|
|
* @param {String} src
|
|
* @param {Object} [options] - Parser options
|
|
* @return {Object} The AST of the given src
|
|
*/
|
|
module.exports.prototype.parse = function(src, options) {
|
|
options = options || this.options;
|
|
|
|
// Keep around for consumers of parse that supply their own options
|
|
if (typeof options.allowHashBang === 'undefined') {
|
|
options.allowHashBang = true;
|
|
}
|
|
|
|
return this.parser.parse(src, options);
|
|
};
|
|
|
|
/**
|
|
* Adapted from substack/node-detective
|
|
* Executes cb on a non-array AST node
|
|
*/
|
|
module.exports.prototype.traverse = function(node, cb) {
|
|
if (this.shouldStop) { return; }
|
|
|
|
if (Array.isArray(node)) {
|
|
for (let i = 0, l = node.length; i < l; i++) {
|
|
const x = node[i];
|
|
if (x !== null) {
|
|
// Mark that the node has been visited
|
|
x.parent = node;
|
|
this.traverse(x, cb);
|
|
}
|
|
}
|
|
|
|
} else if (node && typeof node === 'object') {
|
|
cb(node);
|
|
|
|
for (let key in node) {
|
|
// Avoid visited nodes
|
|
if (key === 'parent' || !node[key]) { continue; }
|
|
|
|
node[key].parent = node;
|
|
this.traverse(node[key], cb);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Executes the passed callback for every traversed node of
|
|
* the passed in src's ast
|
|
*
|
|
* @param {String|Object} src - The source code or AST to traverse
|
|
* @param {Function} cb - Called for every node
|
|
*/
|
|
module.exports.prototype.walk = function(src, cb) {
|
|
this.shouldStop = false;
|
|
|
|
const ast = typeof src === 'object' ? src : this.parse(src);
|
|
|
|
this.traverse(ast, cb);
|
|
};
|
|
|
|
module.exports.prototype.moonwalk = function(node, cb) {
|
|
this.shouldStop = false;
|
|
|
|
if (typeof node !== 'object') {
|
|
throw new Error('node must be an object');
|
|
}
|
|
|
|
reverseTraverse.call(this, node, cb);
|
|
};
|
|
|
|
function reverseTraverse(node, cb) {
|
|
if (this.shouldStop || !node.parent) { return; }
|
|
|
|
if (node.parent instanceof Array) {
|
|
for (let i = 0, l = node.parent.length; i < l; i++) {
|
|
cb(node.parent[i]);
|
|
}
|
|
} else {
|
|
cb(node.parent);
|
|
}
|
|
|
|
reverseTraverse.call(this, node.parent, cb);
|
|
}
|
|
|
|
/**
|
|
* Halts further traversal of the AST
|
|
*/
|
|
module.exports.prototype.stopWalking = function() {
|
|
this.shouldStop = true;
|
|
};
|