How to Implement Custom Transformations Using SWC's Visitor and Fold Patterns

SWC provides two complementary AST traversal patterns—VisitMut for in-place mutations and Fold for producing new nodes—that let you transform JavaScript/TypeScript code by implementing traits generated for every AST node type.

The SWC compiler represents JavaScript and TypeScript code as a rich abstract syntax tree (AST). To manipulate this tree, the library offers two distinct traversal mechanisms defined in crates/swc_ecma_visit/src/generated.rs: the Visitor pattern for read-only inspection or in-place mutation, and the Fold pattern for creating entirely new AST nodes. These traits are automatically generated for every node type in the swc_ecma_ast crate, providing zero-cost abstractions for custom transformations.

Understanding SWC's AST Traversal Architecture

SWC's transformation system relies on code-generated traits that handle the boilerplate of tree walking, allowing you to focus only on the specific nodes you want to transform.

The Visitor Pattern (VisitMut)

The Visitor pattern is designed for read-only analysis or in-place mutation of existing nodes. Defined at line 7 of crates/swc_ecma_visit/src/generated.rs, the Visit trait provides read-only access, while VisitMut (defined in the same file) allows mutation of nodes in place.

The default implementation recursively traverses all children, calling the appropriate hook method for each node type. When you implement VisitMut, you override only the methods for node types you care about, leaving the recursive traversal to the generated boilerplate.

The Fold Pattern

The Fold pattern, defined at line 111 of crates/swc_ecma_visit/src/generated.rs, is designed for transformations that replace nodes entirely. Unlike VisitMut, which mutates nodes in place, Fold methods take ownership of a node and return a potentially different node of the same type.

This pattern is ideal when you need to drop nodes, replace them with different node types, or construct new AST structures (such as converting JSX elements into function calls).

When to Use Each Pattern

  • Use VisitMut when you need to modify properties of existing nodes without changing their location in the tree (e.g., renaming identifiers, adding annotations).
  • Use Fold when you need to replace, remove, or drastically restructure nodes (e.g., desugaring syntax, inlining constants).

Implementing In-Place Mutations with VisitMut

For transformations that modify existing nodes, implement the VisitMut trait. The trait is automatically generated for every AST node, with default implementations that recursively visit children.

The following example demonstrates a custom visitor that renames all identifiers matching a specific pattern:

use swc_common::{errors::Handler, SourceMap, FileName, sync::Lrc};
use swc_ecma_ast::*;
use swc_ecma_parser::{Parser, StringInput, Syntax, EsConfig};
use swc_ecma_visit::{VisitMut, VisitMutWith};

/// Visitor that renames every identifier named `old` to `new_name`.
struct Renamer {
    old: JsWord,
    new_name: JsWord,
}

impl VisitMut for Renamer {
    fn visit_mut_ident(&mut self, ident: &mut Ident) {
        if ident.sym == self.old {
            ident.sym = self.new_name.clone();
        }
        // Continue walking into possible child nodes (none for Ident)
        ident.visit_mut_children_with(self);
    }
}

fn main() {
    // 1️⃣ Parse some source.
    let cm: Lrc<SourceMap> = Default::default();
    let handler = Handler::with_tty_emitter(swc_common::errors::ColorConfig::Auto, true, false, Some(cm.clone()));
    let src = r#"function foo(old) { console.log(old); }"#;
    let mut parser = Parser::new(
        Syntax::Es(EsConfig { ..Default::default() }),
        StringInput::new(src, 0, 0),
        None,
    );
    let mut module = parser.parse_module().expect("failed to parse");

    // 2️⃣ Run the visitor.
    let mut renamer = Renamer {
        old: "old".into(),
        new_name: "renamed".into(),
    };
    module.visit_mut_with(&mut renamer);

    // 3️⃣ Emit the transformed code (using the builtin printer).
    use swc_ecma_codegen::{Emitter, text_writer::JsWriter};
    let mut buf = vec![];
    {
        let mut emitter = Emitter {
            cfg: swc_ecma_codegen::Config { ..Default::default() },
            cm: cm.clone(),
            comments: None,
            wr: JsWriter::new(cm.clone(), "\n", &mut buf, None),
        };
        emitter.emit_module(&module).unwrap();
    }
    println!("{}", String::from_utf8(buf).unwrap());
}

Key implementation details:

  • Override only the specific node methods you need (e.g., visit_mut_ident).
  • Always call visit_mut_children_with(self) to ensure nested structures are traversed.
  • Invoke the transformation using module.visit_mut_with(&mut renamer), a thin wrapper generated by the trait.

Creating New Nodes with Fold

When you need to replace nodes entirely or return different node types, use the Fold trait. This pattern consumes nodes and produces new ones, making it suitable for desugaring and structural transformations.

The following example replaces specific string literals with new values:

use swc_common::{SourceMap, FileName, sync::Lrc};
use swc_ecma_ast::*;
use swc_ecma_parser::{Parser, StringInput, Syntax, EsConfig};
use swc_ecma_visit::{Fold, FoldWith};

/// Fold that turns every string literal `"foo"` into `"bar"`.
struct StringReplacer;

impl Fold for StringReplacer {
    fn fold_str(&mut self, mut s: Str) -> Str {
        if s.value == "foo".into() {
            s.value = "bar".into();
        }
        // Recursively fold children (none for Str, but required by the macro)
        s.fold_children_with(self)
    }
}

fn main() {
    // Parse source.
    let cm: Lrc<SourceMap> = Default::default();
    let src = r#"const x = "foo";"#;
    let mut parser = Parser::new(
        Syntax::Es(EsConfig { ..Default::default() }),
        StringInput::new(src, 0, 0),
        None,
    );
    let module = parser.parse_module().expect("parse");

    // Apply the fold.
    let mut replacer = StringReplacer;
    let new_module = module.fold_with(&mut replacer);

    // Print result.
    use swc_ecma_codegen::{Emitter, text_writer::JsWriter};
    let mut buf = vec![];
    {
        let mut emitter = Emitter {
            cfg: swc_ecma_codegen::Config { ..Default::default() },
            cm: cm.clone(),
            comments: None,
            wr: JsWriter::new(cm.clone(), "\n", &mut buf, None),
        };
        emitter.emit_module(&new_module).unwrap();
    }
    println!("{}", String::from_utf8(buf).unwrap());
}

Critical differences from VisitMut:

  • Methods return the node type (e.g., fn fold_str(&mut self, s: Str) -> Str) rather than accepting mutable references.
  • Call fold_children_with(self) to continue traversal.
  • The transformation returns a new value: let new_module = module.fold_with(&mut replacer).

How the Traits Are Generated

The visitor and fold traits are not hand-written for each AST node. Instead, they are automatically generated by the code generator located at tools/generate-code/src/generators/visitor.rs. This generator reads the AST definitions in crates/swc_ecma_ast and emits the traversal logic in crates/swc_ecma_visit/src/generated.rs.

The generated code ensures that every field of every node is visited in the correct order. The VisitMut implementation delegates to VisitMutWith, while Fold uses FoldWith::fold_children_with, providing the entry points for the traversal logic.

Summary

  • SWC's visitor and fold patterns provide type-safe, zero-cost abstractions for AST transformations in Rust.
  • Use VisitMut (defined in crates/swc_ecma_visit/src/generated.rs at line 7) for in-place mutations like renaming identifiers or adding metadata.
  • Use Fold (defined at line 111 of the same file) when you need to replace nodes entirely or change AST structure.
  • Both traits are auto-generated for every AST node by tools/generate-code/src/generators/visitor.rs, ensuring complete coverage of the JavaScript/TypeScript grammar.
  • Always call the children traversal methods (visit_mut_children_with or fold_children_with) to ensure nested nodes are processed.
  • Entry points are module.visit_mut_with(&mut visitor) for mutation and module.fold_with(&mut folder) for replacement.

Frequently Asked Questions

When should I use Fold instead of VisitMut?

Use Fold when you need to replace a node with a different node, remove nodes from the tree, or change the AST structure (such as converting arrow functions to regular functions). Use VisitMut when you only need to modify properties of existing nodes in place, such as renaming identifiers or toggling flags on nodes. According to the SWC source code, Fold produces new nodes while VisitMut modifies existing ones.

How are the visitor traits generated for all AST nodes?

The traits are generated by the code generator in tools/generate-code/src/generators/visitor.rs. This tool reads the AST definitions from crates/swc_ecma_ast and outputs the complete visitor and fold implementations to crates/swc_ecma_visit/src/generated.rs. The generator creates the Visit, VisitMut, and Fold trait definitions along with the visit_mut_children_with and fold_children_with helper functions for every node type.

Can I combine VisitMut and Fold in the same transformation pipeline?

Yes. The two patterns interoperate cleanly. You can implement a Fold that internally creates a mutable visitor to inspect or modify specific nodes during the folding process, or use a visitor to gather information before running a fold pass. The swc_ecma_visit crate re-exports both traits to facilitate this composition.

Do these patterns work with TypeScript-specific AST nodes?

Yes. The code generator creates visitor and fold implementations for the entire AST, including TypeScript-specific nodes like TsMethodSignature or TsTypeAnnotation. When you parse TypeScript source using swc_ecma_parser with TypeScript syntax enabled, the resulting AST can be traversed using the same VisitMut and Fold traits, as they are implemented for all node types in the unified AST definition.

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 →