Skip to main content

Overview

Full Moon provides two visitor traits for traversing AST nodes:
  • Visitor - For read-only traversal and analysis
  • VisitorMut - For modifying the AST during traversal
Visitors automatically recurse through the entire AST, calling your methods for each node type.

Read-Only Visitors

Use the Visitor trait when you want to analyze code without modifying it:
use full_moon::ast::*;
use full_moon::visitors::Visitor;

#[derive(Default)]
struct LocalVariableVisitor {
    names: Vec<String>,
}

impl Visitor for LocalVariableVisitor {
    fn visit_local_assignment(&mut self, local: &LocalAssignment) {
        // Collect all local variable names
        self.names.extend(
            local.names().iter().map(|name| name.token().to_string())
        );
    }
}

Using a Visitor

use full_moon::{parse, visitors::Visitor};

let ast = parse("local x = 1; local y, z = 2, 3")?;

let mut visitor = LocalVariableVisitor::default();
visitor.visit_ast(&ast);

assert_eq!(visitor.names, vec!["x", "y", "z"]);

Visitor Methods

Every AST node type has two visitor methods:
impl Visitor for MyVisitor {
    // Called when entering the node
    fn visit_if(&mut self, if_stmt: &If) {
        println!("Found if statement");
    }
    
    // Called when exiting the node (after children)
    fn visit_if_end(&mut self, if_stmt: &If) {
        println!("Finished if statement");
    }
}
The _end methods are called after all child nodes have been visited, useful for scope tracking or cleanup.

Analyzing Code Patterns

Counting Function Calls

use full_moon::ast::*;
use full_moon::visitors::Visitor;

#[derive(Default)]
struct FunctionCallCounter {
    count: usize,
}

impl Visitor for FunctionCallCounter {
    fn visit_function_call(&mut self, _call: &FunctionCall) {
        self.count += 1;
    }
}

Finding Specific Patterns

use full_moon::ast::*;
use full_moon::visitors::Visitor;

struct GlobalAssignmentFinder {
    globals: Vec<String>,
}

impl Visitor for GlobalAssignmentFinder {
    fn visit_assignment(&mut self, assignment: &Assignment) {
        for var in assignment.variables() {
            if let Var::Name(name) = var {
                self.globals.push(name.token().to_string());
            }
        }
    }
}

Tracking Scope

Use _end methods to maintain scope:
use full_moon::ast::*;
use full_moon::visitors::Visitor;

struct ScopeTracker {
    depth: usize,
    max_depth: usize,
}

impl Visitor for ScopeTracker {
    fn visit_block(&mut self, _block: &Block) {
        self.depth += 1;
        self.max_depth = self.max_depth.max(self.depth);
    }
    
    fn visit_block_end(&mut self, _block: &Block) {
        self.depth -= 1;
    }
}

Mutable Visitors

Use VisitorMut to modify the AST during traversal:
use full_moon::ast::*;
use full_moon::visitors::VisitorMut;

struct VariableRenamer {
    from: String,
    to: String,
}

impl VisitorMut for VariableRenamer {
    fn visit_token_reference(&mut self, token: TokenReference) -> TokenReference {
        if token.token().to_string() == self.from {
            TokenReference::new(
                token.leading_trivia().cloned().collect(),
                Token::new(TokenType::Identifier { 
                    identifier: self.to.clone().into() 
                }),
                token.trailing_trivia().cloned().collect(),
            )
        } else {
            token
        }
    }
}

Using a Mutable Visitor

use full_moon::{parse, visitors::VisitorMut};

let ast = parse("local x = 1; print(x)")?;

let mut renamer = VariableRenamer {
    from: "x".to_string(),
    to: "y".to_string(),
};

let modified_ast = renamer.visit_ast(ast);
assert_eq!(modified_ast.to_string(), "local y = 1; print(y)");

Advanced Patterns

Conditional Transformations

use full_moon::ast::*;
use full_moon::visitors::VisitorMut;

struct NumberDoubler;

impl VisitorMut for NumberDoubler {
    fn visit_expression(&mut self, expr: Expression) -> Expression {
        match expr {
            Expression::Number(token) => {
                // Parse the number, double it
                if let Ok(num) = token.token().to_string().parse::<i64>() {
                    Expression::Number(
                        TokenReference::new(
                            token.leading_trivia().cloned().collect(),
                            Token::new(TokenType::Number { 
                                text: (num * 2).to_string().into() 
                            }),
                            token.trailing_trivia().cloned().collect(),
                        )
                    )
                } else {
                    Expression::Number(token)
                }
            }
            _ => expr,
        }
    }
}

Building Context During Traversal

use full_moon::ast::*;
use full_moon::visitors::VisitorMut;
use std::collections::HashSet;

struct UnusedRemover {
    defined: HashSet<String>,
    used: HashSet<String>,
}

impl VisitorMut for UnusedRemover {
    fn visit_local_assignment(&mut self, local: LocalAssignment) -> LocalAssignment {
        // Track defined variables
        for name in local.names() {
            self.defined.insert(name.token().to_string());
        }
        local
    }
    
    fn visit_var(&mut self, var: Var) -> Var {
        // Track used variables
        if let Var::Name(name) = &var {
            self.used.insert(name.token().to_string());
        }
        var
    }
}

Working with Tokens

Visitors can also hook into individual token types:
use full_moon::tokenizer::Token;
use full_moon::visitors::Visitor;

struct CommentCollector {
    comments: Vec<String>,
}

impl Visitor for CommentCollector {
    fn visit_single_line_comment(&mut self, token: &Token) {
        self.comments.push(token.to_string());
    }
    
    fn visit_multi_line_comment(&mut self, token: &Token) {
        self.comments.push(token.to_string());
    }
}

Visitor Trait Methods

Full Moon generates visitor methods for all AST node types:
  • visit_stmt / visit_stmt_end
  • visit_assignment / visit_assignment_end
  • visit_local_assignment / visit_local_assignment_end
  • visit_function_declaration / visit_function_declaration_end
  • visit_if / visit_if_end
  • visit_while / visit_while_end
  • visit_repeat / visit_repeat_end
  • visit_numeric_for / visit_numeric_for_end
  • visit_generic_for / visit_generic_for_end
  • visit_expression / visit_expression_end
  • visit_function_call / visit_function_call_end
  • visit_table_constructor / visit_table_constructor_end
  • visit_binary_op / visit_binary_op_end
  • visit_unary_op / visit_unary_op_end
  • visit_token
  • visit_identifier
  • visit_number
  • visit_string_literal
  • visit_whitespace
  • visit_single_line_comment
  • visit_multi_line_comment

Luau-Specific Visitors

With the luau feature enabled, additional visitors are available:
#[cfg(feature = "luau")]
impl Visitor for MyVisitor {
    fn visit_type_declaration(&mut self, type_decl: &TypeDeclaration) {
        println!("Found type: {}", type_decl.type_name());
    }
    
    fn visit_if_expression(&mut self, if_expr: &IfExpression) {
        println!("Found if expression");
    }
    
    fn visit_interpolated_string(&mut self, interp: &InterpolatedString) {
        println!("Found interpolated string");
    }
}

Performance Tips

Selective Visiting

Only implement visitor methods for nodes you care about - others will be traversed automatically.

Avoid Cloning

In Visitor (read-only), nodes are passed by reference. Avoid unnecessary cloning.

Batch Modifications

For complex transformations, collect changes in first pass, apply in second pass.

Early Exit

Use custom flags to stop traversal early when you’ve found what you need.

Common Use Cases

Linting

use full_moon::visitors::Visitor;

struct LintVisitor {
    errors: Vec<String>,
}

impl Visitor for LintVisitor {
    fn visit_if(&mut self, if_stmt: &If) {
        // Check for empty if blocks
        if if_stmt.block().stmts().next().is_none() {
            self.errors.push("Empty if block".to_string());
        }
    }
}

Code Metrics

use full_moon::visitors::Visitor;

#[derive(Default)]
struct ComplexityCalculator {
    complexity: usize,
}

impl Visitor for ComplexityCalculator {
    fn visit_if(&mut self, _: &If) {
        self.complexity += 1;
    }
    
    fn visit_while(&mut self, _: &While) {
        self.complexity += 1;
    }
    
    fn visit_generic_for(&mut self, _: &GenericFor) {
        self.complexity += 1;
    }
}

Next Steps

Static Analysis Example

Build a complete static analysis tool

AST API Reference

Full visitor trait documentation

Build docs developers (and LLMs) love