Visitor Pattern
The visitor pattern is the core concept in ReluxScript for traversing and transforming ASTs.
Overview
The visitor pattern works by:
- Traversing the AST tree
- Calling visitor methods for matching node types
- Allowing transformations at each node
Basic Visitor
reluxscript
plugin MyPlugin {
fn visit_identifier(node: &mut Identifier, ctx: &Context) {
// Called for every identifier in the code
}
fn visit_call_expression(node: &mut CallExpression, ctx: &Context) {
// Called for every function call
}
}Traversal Order
By default, traversal is depth-first, pre-order:
reluxscript
fn visit_function_declaration(node: &mut FunctionDeclaration, ctx: &Context) {
// 1. This runs first
// 2. Then children are visited automatically
node.visit_children(self);
// 3. Post-processing can go here
}Manual Traversal Control
Skip Children
Don't visit child nodes:
reluxscript
fn visit_function_declaration(node: &mut FunctionDeclaration, ctx: &Context) {
// Don't call visit_children() - children won't be visited
// Useful for skipping certain subtrees
}Explicit Child Traversal
Manually control which children to visit:
reluxscript
fn visit_block_statement(node: &mut BlockStatement, ctx: &Context) {
for stmt in &mut node.body {
if should_visit(stmt) {
stmt.visit_with(self);
}
}
}Nested Visitors
Create scoped visitors for subtrees:
reluxscript
fn visit_function_declaration(func: &mut FunctionDeclaration, ctx: &Context) {
// Use a nested visitor for the function body
traverse(&mut func.body) {
let mut return_count = 0;
fn visit_return_statement(ret: &mut ReturnStatement, ctx: &Context) {
self.return_count += 1;
}
}
println!("Function has {} returns", return_count);
}State Tracking
Track state across visitor methods:
reluxscript
plugin StateTracker {
struct State {
depth: i32,
in_async: bool,
}
fn visit_function_declaration(node: &mut FunctionDeclaration, ctx: &Context) {
self.state.depth += 1;
let was_async = self.state.in_async;
self.state.in_async = node.async;
node.visit_children(self);
self.state.in_async = was_async;
self.state.depth -= 1;
}
}Multiple Visitors
Combine multiple transformations:
reluxscript
plugin CompositePlugin {
fn visit_call_expression(node: &mut CallExpression, ctx: &Context) {
// First transformation
self.remove_console_logs(node);
// Second transformation
self.inline_simple_calls(node);
node.visit_children(self);
}
fn remove_console_logs(&mut self, node: &mut CallExpression) {
if matches!(node.callee, "console.log") {
*node = Statement::empty();
}
}
fn inline_simple_calls(&mut self, node: &mut CallExpression) {
// ...
}
}Using External Visitors
Delegate to another visitor:
reluxscript
plugin Main {
fn visit_function(node: &mut Function, ctx: &Context) {
if node.is_async {
// Use a specialized visitor for async functions
traverse(node) using AsyncTransformer;
}
}
}
plugin AsyncTransformer {
fn visit_await_expression(node: &mut AwaitExpression, ctx: &Context) {
// Handle await expressions
}
}Visitor Entry Points
Regular Visitors
Most visitor methods:
reluxscript
fn visit_<node_type>(node: &mut NodeType, ctx: &Context) {
// Transform node
}Program Hooks
Special lifecycle methods:
reluxscript
// Called before traversal begins
fn pre(file: &File) {
// Initialize
}
// Called after traversal completes
fn exit(program: &mut Program, state: &PluginState) {
// Finalize
}Best Practices
1. Keep Visitors Focused
reluxscript
// ✅ Good: One concern per visitor
fn visit_call_expression(node: &mut CallExpression, ctx: &Context) {
if self.should_remove(node) {
*node = Statement::empty();
}
}
// ❌ Bad: Too much logic
fn visit_call_expression(node: &mut CallExpression, ctx: &Context) {
// 100 lines of complex logic
}2. Use Helper Methods
reluxscript
plugin MyPlugin {
fn is_console_method(&self, callee: &Expression) -> bool {
matches!(callee, MemberExpression {
object: Identifier { name: "console" }
})
}
fn visit_call_expression(node: &mut CallExpression, ctx: &Context) {
if self.is_console_method(&node.callee) {
self.handle_console_call(node);
}
}
}3. Track State Explicitly
reluxscript
// ✅ Good: Explicit state
struct State {
current_component: Option<Str>,
hooks_found: Vec<HookInfo>,
}
// ❌ Bad: Implicit tracking (won't work)
// Don't rely on parent node context4. Clean Up State
reluxscript
fn visit_function_declaration(node: &mut FunctionDeclaration, ctx: &Context) {
// Save state
let prev_component = self.state.current_component.clone();
// Set new state
self.state.current_component = Some(node.id.name.clone());
// Visit children
node.visit_children(self);
// Restore state
self.state.current_component = prev_component;
}Common Patterns
Count Occurrences
reluxscript
struct State {
call_count: i32,
}
fn visit_call_expression(node: &mut CallExpression, ctx: &Context) {
self.state.call_count += 1;
node.visit_children(self);
}Collect Information
reluxscript
struct State {
identifiers: Vec<Str>,
}
fn visit_identifier(node: &mut Identifier, ctx: &Context) {
self.state.identifiers.push(node.name.clone());
}Conditional Transformation
reluxscript
fn visit_call_expression(node: &mut CallExpression, ctx: &Context) {
if self.should_transform(node) {
*node = self.create_replacement(node);
} else {
node.visit_children(self);
}
}See Visitor Methods API for all available methods.
