Skip to main content
The visitor pattern in full-moon allows you to traverse the AST and inspect specific node types without modifying the code. This is perfect for building static analysis tools, linters, and code quality checkers.

Overview

Visitors work by implementing the Visitor trait and defining methods for each AST node type you want to inspect. Full-moon automatically traverses the tree and calls your methods when it encounters matching nodes.
The Visitor trait provides immutable access to nodes, making it ideal for analysis without modification. For transformations, use VisitorMut instead.

Example: Unused Variable Detector

This example builds a static analyzer that detects potentially unused local variables by tracking assignments and usages.
use full_moon::{
    ast::{self, Ast},
    parse,
    visitors::Visitor,
};
use std::collections::{HashMap, HashSet};

#[derive(Default)]
struct UnusedVariableDetector {
    // Track all local variable declarations
    declared: HashMap<String, usize>,
    // Track all variable usages
    used: HashSet<String>,
    // Store warnings
    warnings: Vec<String>,
}

impl Visitor for UnusedVariableDetector {
    // Track local variable declarations
    fn visit_local_assignment(&mut self, local: &ast::LocalAssignment) {
        for name in local.names() {
            let var_name = name.token().to_string();
            let line = name.start_position()
                .map(|pos| pos.line())
                .unwrap_or(0);
            self.declared.insert(var_name, line);
        }
    }

    // Track variable usage
    fn visit_var(&mut self, var: &ast::Var) {
        match var {
            ast::Var::Name(name) => {
                self.used.insert(name.token().to_string());
            }
            ast::Var::Expression(expr) => {
                // Handle complex expressions like x.y
                if let ast::Prefix::Name(name) = expr.prefix() {
                    self.used.insert(name.token().to_string());
                }
            }
        }
    }

    // Check at the end of AST traversal
    fn visit_ast(&mut self, ast: &Ast) {
        // First traverse to collect data
        ast.nodes().visit(self);
        ast.eof().visit(self);

        // Then analyze
        for (var_name, line) in &self.declared {
            if !self.used.contains(var_name) {
                self.warnings.push(format!(
                    "Line {}: Variable '{}' is declared but never used",
                    line, var_name
                ));
            }
        }
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let code = r#"
        local unused_var = 42
        local used_var = 10
        local another = 5
        
        print(used_var)
        print(another + 2)
    "#;

    let ast = parse(code)?;
    let mut detector = UnusedVariableDetector::default();
    detector.visit_ast(&ast);

    for warning in detector.warnings {
        println!("⚠️  {}", warning);
    }

    Ok(())
}
Output:
⚠️  Line 2: Variable 'unused_var' is declared but never used

Example: Function Complexity Analyzer

This analyzer calculates cyclomatic complexity by counting decision points in functions.
use full_moon::{
    ast::{self, Ast},
    parse,
    visitors::Visitor,
    node::Node,
};

#[derive(Default)]
struct ComplexityAnalyzer {
    current_function: Option<String>,
    complexity: HashMap<String, usize>,
    depth: usize,
}

impl ComplexityAnalyzer {
    fn increment_complexity(&mut self) {
        if let Some(func_name) = &self.current_function {
            *self.complexity.entry(func_name.clone()).or_insert(1) += 1;
        }
    }
}

impl Visitor for ComplexityAnalyzer {
    fn visit_function_declaration(&mut self, func: &ast::FunctionDeclaration) {
        // Get function name
        let name = func.name()
            .names()
            .iter()
            .map(|n| n.token().to_string())
            .collect::<Vec<_>>()
            .join(".");
        
        self.current_function = Some(name.clone());
        self.complexity.insert(name, 1); // Base complexity
    }

    fn visit_function_declaration_end(&mut self, _func: &ast::FunctionDeclaration) {
        self.current_function = None;
    }

    fn visit_local_function(&mut self, func: &ast::LocalFunction) {
        let name = func.name().token().to_string();
        self.current_function = Some(name.clone());
        self.complexity.insert(name, 1);
    }

    fn visit_local_function_end(&mut self, _func: &ast::LocalFunction) {
        self.current_function = None;
    }

    // Each if adds complexity
    fn visit_if(&mut self, _: &ast::If) {
        self.increment_complexity();
    }

    // Each elseif adds complexity
    fn visit_else_if(&mut self, _: &ast::ElseIf) {
        self.increment_complexity();
    }

    // Loops add complexity
    fn visit_while(&mut self, _: &ast::While) {
        self.increment_complexity();
    }

    fn visit_repeat(&mut self, _: &ast::Repeat) {
        self.increment_complexity();
    }

    fn visit_numeric_for(&mut self, _: &ast::NumericFor) {
        self.increment_complexity();
    }

    fn visit_generic_for(&mut self, _: &ast::GenericFor) {
        self.increment_complexity();
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let code = r#"
        function complexFunction(x)
            if x > 10 then
                return true
            elseif x > 5 then
                for i = 1, x do
                    if i % 2 == 0 then
                        print(i)
                    end
                end
            else
                while x < 10 do
                    x = x + 1
                end
            end
            return false
        end

        function simpleFunction()
            return 42
        end
    "#;

    let ast = parse(code)?;
    let mut analyzer = ComplexityAnalyzer::default();
    analyzer.visit_ast(&ast);

    println!("Function Complexity Analysis:\n");
    for (func_name, complexity) in analyzer.complexity {
        let status = if complexity > 10 {
            "🔴 High"
        } else if complexity > 5 {
            "🟡 Medium"
        } else {
            "🟢 Low"
        };
        
        println!("  {} - Complexity: {} {}", func_name, complexity, status);
    }

    Ok(())
}
Output:
Function Complexity Analysis:

  complexFunction - Complexity: 6 🟡 Medium
  simpleFunction - Complexity: 1 🟢 Low

Example: Comment Coverage Analyzer

This analyzer checks if functions have documentation comments.
use full_moon::{
    ast::{self, Ast},
    parse,
    tokenizer::Token,
    visitors::Visitor,
    node::Node,
};

#[derive(Default)]
struct CommentAnalyzer {
    functions: Vec<FunctionInfo>,
    last_comment_line: Option<usize>,
}

struct FunctionInfo {
    name: String,
    line: usize,
    has_doc_comment: bool,
}

impl Visitor for CommentAnalyzer {
    fn visit_single_line_comment(&mut self, token: &Token) {
        // Track the last comment we saw
        if let Some(pos) = token.start_position() {
            self.last_comment_line = Some(pos.line());
        }
    }

    fn visit_multi_line_comment(&mut self, token: &Token) {
        if let Some(pos) = token.start_position() {
            self.last_comment_line = Some(pos.line());
        }
    }

    fn visit_function_declaration(&mut self, func: &ast::FunctionDeclaration) {
        let name = func.name()
            .names()
            .iter()
            .map(|n| n.token().to_string())
            .collect::<Vec<_>>()
            .join(".");
        
        let func_line = func.start_position()
            .map(|pos| pos.line())
            .unwrap_or(0);
        
        // Check if there's a comment within 2 lines before the function
        let has_doc_comment = self.last_comment_line
            .map(|comment_line| {
                func_line.saturating_sub(comment_line) <= 2
            })
            .unwrap_or(false);
        
        self.functions.push(FunctionInfo {
            name,
            line: func_line,
            has_doc_comment,
        });
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let code = r#"
        -- This function calculates the sum
        -- of two numbers
        function add(a, b)
            return a + b
        end

        function undocumented(x)
            return x * 2
        end

        -- Documented function
        function multiply(a, b)
            return a * b
        end
    "#;

    let ast = parse(code)?;
    let mut analyzer = CommentAnalyzer::default();
    analyzer.visit_ast(&ast);

    let documented = analyzer.functions.iter()
        .filter(|f| f.has_doc_comment)
        .count();
    let total = analyzer.functions.len();
    let coverage = (documented as f64 / total as f64) * 100.0;

    println!("Documentation Coverage: {:.1}%\n", coverage);
    
    for func in &analyzer.functions {
        let status = if func.has_doc_comment { "✓" } else { "✗" };
        println!("  {} {} (line {})", status, func.name, func.line);
    }

    Ok(())
}
Output:
Documentation Coverage: 66.7%

  ✓ add (line 4)
  ✗ undocumented (line 8)
  ✓ multiply (line 13)

Key Concepts

1

Implement the Visitor trait

Override methods for specific node types you want to analyze (e.g., visit_local_assignment, visit_if, visit_function_call).
2

Maintain state

Use struct fields to track information as you traverse the AST (e.g., declared variables, complexity counters).
3

Use _end methods

For scope-based analysis, use visit_*_end methods to detect when you exit a node (e.g., visit_function_declaration_end).
4

Call visit_ast

Invoke visitor.visit_ast(&ast) to start traversal. The visitor will automatically recurse through all nodes.

Best Practices

  • Keep visitors focused: Create separate visitors for different analysis tasks rather than one monolithic visitor
  • Use HashSet/HashMap: For efficient lookups when tracking variable names, function names, etc.
  • Leverage Node trait: Use start_position() and end_position() methods to report accurate line numbers
  • Handle edge cases: Remember that not all nodes may have position information in partial ASTs

Next Steps

Code Formatter

Learn how to preserve formatting while transforming code

AST Transformation

Modify code by transforming the AST with VisitorMut

Build docs developers (and LLMs) love