AST Types
This module provides an efficient, modular,
Esprima-compatible implementation of
the abstract syntax
tree type hierarchy
pioneered by the Mozilla Parser
API.
Installation
From NPM:
npm install ast-types
From GitHub:
cd path/to/node_modules
git clone git://github.com/benjamn/ast-types.git
cd ast-types
npm install .
Basic Usage
import assert from "assert";
import {
namedTypes as n,
builders as b,
} from "ast-types";
var fooId = b.identifier("foo");
var ifFoo = b.ifStatement(fooId, b.blockStatement([
b.expressionStatement(b.callExpression(fooId, []))
]));
assert.ok(n.IfStatement.check(ifFoo));
assert.ok(n.Statement.check(ifFoo));
assert.ok(n.Node.check(ifFoo));
assert.ok(n.BlockStatement.check(ifFoo.consequent));
assert.strictEqual(
ifFoo.consequent.body[0].expression.arguments.length,
0,
);
assert.strictEqual(ifFoo.test, fooId);
assert.ok(n.Expression.check(ifFoo.test));
assert.ok(n.Identifier.check(ifFoo.test));
assert.ok(!n.Statement.check(ifFoo.test));
AST Traversal
Because it understands the AST type system so thoroughly, this library
is able to provide excellent node iteration and traversal mechanisms.
If you want complete control over the traversal, and all you need is a way
of enumerating the known fields of your AST nodes and getting their
values, you may be interested in the primitives getFieldNames
and
getFieldValue
:
import {
getFieldNames,
getFieldValue,
} from "ast-types";
const partialFunExpr = { type: "FunctionExpression" };
console.log(getFieldNames(partialFunExpr));
console.log(getFieldValue(partialFunExpr, "generator"));
Two more low-level helper functions, eachField
and someField
, are
defined in terms of getFieldNames
and getFieldValue
:
export function eachField(object, callback, context) {
getFieldNames(object).forEach(function(name) {
callback.call(this, name, getFieldValue(object, name));
}, context);
}
export function someField(object, callback, context) {
return getFieldNames(object).some(function(name) {
return callback.call(this, name, getFieldValue(object, name));
}, context);
}
So here's how you might make a copy of an AST node:
import { eachField } from "ast-types";
const copy = {};
eachField(node, function(name, value) {
copy[name] = value;
})
But that's not all! You can also easily visit entire syntax trees using
the powerful types.visit
abstraction.
Here's a trivial example of how you might assert that arguments.callee
is never used in ast
:
import assert from "assert";
import {
visit,
namedTypes as n,
} from "ast-types";
visit(ast, {
visitMemberExpression(path) {
var node = path.node;
if (
n.Identifier.check(node.object) &&
node.object.name === "arguments" &&
n.Identifier.check(node.property)
) {
assert.notStrictEqual(node.property.name, "callee");
}
this.traverse(path);
}
});
Here's a slightly more involved example of transforming ...rest
parameters into browser-runnable ES5 JavaScript:
import { builders as b, visit } from "ast-types";
var sliceExpr = b.memberExpression(
b.memberExpression(
b.memberExpression(
b.identifier("Array"),
b.identifier("prototype"),
false
),
b.identifier("slice"),
false
),
b.identifier("call"),
false
);
visit(ast, {
visitFunction(path) {
const node = path.node;
this.traverse(path);
if (!node.rest) {
return;
}
n.BlockStatement.assert(node.body);
const restVarDecl = b.variableDeclaration("var", [
b.variableDeclarator(
node.rest,
b.callExpression(sliceExpr, [
b.identifier("arguments"),
b.literal(node.params.length)
])
)
]);
path.get("body", "body").unshift(restVarDecl);
path.get("rest").replace(null);
assert.strictEqual(node.rest, null);
}
});
Here's how you might use types.visit
to implement a function that
determines if a given function node refers to this
:
function usesThis(funcNode) {
n.Function.assert(funcNode);
var result = false;
visit(funcNode, {
visitThisExpression(path) {
result = true;
this.abort();
},
visitFunction(path) {
return false;
},
visitCallExpression(path) {
const node = path.node;
if (this.isSuperCallExpression(node)) {
result = true;
this.abort();
}
this.traverse(path);
},
isSuperCallExpression(callExpr) {
n.CallExpression.assert(callExpr);
return this.isSuperIdentifier(callExpr.callee)
|| this.isSuperMemberExpression(callExpr.callee);
},
isSuperIdentifier(node) {
return n.Identifier.check(node.callee)
&& node.callee.name === "super";
},
isSuperMemberExpression(node) {
return n.MemberExpression.check(node.callee)
&& n.Identifier.check(node.callee.object)
&& node.callee.object.name === "super";
}
});
return result;
}
As you might guess, when an AbortRequest
is thrown from a subtree, the
exception will propagate from the corresponding calls to this.traverse
in the ancestor visitor methods. If you decide you want to cancel the
request, simply catch the exception and call its .cancel()
method. The
rest of the subtree beneath the try
-catch
block will be abandoned, but
the remaining siblings of the ancestor node will still be visited.
NodePath
The NodePath
object passed to visitor methods is a wrapper around an AST
node, and it serves to provide access to the chain of ancestor objects
(all the way back to the root of the AST) and scope information.
In general, path.node
refers to the wrapped node, path.parent.node
refers to the nearest Node
ancestor, path.parent.parent.node
to the
grandparent, and so on.
Note that path.node
may not be a direct property value of
path.parent.node
; for instance, it might be the case that path.node
is
an element of an array that is a direct child of the parent node:
path.node === path.parent.node.elements[3]
in which case you should know that path.parentPath
provides
finer-grained access to the complete path of objects (not just the Node
ones) from the root of the AST:
path.parentPath.parentPath === path.parent
path.parentPath.value === path.parent.node.elements
path.parentPath.value[3] === path.node
path.parentPath.node === path.parent.node
path.name === 3
path.parentPath.name === "elements"
path.parent.node[path.parentPath.name][path.name] === path.node
These NodePath
objects are created during the traversal without
modifying the AST nodes themselves, so it's not a problem if the same node
appears more than once in the AST (like Array.prototype.slice.call
in
the example above), because it will be visited with a distict NodePath
each time it appears.
Child NodePath
objects are created lazily, by calling the .get
method
of a parent NodePath
object:
path.get("elements").get(3).value === path.value.elements[3]
path.get("elements", 0).value === path.value.elements[0]
NodePath
objects support a number of useful methods:
var fifth = path.get("elements", 4);
fifth.replace(newNode);
fifth.replace(newerNode);
path.get("elements", 2).replace(
b.identifier("foo"),
b.thisExpression()
);
path.prune();
path.get("elements", 3).replace();
path.get("elements").unshift(a, b, c);
path.get("elements").shift();
path.get("elements").push(d, e);
path.get("elements").pop();
var seventh = path.get("elements", 6);
seventh.insertBefore(newNode);
seventh.insertAfter(newNode);
path.get("elements").insertAt(5, newNode);
Scope
The object exposed as path.scope
during AST traversals provides
information about variable and function declarations in the scope that
contains path.node
. See scope.ts for its public
interface, which currently includes .isGlobal
, .getGlobalScope()
,
.depth
, .declares(name)
, .lookup(name)
, and .getBindings()
.
Custom AST Node Types
The ast-types
module was designed to be extended. To that end, it
provides a readable, declarative syntax for specifying new AST node types,
based primarily upon the require("ast-types").Type.def
function:
import {
Type,
builtInTypes,
builders as b,
finalize,
} from "ast-types";
const { def } = Type;
const { string } = builtInTypes;
def("File")
.bases("Node")
.build("name", "program")
.field("name", string)
.field("program", def("Program"));
finalize();
const main = b.file("main.js", b.program([
b.functionDeclaration(b.identifier("succ"), [
b.identifier("x")
], b.blockStatement([
b.returnStatement(
b.binaryExpression(
"+", b.identifier("x"), b.literal(1)
)
)
]))
]));
assert.strictEqual(main.name, "main.js");
assert.strictEqual(main.program.body[0].params[0].name, "x");
b.file(b.blockStatement([]));
b.file("lib/types.js", b.thisExpression());
The def
syntax is used to define all the default AST node types found in
babel-core.ts,
babel.ts,
core.ts,
es-proposals.ts,
es6.ts,
es7.ts,
es2020.ts,
esprima.ts,
flow.ts,
jsx.ts,
type-annotations.ts,
and
typescript.ts,
so you have
no shortage of examples to learn from.