Learn how to analyze Lua code using full-moon’s visitor pattern to detect patterns, enforce coding standards, and find potential issues
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.
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.
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
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(())}