Understanding VisitorMut
TheVisitorMut trait is similar to Visitor, but instead of just inspecting nodes, it returns modified versions. Each visit_* method receives a node by value and returns a (potentially modified) node.
impl VisitorMut for MyTransformer {
fn visit_local_assignment(&mut self, node: ast::LocalAssignment) -> ast::LocalAssignment {
// Transform the node and return the modified version
node.with_names(modified_names)
}
}
VisitorMut consumes the original AST and returns a new one. Make sure you don’t need the original before transforming.Example: Function Call Renamer
Rename function calls throughout a codebase (useful for API migrations).use full_moon::{
ast::{self, Ast},
parse,
tokenizer::{Token, TokenType, TokenReference},
visitors::VisitorMut,
};
use std::collections::HashMap;
struct FunctionRenamer {
renames: HashMap<String, String>,
}
impl FunctionRenamer {
fn new() -> Self {
let mut renames = HashMap::new();
// Define old -> new function name mappings
renames.insert("oldPrint".to_string(), "print".to_string());
renames.insert("deprecatedFunction".to_string(), "newFunction".to_string());
Self { renames }
}
}
impl VisitorMut for FunctionRenamer {
fn visit_function_call(&mut self, call: ast::FunctionCall) -> ast::FunctionCall {
// Check if the prefix is a simple name we want to rename
let call = match call.prefix() {
ast::Prefix::Name(name) => {
let func_name = name.token().to_string();
if let Some(new_name) = self.renames.get(&func_name) {
// Create a new token with the new name
let new_token = TokenReference::new(
vec![], // leading trivia
Token::new(TokenType::Identifier {
identifier: new_name.clone().into(),
}),
vec![], // trailing trivia
);
call.with_prefix(ast::Prefix::Name(new_token))
} else {
call
}
}
_ => call,
};
// Continue visiting the rest of the call
call.visit_mut(self)
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let code = r#"
oldPrint("Hello")
deprecatedFunction(42)
normalFunction() -- This won't be changed
local x = oldPrint("test")
"#;
println!("Original code:\n{}", code);
let ast = parse(code)?;
let mut renamer = FunctionRenamer::new();
let transformed = renamer.visit_ast(ast);
println!("\nTransformed code:\n{}", transformed);
Ok(())
}
print("Hello")
newFunction(42)
normalFunction() -- This won't be changed
local x = print("test")
Example: Add Type Annotations (Luau)
Automatically add type annotations to function parameters based on heuristics.use full_moon::{
ast::{self, Ast, punctuated::Punctuated},
parse,
visitors::VisitorMut,
};
#[cfg(feature = "luau")]
struct TypeAnnotator;
#[cfg(feature = "luau")]
impl TypeAnnotator {
fn infer_type_from_default(&self, expr: &ast::Expression) -> Option<String> {
match expr {
ast::Expression::Number(_) => Some("number".to_string()),
ast::Expression::String(_) => Some("string".to_string()),
ast::Expression::Symbol(token) => {
match token.token().to_string().as_str() {
"true" | "false" => Some("boolean".to_string()),
"nil" => Some("nil".to_string()),
_ => None,
}
}
ast::Expression::TableConstructor(_) => Some("table".to_string()),
ast::Expression::Function(_) => Some("function".to_string()),
_ => None,
}
}
}
#[cfg(feature = "luau")]
impl VisitorMut for TypeAnnotator {
fn visit_function_body(&mut self, body: ast::FunctionBody) -> ast::FunctionBody {
// Check if parameters already have type specifiers
if body.type_specifiers().any(|spec| spec.is_some()) {
return body; // Already annotated
}
// Try to infer types from parameter names
let type_specifiers: Vec<Option<ast::luau::TypeSpecifier>> = body
.parameters()
.iter()
.map(|param| {
if let ast::Parameter::Name(name) = param {
let param_name = name.token().to_string();
// Simple heuristics based on name
let type_str = if param_name.contains("count") || param_name.contains("index") {
Some("number")
} else if param_name.contains("name") || param_name.contains("message") {
Some("string")
} else if param_name.starts_with("is_") || param_name.starts_with("has_") {
Some("boolean")
} else {
None
};
type_str.map(|t| {
// Create a basic type specifier
// Note: This is simplified; actual implementation would
// need to construct proper TypeSpecifier AST nodes
unimplemented!("Type specifier construction")
})
} else {
None
}
})
.collect();
body.with_type_specifiers(type_specifiers)
}
}
Example: Variable Name Refactoring
Refactor variable names from camelCase to snake_case throughout the code.use full_moon::{
ast::{self, Ast, punctuated::Punctuated},
parse,
tokenizer::{Token, TokenType, TokenReference},
visitors::VisitorMut,
};
use std::collections::HashMap;
struct SnakeCaseConverter {
// Track original -> converted names
name_map: HashMap<String, String>,
}
impl SnakeCaseConverter {
fn new() -> Self {
Self {
name_map: HashMap::new(),
}
}
fn to_snake_case(&mut self, name: &str) -> String {
// Check if we've already converted this name
if let Some(converted) = self.name_map.get(name) {
return converted.clone();
}
// Convert camelCase to snake_case
let mut result = String::new();
let mut chars = name.chars().peekable();
while let Some(c) = chars.next() {
if c.is_uppercase() && !result.is_empty() {
result.push('_');
result.push(c.to_lowercase().next().unwrap());
} else {
result.push(c);
}
}
self.name_map.insert(name.to_string(), result.clone());
result
}
fn convert_token(&mut self, token: &TokenReference) -> TokenReference {
let old_name = token.token().to_string();
let new_name = self.to_snake_case(&old_name);
if old_name == new_name {
return token.clone();
}
// Preserve trivia (whitespace and comments)
let leading: Vec<_> = token.leading_trivia().cloned().collect();
let trailing: Vec<_> = token.trailing_trivia().cloned().collect();
TokenReference::new(
leading,
Token::new(TokenType::Identifier {
identifier: new_name.into(),
}),
trailing,
)
}
}
impl VisitorMut for SnakeCaseConverter {
fn visit_local_assignment(&mut self, local: ast::LocalAssignment) -> ast::LocalAssignment {
// Convert variable names in declarations
let names: Punctuated<TokenReference> = local
.names()
.pairs()
.map(|pair| {
let name = self.convert_token(pair.value());
pair.to_owned().map(|_| name)
})
.collect();
local.with_names(names).visit_mut(self)
}
fn visit_var(&mut self, var: ast::Var) -> ast::Var {
// Convert variable names in usage
match var {
ast::Var::Name(name) => {
ast::Var::Name(self.convert_token(&name))
}
_ => var.visit_mut(self),
}
}
fn visit_function_declaration(&mut self, func: ast::FunctionDeclaration) -> ast::FunctionDeclaration {
// Convert function names
let name = func.name();
let names: Punctuated<TokenReference> = name
.names()
.pairs()
.map(|pair| {
let name = self.convert_token(pair.value());
pair.to_owned().map(|_| name)
})
.collect();
let new_name = name.clone().with_names(names);
func.with_name(new_name).visit_mut(self)
}
fn visit_parameter(&mut self, param: ast::Parameter) -> ast::Parameter {
// Convert parameter names
match param {
ast::Parameter::Name(name) => {
ast::Parameter::Name(self.convert_token(&name))
}
ast::Parameter::Ellipsis(token) => ast::Parameter::Ellipsis(token),
}
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let code = r#"
local myVariable = 10
local anotherName = 20
function calculateTotal(firstNumber, secondNumber)
local totalSum = firstNumber + secondNumber
return totalSum
end
print(myVariable)
"#;
println!("Original (camelCase):\n{}", code);
let ast = parse(code)?;
let mut converter = SnakeCaseConverter::new();
let transformed = converter.visit_ast(ast);
println!("\nTransformed (snake_case):\n{}", transformed);
println!("\nName mappings:");
for (old, new) in converter.name_map {
println!(" {} -> {}", old, new);
}
Ok(())
}
local my_variable = 10
local another_name = 20
function calculate_total(first_number, second_number)
local total_sum = first_number + second_number
return total_sum
end
print(my_variable)
Name mappings:
myVariable -> my_variable
anotherName -> another_name
calculateTotal -> calculate_total
firstNumber -> first_number
secondNumber -> second_number
totalSum -> total_sum
Example: Dead Code Elimination
Remove unreachable code after return statements.use full_moon::{
ast::{self, Ast},
parse,
visitors::VisitorMut,
};
struct DeadCodeEliminator;
impl VisitorMut for DeadCodeEliminator {
fn visit_block(&mut self, block: ast::Block) -> ast::Block {
let mut statements = Vec::new();
let mut found_return = false;
// Keep statements until we hit a return
for (stmt, semi) in block.stmts_with_semicolon() {
if found_return {
// Skip statements after return
continue;
}
statements.push((stmt.clone(), semi.cloned()));
// Check if this statement contains a return
// (simplified - a real implementation would check nested blocks)
if matches!(stmt, ast::Stmt::FunctionCall(_)) {
// Could be a return in disguise, be conservative
}
}
// Check last statement
let last_stmt = if !found_return {
block.last_stmt_with_semicolon().map(|(stmt, semi)| {
if matches!(stmt, ast::LastStmt::Return(_)) {
found_return = true;
}
(stmt.clone(), semi.cloned())
})
} else {
None
};
block
.with_stmts(statements)
.with_last_stmt(last_stmt)
.visit_mut(self)
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let code = r#"
function example(x)
if x > 10 then
return true
print("This won't execute") -- Dead code
local y = 5 -- Dead code
end
return false
end
"#;
println!("Before optimization:\n{}", code);
let ast = parse(code)?;
let mut eliminator = DeadCodeEliminator;
let optimized = eliminator.visit_ast(ast);
println!("\nAfter optimization:\n{}", optimized);
Ok(())
}
Combining Transformations
Chain multiple transformers for complex refactoring:use full_moon::{ast::Ast, parse};
fn refactor_codebase(code: &str) -> Result<String, Box<dyn std::error::Error>> {
let ast = parse(code)?;
// Apply multiple transformations in sequence
let ast = FunctionRenamer::new().visit_ast(ast);
let ast = SnakeCaseConverter::new().visit_ast(ast);
let ast = DeadCodeEliminator.visit_ast(ast);
let ast = IndentationFormatter::new(4).visit_ast(ast);
Ok(ast.to_string())
}
Key Concepts
Return modified nodes
Each
visit_* method must return a node of the same type, either the original or a modified version.Chain transformations
Call
.visit_mut(self) at the end of your visitor method to continue traversing child nodes.Use with_* methods
AST nodes have
with_* builder methods to create modified copies (e.g., with_name(), with_body()).Best Practices
- Test thoroughly: Parse, transform, and parse again to ensure valid Lua output
- Handle all cases: Consider edge cases like nested structures, method calls, and table indexing
- Preserve formatting: Copy trivia from original tokens when creating replacements
- Be conservative: When unsure if a transformation is safe, skip it and log a warning
- Validate scope: Track variable scopes to avoid renaming conflicts
- Update positions: Call
ast.update_positions()after transformations if you need accurate positions
Scope safety: When renaming variables or functions, be careful about shadowing and scope. A variable named
x in one scope might be different from x in another scope.Advanced: Custom AST Node Construction
Sometimes you need to build AST nodes from scratch:use full_moon::{
ast::{self, punctuated::Punctuated},
tokenizer::{Token, TokenType, TokenReference},
};
// Create a new local assignment: local newVar = 42
fn create_local_assignment() -> ast::LocalAssignment {
let name = TokenReference::new(
vec![], // no leading trivia
Token::new(TokenType::Identifier {
identifier: "newVar".into(),
}),
vec![], // no trailing trivia
);
let mut names = Punctuated::new();
names.push(ast::punctuated::Pair::End(name));
let value = ast::Expression::Number(
TokenReference::basic_symbol("42")
);
let mut values = Punctuated::new();
values.push(ast::punctuated::Pair::End(value));
ast::LocalAssignment::new(names)
.with_equal_token(Some(TokenReference::basic_symbol(" = ")))
.with_expressions(values)
}
Next Steps
Static Analysis
Analyze code patterns with the Visitor trait
Code Formatter
Build formatters that preserve original style