abstract-syntax-tree
A library for working with abstract syntax trees.
Key features
- parse, transform and generate code with a single dependency
- offers both functional and class interfaces
- built-in ast <-> js types helpers - serialize and template
- built-in find, has, scope analysis, rename, prune, graph and bundle
- reads CommonJS as well as modules, and no filesystem access unless you opt in with abstract-syntax-tree/fs
- built-in transformations like append, prepend
- 25+ methods total
Table of Contents
Background
An abstract syntax tree is a way to represent the source code. In case of this library it is represented in the estree format.
For example, the following source code:
const answer = 42
Has the following representation:
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "answer"
},
"init": {
"type": "Literal",
"value": 42
}
}
],
"kind": "const"
}
]
}
The goal of this library is to consolidate common abstract syntax tree operations in one place. It uses a variety of libraries under the hood based on their performance and flexibility, e.g. meriyah for parsing and astring for source code generation.
The library exposes a set of utility methods that can be useful for analysis or transformation of abstract syntax trees. It supports functional and object-oriented programming style.
Installation
npm install abstract-syntax-tree
Usage
Functional programming style
const { parse, find } = require("abstract-syntax-tree")
const source = "const answer = 42"
const tree = parse(source)
console.log(find(tree, "Literal")) // [ { type: 'Literal', value: 42 } ]
Object oriented programming style
const AbstractSyntaxTree = require("abstract-syntax-tree")
const source = "const answer = 42"
const tree = new AbstractSyntaxTree(source)
console.log(tree.find("Literal")) // [ { type: 'Literal', value: 42 } ]
API
Static Methods
parse
The library uses meriyah to create an estree compatible abstract syntax tree. All meriyah parsing options can be passed to the parse method.
const { parse } = require("abstract-syntax-tree")
const source = "const answer = 42"
const tree = parse(source)
console.log(tree) // { type: 'Program', body: [ ... ] }
const { parse } = require("abstract-syntax-tree")
const source = "const answer = 42"
const tree = parse(source, {
loc: true,
ranges: true,
})
console.log(tree) // { type: 'Program', body: [ ... ], loc: {...} }
generate
The library uses astring to generate the source code. All astring generate options can be passed to the generate method.
const { parse, generate } = require("abstract-syntax-tree")
const source = "const answer = 42"
const tree = parse(source)
console.log(generate(tree)) // 'const answer = 42;'
walk
Walk method is a thin layer over estraverse.
const { parse, walk } = require("abstract-syntax-tree")
const source = "const answer = 42"
const tree = parse(source)
walk(tree, (node, parent) => {
console.log(node)
console.log(parent)
})
find
Find supports two traversal methods. You can pass a string selector or pass an object that will be compared to every node in the tree. The method returns an array of nodes.
The following selectors are supported:
- node type (
Identifier) - node attribute (
[name="foo"]) - node attribute existence (
[name]) - wildcard (
*)
const { parse, find } = require("abstract-syntax-tree")
const source = "const answer = 42"
const tree = parse(source)
console.log(find(tree, "VariableDeclaration")) // [ { type: 'VariableDeclaration', ... } ]
console.log(find(tree, { type: "VariableDeclaration" })) // [ { type: 'VariableDeclaration', ... } ]
serialize
Serialize can transform nodes into values. Works for: Array, Boolean, Error, Infinity, Map, NaN, Number, Object, RegExp, Set, String, Symbol, WeakMap, WeakSet, null and undefined.
const { serialize } = require("abstract-syntax-tree")
const node = {
type: "ArrayExpression",
elements: [
{ type: "Literal", value: 1 },
{ type: "Literal", value: 2 },
{ type: "Literal", value: 3 },
{ type: "Literal", value: 4 },
{ type: "Literal", value: 5 },
],
}
const array = serialize(node) // [1, 2, 3, 4, 5]
traverse
Traverse method accepts a configuration object with enter and leave callbacks. It allows multiple transformations in one traversal.
const { parse, traverse } = require("abstract-syntax-tree")
const source = "const answer = 42"
const tree = parse(source)
traverse(tree, {
enter(node) {},
leave(node) {},
})
replace
Replace extends estraverse by handling replacement of give node with multiple nodes. It will also remove given node if null is returned.
const { parse, replace } = require("abstract-syntax-tree")
const source = "const answer = 42"
const tree = parse(source)
replace(tree, (node) => {
if (node.type === "VariableDeclaration") {
node.kind = "let"
}
return node
})
remove
Remove uses estraverse and ensures that no useless nodes are left in the tree. It accepts a string, object or callback as the matching strategy.
const { parse, remove, generate } = require("abstract-syntax-tree")
const source = '"use strict"; const b = 4;'
const tree = parse(source)
remove(tree, 'Literal[value="use strict"]')
// or
// remove(tree, { type: 'Literal', value: 'use strict' })
// or
// remove(tree, (node) => {
// if (node.type === 'Literal' && node.value === 'use strict') return null
// return node
// })
console.log(generate(tree)) // 'const b = 4;'
each
const { parse, each } = require("abstract-syntax-tree")
const source = "const foo = 1; const bar = 2;"
const tree = parse(source)
each(tree, "VariableDeclaration", (node) => {
console.log(node)
})
first
const { parse, first } = require("abstract-syntax-tree")
const source = "const answer = 42"
const tree = parse(source)
console.log(first(tree, "VariableDeclaration")) // { type: 'VariableDeclaration', ... }
last
const { parse, last } = require("abstract-syntax-tree")
const source = "const answer = 42"
const tree = parse(source)
console.log(last(tree, "VariableDeclaration")) // { type: 'VariableDeclaration', ... }
reduce
const { parse, reduce } = require("abstract-syntax-tree")
const source = "const a = 1, b = 2"
const tree = parse(source)
const value = reduce(
tree,
(sum, node) => {
if (node.type === "Literal") {
sum += node.value
}
return sum
},
0
)
console.log(value) // 3
scope
Builds a lexical scope tree from the abstract syntax tree. The tree provides generic information about scopes, bindings (declarations) and references (usages), which is useful for analysis, renaming, dead code elimination, reactivity tracking and more.
const { parse, scope } = require("abstract-syntax-tree")
const source = "const answer = 42; console.log(answer)"
const tree = parse(source)
const root = scope(tree)
console.log(root.type) // 'module'
console.log(root.bindings.map((binding) => binding.name)) // [ 'answer' ]
console.log(root.getBinding("answer").references.length) // 1
console.log(root.globals.map((global) => global.name)) // [ 'console' ]
Each scope exposes the following shape:
type- the kind of scope (module,global,function,block,for,switch,catchorclass)node- the node that creates the scopeparent- the enclosing scope, ornullfor the rootchildren- the nested scopesbindings- the declarations made in this scopereferences- the references that occur directly in this scopeglobals- unresolved references (populated on the root scope)root- the root scope of the treevariableScope- the nearest function or module scope (wherevaris hoisted)getBinding(name)- returns a binding declared in this scopelookup(name)- resolves a binding by name, walking up the scope chaingetReference(node)- returns the reference for an identifier node (keyed by the node itself, so it handles shadowing), ornullwhen the node is not a reference; the returned reference exposes the resolvedbinding
Each binding exposes:
name- the declared namekind- the kind of binding (var,let,const,function,class,param,catch,importorimplicit)scope- the scope the binding belongs toidentifier/declarations- the identifier node(s) that declare the bindingnode- the declaration nodereferences- all references that resolve to the bindingreferenced- whether the binding is used at least onceconstant- whether the binding is never reassignedreads/writes- the read and write references
Each reference exposes:
identifier- the identifier nodename- the referenced namescope- the scope the reference occurs inparent- the node that directly contains the identifier (e.g. theCallExpressionforfoo()), useful for understanding how a reference is usedbinding- the resolved binding, ornullfor a globalread/write- how the value is usedresolved- whether the reference resolved to a binding
Finding unused declarations (e.g. for dead code elimination):
const { parse, scope } = require("abstract-syntax-tree")
const tree = parse("const used = 1; const unused = 2; used")
const root = scope(tree)
const unused = root.bindings.filter((binding) => !binding.referenced)
console.log(unused.map((binding) => binding.name)) // [ 'unused' ]
Finding reassigned variables (e.g. for reactivity):
const { parse, scope } = require("abstract-syntax-tree")
const tree = parse("let a = 1; a = 2; const b = 3")
const root = scope(tree)
const mutable = root.bindings.filter((binding) => !binding.constant)
console.log(mutable.map((binding) => binding.name)) // [ 'a' ]
rename
Renames a variable and every reference to it, using scope analysis so that only the right identifiers are changed. Unlike a plain find and replace it leaves property keys, string literals, labels and unrelated variables in other scopes untouched, and it expands shorthand properties and adds as clauses to imports and exports where needed. The tree is mutated in place and returned.
const { parse, generate, rename } = require("abstract-syntax-tree")
const tree = parse("const foo = 1; const o = { foo }; foo")
rename(tree, "foo", "bar")
console.log(generate(tree)) // const bar = 1; const o = { foo: bar }; bar;
Every binding with the given name is renamed, each together with its own references, so shadowed variables stay correct:
const { parse, generate, rename } = require("abstract-syntax-tree")
const tree = parse("let foo = 1; function f () { let foo = 2; return foo } foo")
rename(tree, "foo", "bar")
console.log(generate(tree)) // let bar = 1; function f() { let bar = 2; return bar; } bar;
Pass scope to rename a single binding instead of every binding with that name. It takes "all" (the default), "top" for the binding declared in the root scope, or a scope from a scope call on the same tree:
const { parse, generate, rename } = require("abstract-syntax-tree")
const tree = parse("let foo = 1; function f () { let foo = 2; return foo } foo")
rename(tree, "foo", "renamed", { scope: "top" })
console.log(generate(tree)) // let renamed = 1; function f() { let foo = 2; return foo; } renamed;
const { parse, generate, scope, rename } = require("abstract-syntax-tree")
const tree = parse("let foo = 1; function f () { let foo = 2; return foo } foo")
const root = scope(tree)
rename(tree, "foo", "inner", { scope: root.children[0] })
console.log(generate(tree)) // let foo = 1; function f() { let inner = 2; return inner; } foo;
It throws when the new name is already declared in the same scope, which would change the meaning of the code:
const { parse, rename } = require("abstract-syntax-tree")
const tree = parse("let foo = 1; let bar = 2")
rename(tree, "foo", "bar") // throws: Cannot rename "foo" to "bar": "bar" is already declared in the same scope
It also throws when the rename would leave an identifier resolving to a different binding. This is the dangerous case, because the result is valid code that quietly means something else, so it is rejected rather than emitted:
const { parse, rename } = require("abstract-syntax-tree")
// f returns 1; renaming foo to bar would make it return 2
const tree = parse("const foo = 1; function f () { const bar = 2; return foo }")
rename(tree, "foo", "bar") // throws: a reference to "foo" would be captured by "bar" declared in an inner scope
const { parse, rename } = require("abstract-syntax-tree")
// f returns the outer bar; renaming foo to bar would shadow it
const tree = parse("const bar = 1; function f () { const foo = 2; return bar }")
rename(tree, "foo", "bar") // throws: an existing reference to "bar" would be shadowed by the renamed binding
Nothing is mutated when a rename is rejected, so a throw always leaves the tree as it was.
prune
Removes unused declarations from the tree, a form of dead code elimination built on scope analysis. A declaration is removed only when it is not referenced anywhere and removing it cannot change behaviour. Removing one binding can make another unused, so pruning repeats until nothing else can be removed. The tree is mutated in place and returned.
const { parse, generate, prune } = require("abstract-syntax-tree")
const tree = parse("const used = 1; const unused = 2; used")
prune(tree)
console.log(generate(tree)) // const used = 1; used;
const { parse, generate, prune } = require("abstract-syntax-tree")
// a becomes unused once b is removed, so both go
const tree = parse("const a = 1; const b = a")
prune(tree)
console.log(generate(tree)) // (empty)
To stay safe, prune is deliberately conservative. It removes unreferenced var, let, const and function declarations, but keeps:
- declarations whose initializer may have side effects (
const x = compute(),const x = obj.prop,const x = [...items]) - exported declarations and anything referenced by an
export - destructuring declarations, function parameters, catch parameters and loop variables
- class declarations and imports
- write only bindings (a variable that is assigned but never read)
const { parse, generate, prune } = require("abstract-syntax-tree")
// the call is kept because it may have side effects
const tree = parse("const x = compute()")
prune(tree)
console.log(generate(tree)) // const x = compute();
graph
Walks the module graph from an entry module and returns what it reaches: every module, the order they evaluate in, any circular imports and any external specifiers. Nothing is read from disk, so the modules to resolve against are given as options.modules, keyed by whatever ids you want to use.
const { graph } = require("abstract-syntax-tree")
const modules = {
"./math.js": "export const PI = 3.14",
"./util.js": 'import "./math.js"'
}
const result = graph('import "./util.js"', { modules })
console.log(result.order) // [ "./math.js", "./util.js", "entry" ]
The result has the following shape:
entry- the id of the entry module,"entry"unless you passoptions.entrymodules- aMapof id to module, each withid,tree,dependencies,importsandexportsorder- module ids in evaluation order, dependencies before their importerscycles- circular import paths, each ending where it startedexternals- specifiers left as imports, each listed oncemissing- unresolvable specifiers, only populated whenmissingis"collect"
Circular imports are valid modules, so they are reported rather than thrown on. The cycle is cut at the back edge, which leaves each module in the position ESM itself evaluates it in:
const { graph } = require("abstract-syntax-tree")
const modules = { "./a.js": 'import "./b.js"', "./b.js": 'import "./a.js"' }
const result = graph('import "./a.js"', { modules })
console.log(result.cycles) // [ [ "./a.js", "./b.js", "./a.js" ] ]
A specifier that names a package rather than a module in the map is external and is not followed:
const { graph } = require("abstract-syntax-tree")
const result = graph('import React from "react"', { modules: {} })
console.log(result.externals) // [ "react" ]
console.log(result.order) // [ "entry" ]
Resolution tries an exact key first, so a flat map keyed by the specifiers themselves needs no path handling at all. Otherwise a relative specifier is joined to its importer and tried with no extension, .js, .mjs and as a directory index. Pass resolve to replace this entirely, or external to mark specifiers as external without resolving them:
const { graph } = require("abstract-syntax-tree")
const modules = { "src/math.js": "export const PI = 3.14" }
const result = graph('import "./math.js"', { modules, entry: "src/entry.js" })
console.log(result.order) // [ "src/math.js", "src/entry.js" ]
const { graph } = require("abstract-syntax-tree")
// resolve receives the specifier and the id of the module importing it
const result = graph('import "./a.js"', {
modules: { a: "const x = 1" },
resolve: (specifier, importer) => "a"
})
console.log(result.order) // [ "a", "entry" ]
A resolver returns null to leave a specifier as an import, and undefined when it did not find anything, which is reported as missing rather than quietly turned into an external.
An unresolvable relative specifier throws, because it names a module that was meant to be there. Pass missing: "collect" to gather them instead:
const { graph } = require("abstract-syntax-tree")
graph('import "./nope.js"', { modules: {} }) // throws: Cannot resolve "./nope.js" from "entry"
const { graph } = require("abstract-syntax-tree")
const result = graph('import "./nope.js"', { modules: {}, missing: "collect" })
console.log(result.missing) // [ { specifier: "./nope.js", importer: "entry" } ]
bundle
Bundles the entry module and everything it imports into a single program. Module bodies are concatenated into one scope, so the output reads like the source: there is no runtime, no wrapper functions and no module registry. Bindings are renamed only where names actually collide, which means a set of modules with no clashes comes out exactly as it went in. Nothing is read from disk, so the modules are given as options.modules, the same way graph takes them.
const { bundle, generate } = require("abstract-syntax-tree")
const modules = {
"./math.js": "export const PI = 3.14; export default function multiply (a, b) { return a * b }"
}
const entry = 'import multiply, { PI } from "./math.js"; console.log(multiply(PI, 2))'
console.log(generate(bundle(entry, { modules })))
// const PI = 3.14;
// function multiply(a, b) { return a * b; }
// console.log(multiply(PI, 2));
Because everything ends up in one scope, a name used by two modules has to move. Dependencies are deconflicted first, so the module that is imported keeps its name:
const { bundle, generate } = require("abstract-syntax-tree")
const modules = { "./math.js": "export const PI = 3.14" }
const entry = 'import { PI as P } from "./math.js"; const PI = 3; console.log(P, PI)'
console.log(generate(bundle(entry, { modules })))
// const PI = 3.14;
// const PI$1 = 3;
// console.log(PI, PI$1);
Unused exports are removed with prune, which sees the hoisted code as ordinary declarations. Pass treeshake: false to keep everything:
const { bundle, generate } = require("abstract-syntax-tree")
const modules = { "./math.js": "export const used = 1; export const unused = 2" }
const entry = 'import { used } from "./math.js"; console.log(used)'
console.log(generate(bundle(entry, { modules }))) // const used = 1; console.log(used);
A namespace import is dissolved into the bindings it reads when every use is a static property access. When the namespace is used as a value, or read with a computed key, the object is built instead, with a getter per name so the bindings stay live:
const { bundle, generate } = require("abstract-syntax-tree")
const modules = { "./math.js": "export const PI = 3.14; export const E = 2.71" }
const entry = 'import * as math from "./math.js"; console.log(math.PI)'
console.log(generate(bundle(entry, { modules }))) // const PI = 3.14; console.log(PI);
Specifiers that name a package rather than a module in the map stay as imports, and locals for the same external binding are shared so each source produces one import:
const { bundle, generate } = require("abstract-syntax-tree")
const entry = 'import React from "react"; console.log(React)'
console.log(generate(bundle(entry, { modules: {} })))
// import React from "react";
// console.log(React);
The options are the ones graph takes - entry, modules, resolve, external and missing - plus:
treeshake- whether to remove unused declarations,trueby defaultnamespace-"auto"(default) or"object"to always build the namespace objectcycles-"allow"(default) or"throw"
Caveats:
- the module trees are mutated and spliced into the result, so pass trees you can afford to lose, or re-parse
- modules in, modules out - CommonJS is not converted
- nothing is read from disk, so
modulesis the whole universe import.metaand dynamicimport()are passed through untouched, which means a dynamic import is not followed and its specifier is left as written- hoisting reorders statements, so a circular dependency that worked as separate modules can become a temporal dead zone error; graph reports
cyclesif you want to check first - the entry's exports are stripped with everyone else's and re-emitted at the end, so they keep their public names even when the binding behind them is renamed
CommonJS
A module written as CommonJS is converted to modules syntax before anything else looks at it, so graph sees its requires as the dependencies they are and bundle can hoist it like everything else. Nothing opts in; a module that already uses import or export is left exactly as it was.
const { bundle, generate } = require("abstract-syntax-tree")
const modules = { "./math.js": "function add (a, b) { return a + b }\nmodule.exports = add" }
const entry = 'import add from "./math.js"\nconsole.log(add(1, 2))'
console.log(generate(bundle(entry, { modules })))
// function add(a, b) { return a + b; }
// console.log(add(1, 2));
module.exports = { a, b } becomes a default export and named exports for the keys that can be read statically, the same thing Node does, so both import styles work and unused names still shake out:
const { bundle, generate } = require("abstract-syntax-tree")
const modules = { "./m.js": "const a = 1\nconst unused = 2\nmodule.exports = { a, unused }" }
const entry = 'import { a } from "./m.js"\nconsole.log(a)'
console.log(generate(bundle(entry, { modules }))) // const a = 1; console.log(a);
What is converted:
const x = require("./y")andconst { a, b } = require("./y")at the top levelrequire("./y")on its own, for side effectsmodule.exports = <identifier>andmodule.exports = <expression>module.exports = { a, b }, which also produces named exportsexports.name = <expression>
Everything else is refused by name rather than translated into something that looks right and behaves differently. A require that is computed, nested in a function or inside a condition cannot be resolved without running the code; __dirname and __filename have no meaning once modules are concatenated; and a reassigned module.exports has no single value to export:
const { bundle } = require("abstract-syntax-tree")
const modules = { "./m.js": 'module.exports = function () { return require("./other.js") }' }
bundle('import "./m.js"', { modules })
// throws: Cannot convert "./m.js" from CommonJS: a require call outside a top level declaration
require, module and exports only mean CommonJS when they are free variables, so a module that declares its own is an ordinary script and is left alone.
Pass commonjs: false to turn the conversion off and treat every module as written. graph records which modules were converted, as commonjs on each one. Nothing here reads a package.json or consults a type field; the format is read off the code itself, which is what keeps graph and bundle free of any package manager.
Reading from disk
The library itself never touches the filesystem, which is what keeps it usable anywhere and free of a resolver of its own. abstract-syntax-tree/fs is a separate entry point that provides the two callbacks graph and bundle need to work against real files, so requiring it is how you opt in to disk access:
const path = require("node:path")
const { bundle, generate } = require("abstract-syntax-tree")
const { modules, resolve } = require("abstract-syntax-tree/fs")
const entry = path.resolve("src/index.js")
console.log(generate(bundle(modules(entry), { entry, modules, resolve })))
modules(id)reads a file, or returnsundefinedwhen the id is not a readable fileresolve(specifier, importer)resolves against the importer's directory, trying no extension,.js,.mjsand a directory indexresolver(options)builds a resolver, which is how you turn onpackages
Resolution is anchored to the importer rather than the working directory, and a relative specifier that matches no file is reported as missing instead of being left as an import. Ids are canonical paths, so a file reached through a symlink and through a relative path is one module rather than two.
A bare specifier names a package, and by default it stays an import the way it does in every other bundler. Pass packages: true to read it out of node_modules and pull it into the bundle instead:
const path = require("node:path")
const { bundle, generate } = require("abstract-syntax-tree")
const { modules, resolver } = require("abstract-syntax-tree/fs")
const resolve = resolver({ packages: true })
const entry = path.resolve("src/index.js")
console.log(generate(bundle(modules(entry), { entry, modules, resolve })))
Packages are resolved with Node's own algorithm, so package walking, main, exports maps, subpaths like lodash/fp and scoped names all behave the way they do everywhere else. Node builtins stay external. Most packages on npm are CommonJS, which is converted on the way in.
Both are ordinary functions, so layering your own rules on top is just a wrapper:
const path = require("node:path")
const { modules, resolve } = require("abstract-syntax-tree/fs")
const alias = (specifier, importer) =>
specifier.startsWith("~/")
? path.resolve("src", specifier.slice(2))
: resolve(specifier, importer)
// pass { modules, resolve: alias } to graph or bundle
has
const { parse, has } = require("abstract-syntax-tree")
const source = "const answer = 42"
const tree = parse(source)
console.log(has(tree, "VariableDeclaration")) // true
console.log(has(tree, { type: "VariableDeclaration" })) // true
count
const { parse, count } = require("abstract-syntax-tree")
const source = "const answer = 42"
const tree = parse(source)
console.log(count(tree, "VariableDeclaration")) // 1
console.log(count(tree, { type: "VariableDeclaration" })) // 1
append
Append pushes nodes to the body of the abstract syntax tree. It accepts estree nodes as input.
const { parse, append } = require("abstract-syntax-tree")
const source = "const answer = 42"
const tree = parse(source)
append(tree, {
type: "ExpressionStatement",
expression: {
type: "CallExpression",
callee: {
type: "MemberExpression",
object: {
type: "Identifier",
name: "console",
},
property: {
type: "Identifier",
name: "log",
},
computed: false,
},
arguments: [
{
type: "Identifier",
name: "answer",
},
],
},
})
Strings will be converted into abstract syntax tree under the hood. Please note that this approach might make the code run a bit slower due to an extra interpretation step.
const { parse, append } = require("abstract-syntax-tree")
const source = "const answer = 42"
const tree = parse(source)
append(tree, "console.log(answer)")
prepend
Prepend unshifts nodes to the body of the abstract syntax tree. Accepts estree nodes or strings as input, same as append.
const { parse, prepend } = require("abstract-syntax-tree")
const source = "const a = 1;"
const tree = parse(source)
prepend(tree, {
type: "ExpressionStatement",
expression: {
type: "Literal",
value: "use strict",
},
})
equal
const { equal } = require("abstract-syntax-tree")
console.log(
equal({ type: "Literal", value: 42 }, { type: "Literal", value: 42 })
) // true
console.log(
equal({ type: "Literal", value: 41 }, { type: "Literal", value: 42 })
) // false
match
const { match } = require("abstract-syntax-tree")
console.log(match({ type: "Literal", value: 42 }, "Literal[value=42]")) // true
console.log(match({ type: "Literal", value: 41 }, "Literal[value=42]")) // false
template
The function converts the input to an equivalent abstract syntax tree representation. The function uses a <%= %> syntax to insert nodes.
You can also use standard javascript types and they're going to be transformed automatically.
const { template } = require("abstract-syntax-tree")
const literal = template(42)
const nodes = template("const foo = <%= bar %>;", {
bar: { type: "Literal", value: 1 },
})
const { template } = require("abstract-syntax-tree")
const nodes = template("function foo(<%= bar %>) {}", {
bar: [
{ type: "Identifier", name: "baz" },
{ type: "Identifier", name: "qux" },
],
})
const { template } = require("abstract-syntax-tree")
const literal = template(42)
const nodes = template("const foo = <%= bar %>;", {
bar: 1,
})
program
Creates an abstract syntax tree with a blank program.
const { program } = require("abstract-syntax-tree")
const tree = program() // { type: 'Program', sourceType: 'module', body: [] }
iife
Creates an abstract syntax tree for an immediately invoked function expression.
const { iife } = require("abstract-syntax-tree")
const node = iife() // { type: 'ExpressionStatement', expression: { ... } }
Instance Methods
Almost all of the static methods (excluding parse, generate, template and match) have their instance equivalents. There are few extra instance methods:
mark
const AbstractSyntaxTree = require("abstract-syntax-tree")
const tree = new AbstractSyntaxTree("const a = 1")
tree.mark()
console.log(tree.first("Program").cid) // 1
console.log(tree.first("VariableDeclaration").cid) // 2
wrap
const AbstractSyntaxTree = require("abstract-syntax-tree")
const source = "const a = 1"
const tree = new AbstractSyntaxTree(source)
tree.wrap((body) => {
return [
{
type: "ExpressionStatement",
expression: {
type: "CallExpression",
callee: {
type: "FunctionExpression",
params: [],
body: {
type: "BlockStatement",
body,
},
},
arguments: [],
},
},
]
})
unwrap
const AbstractSyntaxTree = require("abstract-syntax-tree")
const source = "(function () { console.log(1); }())"
const tree = new AbstractSyntaxTree(source)
tree.unwrap()
console.log(tree.source) // console.log(1);
Getters
body
Gives the body of the root node.
source
Gives access to the source code representation of the abstract syntax tree.
const AbstractSyntaxTree = require("abstract-syntax-tree")
const source = 'const foo = "bar";'
const tree = new AbstractSyntaxTree(source)
console.log(tree.source) // const foo = "bar";
map
Gives the source map of the source code.
Setters
body
Sets the body of the root node.
Transformations
toBinaryExpression
const { toBinaryExpression } = require("abstract-syntax-tree")
const expression = {
type: "ArrayExpression",
elements: [
{ type: "Literal", value: "foo" },
{ type: "Literal", value: "bar" },
{ type: "Literal", value: "baz" },
],
}
console.log(toBinaryExpression(expression)) // { type: 'BinaryExpression', ... }
Nodes
You can also use classes to create nodes.
const { ArrayExpression, Literal } = require("abstract-syntax-tree")
const expression = new ArrayExpression([
new Literal("foo"),
new Literal("bar"),
new Literal("baz"),
])
Here's a list of all available nodes, with examples.
| Type | Example |
|---|---|
| ArrayExpression | const foo = [] |
| ArrayPattern | const [foo, bar] = bar |
| ArrowFunctionExpression | (() => {}) |
| AssignmentExpression | foo = bar |
| AssignmentOperator | |
| AssignmentPattern | function foo(bar = baz) {} |
| AwaitExpression | (async () => { await foo() })() |
| BigIntLiteral | |
| BinaryExpression | foo + bar |
| BinaryOperator | |
| BlockStatement | { console.log(foo) } |
| BreakStatement | for (foo in bar) break |
| CallExpression | foo() |
| CatchClause | try {} catch (error) {} |
| ChainElement | |
| ChainExpression | foo?.() |
| Class | |
| ClassBody | class Foo {} |
| ClassDeclaration | class Foo {} |
| ClassExpression | (class {}) |
| ConditionalExpression | foo ? bar : baz |
| ContinueStatement | while(true) { continue } |
| DebuggerStatement | debugger |
| Declaration | |
| Directive | |
| DoWhileStatement | do {} while (true) {} |
| EmptyStatement | ; |
| ExportAllDeclaration | export * from "foo" |
| ExportDefaultDeclaration | export default foo |
| ExportNamedDeclaration | export { foo as bar } |
| ExportSpecifier | export { foo } |
| Expression | |
| ExpressionStatement | foo |
| ForInStatement | for (foo in bar) {} |
| ForOfStatement | for (foo of bar) {} |
| ForStatement | for (let i = 0; i < 10; i ++) {} |
| Function | |
| FunctionBody | |
| FunctionDeclaration | function foo () {} |
| FunctionExpression | (function () {}) |
| Identifier | foo |
| IfStatement | if (foo) {} |
| ImportDeclaration | import "foo" |
| ImportDefaultSpecifier | import foo from "bar" |
| ImportExpression | import(foo).then(bar) |
| ImportNamespaceSpecifier | import * as foo from "bar" |
| ImportSpecifier | import { foo } from "bar" |
| LabeledStatement | label: foo |
| Literal | 42 |
| LogicalExpression | true && false |
| LogicalOperator | |
| MemberExpression | foo.bar |
| MetaProperty | function foo () { new.target } |
| MethodDefinition | class Foo { bar() {} } |
| ModuleDeclaration | |
| ModuleSpecifier | |
| NewExpression | new Foo() |
| Node | |
| ObjectExpression | ({}) |
| ObjectPattern | function foo ({}) {} |
| Pattern | |
| Position | |
| Program | 42 |
| Property | |
| RegExpLiteral | |
| RestElement | function foo (...bar) {} |
| ReturnStatement | function foo () { return bar } |
| SequenceExpression | foo, bar |
| SourceLocation | |
| SpreadElement | |
| Statement | |
| Super | class Foo extends Bar { constructor() { super() } } |
| SwitchCase | switch (foo) { case 'bar': } |
| SwitchStatement | switch(foo) {} |
| TaggedTemplateExpression | css |
| TemplateLiteral | css |
| ThisExpression | this.foo = 'bar' |
| ThrowStatement | throw new Error("foo") |
| TryStatement | try { foo() } catch (exception) { bar() } |
| UnaryExpression | !foo |
| UnaryOperator | |
| UpdateExpression | foo++ |
| UpdateOperator | |
| VariableDeclaration | const answer = 42 |
| VariableDeclarator | const foo = 'bar' |
| WhileStatement | while (true) {} |
| WithStatement | with (foo) {} |
| YieldExpression | function* foo() { yield bar } |
Optimizations
How can you optimize an abstract syntax tree?
Abstract syntax tree is a tree-like structure that represents your program. The program is interpreted at some point, e.g. in your browser. Everything takes time, and the same applies to the interpretation. Some of the operations, e.g. adding numbers can be done at compile time, so that the interpreter has less work to do. Having less work to do means that your program will run faster.
Usage
const { binaryExpressionReduction } = require("abstract-syntax-tree")
What optimization techniques are available?
binaryExpressionReduction
const number = 2 + 2
In the example above we have added two numbers. We could optimize the code by:
const number = 4
The tree would be translated from:
{
"type": "BinaryExpression",
"operator": "+",
"left": { "type": "Literal", "value": 2 },
"right": { "type": "Literal", "value": 2 }
}
to
{ "type": "Literal", "value": 4 }
ifStatementRemoval
if (true) {
console.log("foo")
} else {
console.log("bar")
}
It seems that we'll only enter the true path. We can simplify the code to:
console.log("foo")
The tree would be translated from:
{
"type": "IfStatement",
"test": {
"type": "Literal",
"value": true
},
"consequent": {
"type": "BlockStatement",
"body": [
{
"type": "ExpressionStatement",
"expression": {
"type": "CallExpression",
"callee": {
"type": "MemberExpression",
"object": {
"type": "Identifier",
"name": "console"
},
"property": {
"type": "Identifier",
"name": "log"
},
"computed": false
},
"arguments": [
{
"type": "Literal",
"value": "foo"
}
]
}
}
]
},
"alternate": {
"type": "BlockStatement",
"body": [
{
"type": "ExpressionStatement",
"expression": {
"type": "CallExpression",
"callee": {
"type": "MemberExpression",
"object": {
"type": "Identifier",
"name": "console"
},
"property": {
"type": "Identifier",
"name": "log"
},
"computed": false
},
"arguments": [
{
"type": "Literal",
"value": "bar"
}
]
}
}
]
}
}
to:
{
"type": "CallExpression",
"callee": {
"type": "MemberExpression",
"object": {
"type": "Identifier",
"name": "console"
},
"property": {
"type": "Identifier",
"name": "log"
},
"computed": false
},
"arguments": [
{
"type": "Literal",
"value": "foo"
}
]
}
negationOperatorRemoval
if (!(foo === bar)) {
console.log("foo")
}
It seems that our negation operator could be a part of the condition inside the brackets.
if (foo !== bar) {
console.log("foo")
}
The tree would be translated from:
{
"type": "UnaryExpression",
"operator": "!",
"prefix": true,
"argument": {
"type": "BinaryExpression",
"left": {
"type": "Identifier",
"name": "foo"
},
"operator": "===",
"right": {
"type": "Identifier",
"name": "bar"
}
}
}
to
{
"type": "BinaryExpression",
"left": {
"type": "Identifier",
"name": "foo"
},
"operator": "!==",
"right": {
"type": "Identifier",
"name": "bar"
}
}
logicalExpressionReduction
const foo = "bar" || "baz"
The first value is truthy so it's safe to simplify the code.
const foo = "bar"
The tree would be translated from:
{
"type": "LogicalExpression",
"left": {
"type": "Literal",
"value": "bar"
},
"operator": "||",
"right": {
"type": "Literal",
"value": "baz"
}
}
To:
{
"type": "Literal",
"value": "bar"
}
ternaryOperatorReduction
const foo = true ? "bar" : "baz"
Given a known value of the conditional expression it's possible to get the right value immediately.
const foo = "bar"
The tree would be translated from:
{
"type": "ConditionalExpression",
"test": {
"type": "Literal",
"value": true
},
"consequent": {
"type": "Literal",
"value": "bar"
},
"alternate": {
"type": "Literal",
"value": "baz"
}
}
To:
{
"type": "Literal",
"value": "bar"
}
typeofOperatorReduction
const foo = typeof "bar"
It's possible to determine the type of some variables during analysis.
const foo = "string"
The tree would be translated from:
{
"type": "UnaryExpression",
"operator": "typeof",
"prefix": true,
"argument": {
"type": "Literal",
"value": "foo"
}
}
To:
{
"type": "Literal",
"value": "string"
}
memberExpressionReduction
const foo = { bar: "baz" }.bar
Given an inlined object expression it's possible to retrieve the value immediately.
const foo = "baz"
The tree would be translated from:
{
"type": "MemberExpression",
"object": {
"type": "ObjectExpression",
"properties": [
{
"type": "Property",
"method": false,
"shorthand": false,
"computed": false,
"key": {
"type": "Identifier",
"name": "bar"
},
"value": {
"type": "Literal",
"value": "baz"
},
"kind": "init"
}
]
},
"property": {
"type": "Identifier",
"name": "baz"
},
"computed": false
}
To:
{
"type": "Literal",
"value": "baz"
}
Browser
The library is not intended to work inside of a browser. This might change in the future, but it's a bigger lift, pretty time consuming. For now, consider exposing and using an API endpoint instead.
Maintainers
Contributing
All contributions are highly appreciated! Open an issue or a submit PR.
The lib follows the tdd approach and is expected to have a high code coverage. Please follow the Contributor Covenant Code of Conduct.
License
MIT buxlabs