nodesdeprecated

document object model for ast nodes, with parent references, queries and validation

Usage no npm install needed!

<script type="module">
  import nodes from 'https://cdn.skypack.dev/nodes';
</script>

README

nodes

Nodes is a dom-like library for spidermonkey-type ast objects, generated by Esprima or any compatible ast generator. Its purpose is to make it easy to work with ast nodes, by providing a dom-like environment with references to parentNodes, query-selector searches and type validations.

The idea is that you provide an ast json object, and you get back a document-like object. This package does not require esprima, nor does it enforce any specific esprima version.

It supports the full es6 specification.

The file in the root of this project, spec.json, has been manually generated by using estree as a reference. The json specification is used programmatically to set up the fake multiple inheritance for the nodes classes.

overview

Document creation:

var nodes = require('nodes');
var parse = require('esprima').parse;

var ast = parse(javascriptString);
var program = nodes.build(ast); // generate our document

creating new nodes

It is also possible to create new ast nodes using the classses provided by nodes directly:

var types = require('nodes').types;
var identifier = new types.Identifier({ name:  })

var declaration = new types.VariableDeclaration({ kind: "var" });
var declarator = new types.VariableDeclarator;
declaration.declarations.push(declarator)

syntax

Nodes also exports a syntax object, which holds every type as a string.

var syntax = require('nodes').syntax;
syntax.Identifier === "Identifier"; // true

parentNodes

Whenever you update any node or list, which also happens at creation, parentNodes references are saved to child nodes:

var expression = program.body[0];
expression.parentNode === program.body;
expression.expression.parentNode === expression;
// esprima always creates a program, just interested in creating a node for the declaration
var declaration = nodes.build(parse('var x = 0;').body[0]);

parogram.body.push(declaration);
declaration.parentNode === program.body;

validation

Each node property is validated against rules defined by the Mozilla Parser API:

declaration.declarations[0].id = 10; // Error! Declarators id must be Identifiers

Same goes for lists:

program.body.push(nodes.build({type: "Identifier", name: "asd"}));
// Error! program body only accepts Statements

queries

nodes implements css-like queries for any ast nodes.

Say you want to get all the Identifiers in a program:

var identifiers = program.search('#Identifier');
console.log(identifiers);
// [Identifier, Identifier, Identifier, Identifier]

Or maybe you are interested in the names only?

var identifiers = program.search('#Identifier > name');
console.log(identifiers);
// ['a', 'b', 'c', 'd']

An ID selector in this instance is equivalent to [type=id].

Direct children:

var id = program.find('#FunctionDeclaration > id');
console.log(id);
// Identifier

note: find is like search, but ends the traversal when it finds the first result.

Any level:

var id = program.search('#FunctionDeclaration id');
console.log(id);
// [Identifier, Identifier, ...]

It also supports generic types like #Function, #Statement, #Expression or #Pattern, in case you want to filter by the base type. For instance, #FunctionExpression, #FunctionDeclaration and #ArrowFunctionExpression will all react to #Function.

parent combinators:

var functionDeclaration = id.find('< #FunctionDeclaration');
console.log(functionDeclaration);
// FunctionDeclaration

parent method, for direct parents. this works like matchesSelector in dom, and also supports expression sequences:

var functionDeclaration = id.parent('#FunctionDeclaration');
console.log(functionDeclaration);
// FunctionDeclaration

parents query, for any parents, same as parent() but keeps traversing:

var functions = id.parents('#Function');
console.log(functions);
// [FunctionDeclaration, FunctionExpression, ...]

:declaration pseudo class to find any declaration

program.search('#Identifier:declaration > name');
program.search('#Identifier:declaration(someVarName)')

:reference pseudo class to find any reference

program.search('#Identifier:reference');
program.search('#Identifier:reference(someName)')

scope, scopes methods, works like parent / parents, but only cares about scopes.

id.scope(); // Program, even though a FunctionDeclaration id has the FunctionDeclaration as parent.

:scope pseudo class

program.search(':scope'); // all the scopes (Program and any Function)

You can also use attribute selectors or classNames in the queries to check if nodes have / match specific properties. Works pretty much like in the dom.

search / parents / scopes return a BaseList instance, which is an Array-Like object. Just like lists (e.g. program.body) you can run sub queries off of them.

var functions = program.search('#Function');
functions.search('id');

Multiple queries are also supported:

var functions = program.search('#FunctionExpression, #FunctionDeclaration');

Every list gets decomposed to its nodes:

var bodyElements = program.search('body');
// a BaseList of body nodes.

serialization

Document serialization is automatic, and no special steps are needed:

var generate = require('escodegen').generate;
generate(program);

If you need to you can use toJSON to convert the document back to json format:

var object = program.toJSON();

There is a toString() method to generate json, which is just a shortcut to JSON.stringify.

var jsonString = program.toString()