Recast is a great library for parsing and modifying JavaScript code. The result of the parsing is an Abstract Syntax Tree (AST) which is easy to traverse and analyze. Once a tree is built, it’s easy to modify it and convert back to the source code. Additionally, Recast provides methods for pretty printing ASTs as well as API to construct new ASTs without parsing any source code.
Here are some use cases for Recast:
- Static code analysis
- Generation of documentation based on source code comments
- Code formatting
- Automatic code modification
The last one is actually the main use case and Recast was created for this:
The more code you have, the harder it becomes to make big, sweeping changes quickly and confidently. Even if you trust yourself not to make too many mistakes, and no matter how proficient you are with your text editor, changing tens of thousands of lines of code takes precious, non-refundable time.
...
Specifically, my goal is to make it possible for you to run your code through a parser, manipulate the abstract syntax tree directly, subject only to the constraints of your imagination, and then automatically translate those modifications back into source code, without upsetting the formatting of unmodified code.
says [Ben Newman](https://github.com/benjamn/recast#motivation)
It’s very easy to start with Recast.
Getting Started with Recast
The ultimate sources for learning Recast are these two repositories: https://github.com/benjamn/recast and https://github.com/benjamn/ast-types The first one contains a lot of information about Recast’s own API and the second one explains how to work with ASTs.
Simple example demonstrates how to parse the source of a JS function (source: https://github.com/benjamn/recast):
var recast = require("recast");
var code = [
"function add(a, b) {",
" return a +",
" // Weird formatting, huh?",
" b;",
"}"
].join("\n");
var ast = recast.parse(code);
As you see, the source code was not formatting properly. Recast can fix this easily:
var output = recast.prettyPrint(ast, { tabWidth: 2 }).code;
console.log(output);
The output is:
function add(a, b) {
return a + b;
}
That’s it - source code formatting is covered. Now let’s rewrite the source function using AST builder API:
// Grab a reference to the function declaration we just parsed.
var add = ast.program.body[0];
var b = recast.types.builders; // builders help build AST nodes
// declare a var with the same name as the function
ast.program.body[0] = b.variableDeclaration("var", [
b.variableDeclarator(add.id, b.functionExpression(
null, // Anonymize the function expression.
add.params, // params
add.body // and body are left unchanged
))
]);
// Just for fun, because addition is commutative:
add.params.push(add.params.shift());
The output of this transformation is:
var add = function(b, a) {
return a + b;
}
Extracting Source Comments Using Recast
First, we need to parse the source code like in the previous example. Next, we have to find all comments. This can be done using visit
API:
recast.visit(ast, {
visitComment: function(path) {
console.log(path.value);
this.traverse(path); // continue visiting
}
});
This prints out all comments in the tree. When using visit API, you need to call either this.traverse
to continue traversing or this.abort()
to cancel traversing.
All functions of the visit API follow the same convention: visit<NodeType>
. For example, visitCallExpression
, visitFunction
are valid callbacks.
Generating Documentation Based on Comments
The previous method of getting comments has one drawback: you get comments for all parts of the code and you need to figure out what part comments relate to. There is another possibility: first, find nodes you are interested in and, second, get comments related to these nodes.
For example, let’s get comments for all object properties:
var recast = require("recast");
// our object with commented properties
var code = [
"var obj = {",
"/** this is my property */",
"myProperty: 'string'",
"}"
].join("\n");
var objAst = recast.parse(code);
attributeComments = [];
recast.visit(objAst, {
visitProperty: function(path) {
if (path.value.comments) {
attributeComments.push({
name: path.value.key.name,
comments: path.value.comments.map(function(c) {
return c.value;
}).join('\n')
});
}
this.traverse(path);
}
});
console.log(attributeComments);
The attributeComments
array will contain all properties of the object and related comments:
[ { name: 'myProperty', comments: '* this is my property ' } ]
Printing Out Entire AST
For learning it may be useful to inspect the entire tree, when you are not familiar with AST types:
var util = require('util');
console.log(util.inspect(objAst, { showHidden: false, depth: null }));
Switching Default Esprima
Sometimes the default Esprima used for parsing(esprima-fb
) may fail. For example, esprima-fb
failed to parse ES6 statements like const { obj } = otherObj;
. Recast allows you to change the default parser:
npm install babel-core
var babelCore = require('babel-core');
var ast = recast.parse(data, {esprima: babelCore});
Finally, here are some projects that show how to use Recast: Ember Watson, Commoner and others. Ember Watson was especially helpful/inspiring for me.
Thanks for reading and I hope you will make use of such a great tool as Recast.