SWC's AST Structure and Node Types: A Complete Guide for Tooling

SWC's AST is a strongly-typed, enum-based tree defined in the swc_ecma_ast crate where every syntax element—from statements to expressions—exists as a distinct enum variant, enabling compile-time safe traversal and transformation via visitor patterns.

SWC's Abstract Syntax Tree (AST) serves as the foundation for the compiler's JavaScript and TypeScript transformations. Defined primarily in the swc_ecma_ast crate within the swc-project/swc repository, this tree uses Rust enums to represent every syntactic construct, providing tooling authors with zero-cost abstractions and exhaustive pattern matching against the SWC's AST structure.

Core Node Types in SWC's AST Structure

Program and ModuleItem: The Root Hierarchy

The AST structure begins with the Program enum defined in crates/swc_ecma_ast/src/module.rs at line 11. This top-level container holds either a Script or a Module, representing the two execution contexts in JavaScript. Immediately below, the ModuleItem enum (line 108) represents items that can appear at the top level of a module, with variants for Stmt (statements) or ModuleDecl (module declarations like imports and exports).

Statements and Declarations

The Stmt enum in crates/swc_ecma_ast/src/stmt.rs (line 39) defines over 40 variants covering all possible statements, including Block, Empty, Debugger, and Expr. For declarations, the Decl enum in crates/swc_ecma_ast/src/decl.rs (line 18) provides variants such as FnDecl, ClassDecl, and VarDecl, alongside TypeScript-specific declarations.

Expressions and Patterns

Expression nodes are defined in the Expr enum located in crates/swc_ecma_ast/src/expr.rs (line 33), containing more than 80 variants for literals, binary operations, function calls, and JSX elements. For destructuring and binding patterns, the Pat enum in crates/swc_ecma_ast/src/pat.rs (line 16) includes ObjectPat, ArrayPat, and AssignPat.

TypeScript and JSX Extensions

SWC's AST structure provides first-class support for TypeScript through the TsType enum and auxiliary types in crates/swc_ecma_ast/src/typescript.rs (line 312), covering type literals, interfaces, and unions. JSX support is implemented in crates/swc_ecma_ast/src/jsx.rs (line 18) with JSXElement, JSXAttr, and JSXExpr enums.

Classes, Properties, and Operators

Class definitions use the Class struct and ClassMember enum in crates/swc_ecma_ast/src/class.rs (line 73). Object properties are represented by the Prop enum in crates/swc_ecma_ast/src/prop.rs (line 18), which includes KeyValueProp, GetterProp, SetterProp, and MethodProp. Operator tokens are typed as enums in crates/swc_ecma_ast/src/operators.rs (line 18), including BinaryOp, AssignOp, UnaryOp, and UpdateOp.

How SWC Builds the AST Structure

The AstNode Macro and Span Generation

Every struct in the AST structure is annotated with #[ast_node(...)], a procedural macro that automatically generates the Span field and implements Clone, PartialEq, and the Visit traits required by the visitor infrastructure. This macro ensures that every node carries source location information while reducing boilerplate code.

Tagged Enums for Serialization

All enums in the AST structure carry #[tag("NodeType")] attributes, which SWC uses for internal AST serialization when emitting JSON for debugging. For example, the Stmt enum definition in crates/swc_ecma_ast/src/stmt.rs uses these tags to map Rust variants to standard JavaScript node types:

#[ast_node(no_clone)]
#[derive(Eq, Hash, Is, EqIgnoreSpan)]
pub enum Stmt {
    #[tag("BlockStatement")]
    Block(BlockStmt),

    #[tag("EmptyStatement")]
    Empty(EmptyStmt),

    #[tag("DebuggerStatement")]
    Debugger(DebuggerStmt),

    #[tag("ExpressionStatement")]
    Expr(ExprStmt),
}

This design provides zero-cost abstraction—matching on enums compiles to simple integer switches avoiding dynamic dispatch—while offering static guarantees that all variants are handled at compile time.

Working with SWC's AST in Rust Tooling

Parsing JavaScript into AST Nodes

The swc_ecma_parser crate constructs the AST structure from source code. The Parser::new function accepts a Syntax configuration and StringInput to produce a Module or Script root node:

use swc_common::{SourceMap, FileName, sync::Lrc};
use swc_ecma_parser::{Parser, StringInput, Syntax, EsConfig};

fn parse_module(src: &str) -> swc_ecma_ast::Module {
    let cm: Lrc<SourceMap> = Default::default();
    let fm = cm.new_source_file(FileName::Custom("input.js".into()), src.into());

    let mut parser = Parser::new(
        Syntax::Es(EsConfig {
            jsx: true,
            ..Default::default()
        }),
        StringInput::from(&*fm),
        None,
    );

    parser
        .parse_module()
        .expect("failed to parse module")
}

Traversing with the Visit Trait

Tooling can traverse the AST structure using the Visit trait from swc_ecma_visit. The visit_with method dispatches to type-specific methods such as visit_ident or visit_fn_decl:

use swc_ecma_ast::*;
use swc_ecma_visit::{Visit, VisitWith};

struct IdentifierCollector {
    pub ids: Vec<String>,
}

impl Visit for IdentifierCollector {
    fn visit_ident(&mut self, ident: &Ident, _: &dyn Node) {
        self.ids.push(ident.sym.to_string());
    }
}

fn collect_idents(module: &Module) -> Vec<String> {
    let mut visitor = IdentifierCollector { ids: vec![] };
    module.visit_with(&mut visitor);
    visitor.ids
}

Transforming with the Fold Trait

Mutable transformations use the Fold trait, which creates a new AST structure with modified nodes. The fold_with method applies a folder across the entire tree:

use swc_ecma_ast::*;
use swc_ecma_visit::{Fold, FoldWith};

struct VarToLet;

impl Fold for VarToLet {
    fn fold_var_decl(&mut self, mut decl: VarDecl) -> VarDecl {
        decl.kind = VarDeclKind::Let;
        decl
    }
}

fn transform(module: Module) -> Module {
    module.fold_with(&mut VarToLet)
}

Serializing to JSON for Debugging

When the serde feature is enabled, the entire AST structure supports serialization. This is useful for inspecting the tree during development:

use swc_ecma_ast::*;
use serde_json::to_string_pretty;

fn ast_to_json<T: serde::Serialize>(node: &T) -> String {
    to_string_pretty(node).expect("serialization failed")
}

// Example usage:
// let module = parse_module("function foo(){ return 42; }");
// println!("{}", ast_to_json(&module));

Essential Source Files for AST Manipulation

The following files in the swc-project/swc repository constitute the public surface for SWC's AST structure:

Summary

  • SWC's AST structure uses Rust enums to represent all JavaScript and TypeScript syntax, providing compile-time safety and zero-cost pattern matching.
  • The Program and ModuleItem enums serve as the root containers, while Stmt, Expr, Decl, and Pat enums categorize specific syntax elements.
  • The #[ast_node] macro generates common traits and span information, while #[tag(...)] attributes enable JSON serialization.
  • Tooling interacts with the AST through the Visit trait (for reading) and Fold trait (for transforming), defined in the swc_visit crate.
  • All AST types support serde serialization when the feature is enabled, facilitating debugging and interoperability.

Frequently Asked Questions

What makes SWC's AST structure different from Babel's?

SWC's AST uses Rust's enum system to enforce type safety at compile time, whereas Babel uses a loose object-based tree. This means SWC visitors must handle all enum variants, preventing runtime errors, and pattern matching optimizes to integer switches rather than property lookups.

How does pattern matching work on SWC AST nodes?

Pattern matching uses Rust's match expressions on the enum variants. For example, matching on a Stmt can destructure it into specific variants like Stmt::Block(block) or Stmt::Expr(expr), allowing exhaustive handling of all 40+ statement types defined in stmt.rs.

Can I use SWC's AST for TypeScript type-checking?

While the AST structure includes comprehensive TypeScript nodes in typescript.rs (line 312), SWC itself is a transpiler, not a type checker. The AST provides the syntax tree for type annotations, but semantic type checking requires additional symbol resolution logic beyond the scope of the core AST.

What is the performance cost of SWC's visitor pattern?

The visitor pattern incurs zero runtime overhead compared to manual traversal. The Visit and Fold traits use static dispatch, and enum matching compiles to direct jumps. According to the source implementation in swc_visit, this design avoids the virtual table lookups common in object-oriented AST implementations.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →