npm.io
0.15.1 • Published 2d ago

yuku-ast

Licence
MIT
Version
0.15.1
Deps
0
Vulns
0
Weekly
0
Stars
944

yuku-ast

Walk, build, and check any ESTree / TypeScript-ESTree AST, with typed visitors, in-place mutation, builders, guards, and syntactic utilities, part of Yuku.

It is plain JavaScript and works on any ESTree AST, whichever parser produced it.

Install

npm install yuku-ast

Walking

import { parse } from "yuku-parser";
import { walk } from "yuku-ast";

const { program } = parse(source);

walk(program, {
  Identifier(node) {
    console.log(node.name);
  },
  CallExpression: {
    enter(node, ctx) {},
    leave(node, ctx) {},
  },
  Function(node) {
    // function declarations, expressions, and arrows
  },
  enter(node) {},
});

Handlers are keyed by node type, by alias group, or by the universal enter / leave, and receive the exact node type. The alias groups are Expression, Statement, Declaration, ModuleDeclaration, Function, Class, Method, Loop, Pattern, JSX, and TSType. Per node, the order is the universal enter, alias enters, the typed enter, the children, then the same in reverse for leave.

An optional third argument threads state to every handler as ctx.state. walkAsync is the async counterpart, with the same traversal order and mutation semantics and every handler awaited before the walk moves on.

The context

One context object is reused across the whole walk, so do not store it.

ctx.node;        // the current node
ctx.parent;      // its parent, or null at the walk root
ctx.key;         // the field on the parent holding this node
ctx.index;       // position in an array field, or null
ctx.ancestors(); // a copy of the ancestor chain, root first
Mutation

Handlers can mutate the AST in place.

Operation Effect
ctx.skip() Do not descend into this node's children. leave still fires.
ctx.stop() End the walk immediately.
ctx.replace(node) Swap the current node. The walk continues into the replacement's children and leave fires for its new type.
ctx.remove() Splice the node out of an array field, or null a plain field. Children are not walked, leave does not fire.
ctx.insertBefore(node) Insert a sibling before the current node. The inserted node is not visited.
ctx.insertAfter(node) Insert a sibling after the current node. The walk visits it.

A replacement created with start: 0, end: 0, such as a builder node, inherits the original node's span, which keeps source maps meaningful through yuku-codegen.

walk(program, {
  DebuggerStatement(node, ctx) {
    ctx.remove();
  },
});
findAll

findAll collects every node of the given types, in source order.

import { findAll } from "yuku-ast";

findAll(program, "CallExpression");
findAll(program, ["ClassDeclaration", "TSInterfaceDeclaration"]);

CHILD_KEYS maps each node type to its child fields in traversal order, for walkers of your own.

Builders

b has one typed constructor per node type, its fields derived from the node type itself, so a builder never drifts from the AST. Spans default to 0, which ctx.replace fills from the replaced node.

import { b } from "yuku-ast";

b.CallExpression({ callee: b.Identifier({ name: "f" }), arguments: [], optional: false });

Guards

is has one guard per node type, per alias group, and for the shapes ESTree folds into one type: literal kinds, member expression kinds, and directives. Every guard accepts null and undefined and narrows.

import { is } from "yuku-ast";

is.CallExpression(node);
is.Identifier(node, "require");
is.oneOf(node, ["FunctionDeclaration", "ClassDeclaration"]);
is.Expression(node);
is.StringLiteral(node);
is.StaticMemberExpression(node);
is.Directive(node);

Imports and exports

collectImports and collectExports read a module's import and export declarations, one record per bound name, destructuring included.

import { collectExports, collectImports } from "yuku-ast";

for (const record of collectImports(program)) {
  record.source;   // "./m"
  record.local;    // the local binding name
  record.imported; // "default", "*", or the export name
  record.typeOnly; // import type / import { type x }
  record.phase;    // "source" | "defer" | null
}

for (const record of collectExports(program)) {
  record.exported; // the exported name, null for a bare export *
  record.local;    // the backing local name, when there is one
  record.source;   // the re-export specifier, when there is one
  record.typeOnly;
}

collectImportDeclaration and collectExportDeclaration return the records of a single statement, for use inside a walk.

walk(program, {
  ImportDeclaration(node) {
    records.push(...collectImportDeclaration(node));
  },
});

Utilities

import {
  bindingIdentifiers, // every binding Identifier a pattern introduces
  isCallOf,           // isCallOf(node, "require")
  isWrapper,          // true for the wrappers unwrap strips
  literalValue,       // string | number | boolean | bigint | RegExp | null
  nameOf,             // Identifier name or string Literal value
  unwrap,             // strips parens and erased TypeScript assertion wrappers
} from "yuku-ast";

Identifier names

import { isIdentifierName, isValidIdentifier } from "yuku-ast";

isValidIdentifier("foo");   // true
isValidIdentifier("class"); // false, reserved
isIdentifierName("class");  // true, syntactically an IdentifierName

Plus isIdentifierStart, isIdentifierChar, isKeyword, isReservedWord, isStrictReservedWord, isStrictBindReservedWord, and isStrictBindOnlyReservedWord.

Semantic analysis

yuku-analyzer builds on this walker. Its module.walk adds the semantic model to the context, as ctx.binding, ctx.reference, and ctx.scope.

License

MIT

Keywords