Understanding the AST Types API
Writing an ast-types builder
Consider the following code we are going to process with recast
:
const recast = require("recast");
const code = `
function add(a, b) {
return a - b;
}
`;
const ast = recast.parse(code);
const add = ast.program.body[0]; // The node of the add function declaration
and for that we are going to use recast/ast-types builders:
const B = recast.types.builders;
Since we want to convert the function add
declaration in a function expression, we build this AST:
ast.program.body[0] = B.variableDeclaration("const", [
B.variableDeclarator(add.id, B.functionExpression(
null, // Anonymize the function expression.
add.params,
add.body
))
]);
Now the question is: How to be sure about the API of the builders? How to build the call to a builder?
I found the module ast-types (opens in a new tab) and especially the file def/core.ts (opens in a new tab)) module helps a lot
to understand the ast
API.
For instance, the following is the definition of VariableDeclaration (opens in a new tab):
def("VariableDeclaration")
.bases("Declaration")
.build("kind", "declarations")
.field("kind", or("var", "let", "const"))
.field("declarations", [def("VariableDeclarator")]);
tell us that a variable declaration has two fields kind
and declarations
which is an array of VariableDeclarator
:
ast.program.body[0] = B.variableDeclaration("const", [ B.variableDeclarator( ... )) ]);
and this is the definition of VariableDeclarator (opens in a new tab):
def("VariableDeclarator")
.bases("Node")
.build("id", "init")
.field("id", def("Pattern"))
.field("init", or(def("Expression"), null), defaults["null"]);
that it has the fields id
and init
:
B.variableDeclarator(add.id, B.functionExpression( ... )
Expression is a sort of abstract class:
def("Expression").bases("Node");
def("FunctionExpression")
.bases("Function", "Expression")
.build("id", "params", "body");
def("ThisExpression").bases("Expression").build();
def("ArrayExpression")
.bases("Expression")
.build("elements")
.field("elements", [or(def("Expression"), null)]);
/* etc. */
That tell us that the FunctionExpression (opens in a new tab) is a Expression
and a kind of function and that to build a FunctionExpression
node we have to specify
the "id", "params"
and "body"
.
Here is the description of Function
nodes:
def("Function")
.bases("Node")
.field("id", or(def("Identifier"), null), defaults["null"])
.field("params", [def("Pattern")])
.field("body", def("BlockStatement"))
.field("generator", Boolean, defaults["false"])
.field("async", Boolean, defaults["false"]);
and that is why we build the FunctionExpression
this way:
B.functionExpression(null, add.params, add.body )
Where add
is the node containing the original function:
const recast = require("recast");
const code = `
function add(a, b) {
return a - b;
}
`;
const ast = recast.parse(code);
const add = ast.program.body[0]; // The node of the add function declaration
Rasengar AST-builder
A good way to build an ast-types tree is to use the Rasengar tool
On the left panel we write the code:
const add = function(b, a) {
return a * b;
};
and the panel below appears the build expression, so that we can use it:
> recast = require("recast")
> j = recast.types.builders // Now we paste the contents of the panel
> ast = j.variableDeclaration("const", [j.variableDeclarator(
... j.identifier("add"),
... j.functionExpression(null, [j.identifier("b"), j.identifier("a")], j.blockStatement([
... j.returnStatement(j.binaryExpression("*", j.identifier("a"), j.identifier("b")))
... ]))
... )]);
{
type: 'VariableDeclaration',
kind: 'const',
declarations: [ { type: 'VariableDeclarator',
id: [Object], init: [Object], loc: null, comments: null
}],
loc: null, comments: null
}
> recast.print(ast)
PrintResult {
code: 'const add = function(b, a) {\n return a * b;\n};'
}