Abstract Syntax Trees (AST) in JavaScript Guide

Understand Abstract Syntax Trees in JavaScript. Covers AST node types and structure, parsing with Acorn and Babel, AST traversal and visitor patterns, code transformation with AST manipulation, building custom linters, and code generation from modified trees.

JavaScriptadvanced
18 min read

Abstract Syntax Trees represent JavaScript source code as a structured tree of nodes. Every tool in the JavaScript ecosystem, from Babel transpilers to ESLint linters, works by parsing code into an AST, transforming it, and generating new code. This guide covers how ASTs work and how to use them.

For how V8 uses ASTs in its compilation pipeline, see JavaScript Parsing and Compilation: Full Guide.

AST Node Structure

Every JavaScript construct maps to an AST node type defined by the ESTree specification. Each node has a type property and additional properties specific to that construct.

javascriptjavascript
// Source code:
const greeting = "hello";
 
// AST representation (ESTree format):
const ast = {
  type: "Program",
  body: [
    {
      type: "VariableDeclaration",
      kind: "const",
      declarations: [
        {
          type: "VariableDeclarator",
          id: {
            type: "Identifier",
            name: "greeting",
          },
          init: {
            type: "Literal",
            value: "hello",
            raw: '"hello"',
          },
        },
      ],
    },
  ],
  sourceType: "module",
};
 
// More complex: function declaration
// function add(a, b) { return a + b; }
const functionAST = {
  type: "FunctionDeclaration",
  id: { type: "Identifier", name: "add" },
  params: [
    { type: "Identifier", name: "a" },
    { type: "Identifier", name: "b" },
  ],
  body: {
    type: "BlockStatement",
    body: [
      {
        type: "ReturnStatement",
        argument: {
          type: "BinaryExpression",
          operator: "+",
          left: { type: "Identifier", name: "a" },
          right: { type: "Identifier", name: "b" },
        },
      },
    ],
  },
};
 
// Arrow function: (x) => x * 2
const arrowAST = {
  type: "ArrowFunctionExpression",
  params: [{ type: "Identifier", name: "x" }],
  body: {
    type: "BinaryExpression",
    operator: "*",
    left: { type: "Identifier", name: "x" },
    right: { type: "Literal", value: 2 },
  },
  expression: true, // concise body (no braces)
};

Parsing with Acorn

javascriptjavascript
// Acorn is a small, fast JavaScript parser used by many tools
import * as acorn from "acorn";
 
// Parse source code into AST
const source = `
function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}
`;
 
const ast = acorn.parse(source, {
  ecmaVersion: 2025,
  sourceType: "module",
  locations: true, // Include line/column info
});
 
// The AST preserves all structural information
console.log(ast.type); // "Program"
console.log(ast.body[0].type); // "FunctionDeclaration"
console.log(ast.body[0].id.name); // "fibonacci"
console.log(ast.body[0].params[0].name); // "n"
 
// Accessing the if statement
const ifStmt = ast.body[0].body.body[0];
console.log(ifStmt.type); // "IfStatement"
console.log(ifStmt.test.type); // "BinaryExpression"
console.log(ifStmt.test.operator); // "<="
 
// Location information
console.log(ifStmt.loc.start); // { line: 3, column: 2 }
console.log(ifStmt.loc.end);   // { line: 3, column: 24 }
 
// Parse with comments
const astWithComments = acorn.parse(source, {
  ecmaVersion: 2025,
  onComment: (isBlock, text, start, end) => {
    console.log(`${isBlock ? "Block" : "Line"} comment: ${text}`);
  },
});
 
// Handling parse errors
try {
  acorn.parse("function { broken", { ecmaVersion: 2025 });
} catch (err) {
  console.log(err.message); // "Unexpected token (1:9)"
  console.log(err.pos);     // 9
  console.log(err.loc);     // { line: 1, column: 9 }
}

AST Traversal

javascriptjavascript
// Walking an AST to visit every node
 
// Simple recursive traversal
function traverse(node, visitor) {
  if (!node || typeof node !== "object") return;
 
  // Call visitor for this node type
  const handler = visitor[node.type];
  if (handler) handler(node);
 
  // Visit all child nodes
  for (const key of Object.keys(node)) {
    const child = node[key];
    if (Array.isArray(child)) {
      child.forEach((item) => traverse(item, visitor));
    } else if (child && typeof child === "object" && child.type) {
      traverse(child, visitor);
    }
  }
}
 
// Usage: Find all function names in a program
const functionNames = [];
traverse(ast, {
  FunctionDeclaration(node) {
    functionNames.push(node.id.name);
  },
  ArrowFunctionExpression(node) {
    functionNames.push("(arrow)");
  },
});
 
// Advanced traversal with enter/leave phases
function walk(node, visitors) {
  if (!node || typeof node !== "object") return;
 
  const visitor = visitors[node.type];
  if (visitor?.enter) visitor.enter(node);
 
  for (const key of Object.keys(node)) {
    const child = node[key];
    if (Array.isArray(child)) {
      child.forEach((item) => walk(item, visitors));
    } else if (child && typeof child === "object" && child.type) {
      walk(child, visitors);
    }
  }
 
  if (visitor?.leave) visitor.leave(node);
}
 
// Track scope depth
let scopeDepth = 0;
walk(ast, {
  FunctionDeclaration: {
    enter(node) {
      scopeDepth++;
      console.log(`${" ".repeat(scopeDepth * 2)}Enter function: ${node.id.name}`);
    },
    leave(node) {
      console.log(`${" ".repeat(scopeDepth * 2)}Leave function: ${node.id.name}`);
      scopeDepth--;
    },
  },
  BlockStatement: {
    enter() { scopeDepth++; },
    leave() { scopeDepth--; },
  },
});

Code Transformation

javascriptjavascript
// Transforming AST nodes to modify or generate code
 
// Example: Convert var to const/let
function transformVarDeclarations(ast) {
  traverse(ast, {
    VariableDeclaration(node) {
      if (node.kind === "var") {
        // Check if any declarator is reassigned
        const names = node.declarations.map((d) => d.id.name);
        // Simple heuristic: use 'let' (a full implementation would
        // check all references in scope for reassignment)
        node.kind = "let";
      }
    },
  });
  return ast;
}
 
// Example: Add console.log to every function entry
function instrumentFunctions(ast) {
  traverse(ast, {
    FunctionDeclaration(node) {
      const logStatement = {
        type: "ExpressionStatement",
        expression: {
          type: "CallExpression",
          callee: {
            type: "MemberExpression",
            object: { type: "Identifier", name: "console" },
            property: { type: "Identifier", name: "log" },
          },
          arguments: [
            {
              type: "Literal",
              value: `Entering ${node.id.name}`,
            },
          ],
        },
      };
 
      // Insert at the beginning of the function body
      node.body.body.unshift(logStatement);
    },
  });
  return ast;
}
 
// Example: Dead code elimination
function removeDeadCode(ast) {
  traverse(ast, {
    IfStatement(node) {
      // Remove if(false) blocks
      if (node.test.type === "Literal" && node.test.value === false) {
        // Replace with the else block or empty statement
        if (node.alternate) {
          Object.assign(node, node.alternate);
        } else {
          node.type = "EmptyStatement";
          delete node.test;
          delete node.consequent;
          delete node.alternate;
        }
      }
    },
  });
  return ast;
}
 
// Example: Rename variables (simple scope-unaware version)
function renameIdentifier(ast, oldName, newName) {
  traverse(ast, {
    Identifier(node) {
      if (node.name === oldName) {
        node.name = newName;
      }
    },
  });
  return ast;
}

Code Generation

javascriptjavascript
// Generating source code from an AST
 
class CodeGenerator {
  #indent = 0;
 
  generate(node) {
    const method = this[`gen${node.type}`];
    if (!method) throw new Error(`Unknown node type: ${node.type}`);
    return method.call(this, node);
  }
 
  genProgram(node) {
    return node.body.map((stmt) => this.generate(stmt)).join("\n");
  }
 
  genVariableDeclaration(node) {
    const decls = node.declarations
      .map((d) => this.generate(d))
      .join(", ");
    return `${node.kind} ${decls};`;
  }
 
  genVariableDeclarator(node) {
    const id = this.generate(node.id);
    if (node.init) {
      return `${id} = ${this.generate(node.init)}`;
    }
    return id;
  }
 
  genFunctionDeclaration(node) {
    const params = node.params.map((p) => this.generate(p)).join(", ");
    const body = this.generate(node.body);
    return `function ${node.id.name}(${params}) ${body}`;
  }
 
  genBlockStatement(node) {
    this.#indent++;
    const pad = "  ".repeat(this.#indent);
    const body = node.body
      .map((stmt) => `${pad}${this.generate(stmt)}`)
      .join("\n");
    this.#indent--;
    const outerPad = "  ".repeat(this.#indent);
    return `{\n${body}\n${outerPad}}`;
  }
 
  genReturnStatement(node) {
    if (node.argument) {
      return `return ${this.generate(node.argument)};`;
    }
    return "return;";
  }
 
  genBinaryExpression(node) {
    const left = this.generate(node.left);
    const right = this.generate(node.right);
    return `${left} ${node.operator} ${right}`;
  }
 
  genIdentifier(node) { return node.name; }
 
  genLiteral(node) {
    if (typeof node.value === "string") return `"${node.value}"`;
    return String(node.value);
  }
 
  genIfStatement(node) {
    let code = `if (${this.generate(node.test)}) ${this.generate(node.consequent)}`;
    if (node.alternate) {
      code += ` else ${this.generate(node.alternate)}`;
    }
    return code;
  }
 
  genExpressionStatement(node) {
    return `${this.generate(node.expression)};`;
  }
 
  genCallExpression(node) {
    const callee = this.generate(node.callee);
    const args = node.arguments.map((a) => this.generate(a)).join(", ");
    return `${callee}(${args})`;
  }
 
  genMemberExpression(node) {
    return `${this.generate(node.object)}.${this.generate(node.property)}`;
  }
}
 
// Usage
const generator = new CodeGenerator();
const output = generator.generate(ast);
console.log(output);
// function fibonacci(n) {
//   if (n <= 1) return n;
//   return fibonacci(n - 1) + fibonacci(n - 2);
// }
AST ToolPurposeSpeedOutput
AcornParse JS to ESTree ASTFast (10MB/s)Standard ESTree
Babel ParserParse JS/TS/JSX to Babel ASTMediumExtended ESTree
EsprimaParse JS to ESTree ASTFastStandard ESTree
RecastParse with formatting preservationMediumFormatted code
AstringGenerate code from ESTree ASTFastSource code
EscodegenGenerate code from ESTree ASTMediumSource code
Rune AI

Rune AI

Key Insights

  • AST nodes follow the ESTree specification with a type property and construct-specific fields: Every JavaScript construct maps to a node type like FunctionDeclaration, BinaryExpression, or Identifier
  • Parsers like Acorn convert source text into AST trees with optional location tracking: Location info enables source maps, error reporting, and editor integration
  • Traversal visits every node using the visitor pattern with enter and leave phases: Visitors target specific node types while the traversal handles recursive tree walking
  • Code transformations modify AST nodes in place to change program behavior: Adding, removing, or replacing nodes lets tools transpile, instrument, or optimize code
  • Code generators convert modified ASTs back into formatted source text: The generator walks the tree and concatenates syntax strings with proper indentation and semicolons
RunePowered by Rune AI

Frequently Asked Questions

What is the ESTree specification?

ESTree is a community standard that defines the AST node types for JavaScript. It specifies that a `FunctionDeclaration` has `id`, `params`, and `body` properties, that a `BinaryExpression` has `operator`, `left`, and `right`, and so on. Most JavaScript parsers (Acorn, Esprima, Babel) produce ESTree-compatible ASTs, which means tools built on one parser often work with another. The spec lives at github.com/estree/estree.

How does Babel use ASTs for transpilation?

Babel parses source code into an AST, runs transform plugins that modify the tree, and generates new source code. Each plugin is a visitor that targets specific node types. For example, the arrow function plugin finds `ArrowFunctionExpression` nodes and replaces them with `FunctionExpression` nodes, binding `this` appropriately. Plugins run in order, each receiving the modified AST from the previous plugin.

Can I use ASTs to build my own ESLint rules?

Yes. ESLint rules are visitor functions that receive AST nodes and report problems. You define which node types to visit and check properties or patterns. For example, a "no-var" rule visits `VariableDeclaration` nodes and reports if `node.kind === "var"`. The AST traversal is handled by ESLint; you only write the check logic. Custom rules can be loaded as plugins without modifying ESLint itself.

How do source maps relate to ASTs?

Source maps connect generated code positions back to original source positions. During code generation, the generator tracks which AST node (with its original location) produced each piece of output. These mappings are encoded in the source map file. When debugging transpiled code, the browser uses the source map to show the original source instead of the generated output. AST location info (`node.loc`) is essential for accurate source maps.

Conclusion

Abstract Syntax Trees are the foundation of every JavaScript development tool. Parsers like Acorn convert source text into structured trees. Traversal functions visit every node for analysis. Transformations modify nodes to change code behavior. Code generators produce new source from modified trees. For how JavaScript engines use ASTs internally during compilation, see JavaScript Parsing and Compilation: Full Guide. For hidden classes that V8 derives from parsed code, explore V8 Hidden Classes in JavaScript: Full Tutorial.