Loretta provides two powerful patterns for traversing syntax trees: visitors and walkers. Understanding when to use each is key to efficient tree analysis.
LuaSyntaxVisitor vs LuaSyntaxWalker
LuaSyntaxVisitor
Visits only the single node passed to its Visit method. Use this when you want full control over tree traversal.
using Loretta.CodeAnalysis.Lua;
public class MyVisitor : LuaSyntaxVisitor
{
public override void DefaultVisit(SyntaxNode node)
{
// Only called for the node you explicitly visit
Console.WriteLine($"Visiting: {node.Kind()}");
}
}
// Usage
var visitor = new MyVisitor();
visitor.Visit(node); // Visits only this node
LuaSyntaxWalker
Automatically descends through the entire tree in depth-first order. Use this when you want to process all nodes of certain types.
using Loretta.CodeAnalysis.Lua;
public class MyWalker : LuaSyntaxWalker
{
public MyWalker() : base(SyntaxWalkerDepth.Node)
{
}
public override void DefaultVisit(SyntaxNode node)
{
// Called for every node in the tree
Console.WriteLine($"Walking: {node.Kind()}");
base.DefaultVisit(node); // Continue walking
}
}
// Usage
var walker = new MyWalker();
walker.Visit(root); // Visits root and all descendants
When to Use Each
Use LuaSyntaxVisitor when:
- You need precise control over which nodes to visit
- You’re implementing a custom traversal order
- You only want to process specific nodes
Use LuaSyntaxWalker when:
- You want to visit all nodes of certain types
- You need to collect information from the entire tree
- You’re performing analysis or gathering statistics
SyntaxWalkerDepth Options
Walkers can descend to different depths:
public enum SyntaxWalkerDepth
{
Node, // Visit only nodes (default)
Token, // Visit nodes and tokens
Trivia, // Visit nodes, tokens, and trivia
StructuredTrivia // Visit everything including structured trivia
}
Depth Examples
// Visit only nodes
public class NodeWalker : LuaSyntaxWalker
{
public NodeWalker() : base(SyntaxWalkerDepth.Node)
{
}
}
// Visit nodes and tokens
public class TokenWalker : LuaSyntaxWalker
{
public TokenWalker() : base(SyntaxWalkerDepth.Token)
{
}
public override void VisitToken(SyntaxToken token)
{
Console.WriteLine($"Token: {token.Text}");
base.VisitToken(token);
}
}
// Visit nodes, tokens, and trivia
public class TriviaWalker : LuaSyntaxWalker
{
public TriviaWalker() : base(SyntaxWalkerDepth.Trivia)
{
}
public override void VisitTrivia(SyntaxTrivia trivia)
{
if (trivia.IsKind(SyntaxKind.SingleLineCommentTrivia))
{
Console.WriteLine($"Comment: {trivia}");
}
base.VisitTrivia(trivia);
}
}
The deeper you go, the more overhead. Use SyntaxWalkerDepth.Node unless you specifically need tokens or trivia.
Overriding Visit Methods
Both visitors and walkers provide specific Visit methods for each node type:
using Loretta.CodeAnalysis.Lua;
using Loretta.CodeAnalysis.Lua.Syntax;
public class FunctionVisitor : LuaSyntaxWalker
{
public FunctionVisitor() : base(SyntaxWalkerDepth.Node)
{
}
// Override specific node types
public override void VisitFunctionDeclarationStatement(
FunctionDeclarationStatementSyntax node)
{
Console.WriteLine($"Found function: {node.Name}");
// IMPORTANT: Call base to continue walking
base.VisitFunctionDeclarationStatement(node);
}
public override void VisitLocalFunctionDeclarationStatement(
LocalFunctionDeclarationStatementSyntax node)
{
Console.WriteLine($"Found local function: {node.Name}");
base.VisitLocalFunctionDeclarationStatement(node);
}
}
Always call base.VisitXxx(node) in walker methods, or child nodes won’t be visited!
Example: Collecting Function Calls
Here’s a complete walker that collects all function calls:
using System.Collections.Immutable;
using Loretta.CodeAnalysis;
using Loretta.CodeAnalysis.Lua;
using Loretta.CodeAnalysis.Lua.Syntax;
public class FunctionCallCollector : LuaSyntaxWalker
{
private readonly ImmutableArray<FunctionCallExpressionSyntax>.Builder _functionCalls;
private FunctionCallCollector() : base(SyntaxWalkerDepth.Node)
{
_functionCalls = ImmutableArray.CreateBuilder<FunctionCallExpressionSyntax>();
}
public static ImmutableArray<FunctionCallExpressionSyntax> Collect(SyntaxNode node)
{
var collector = new FunctionCallCollector();
collector.Visit(node);
return collector._functionCalls.ToImmutable();
}
public override void VisitFunctionCallExpression(
FunctionCallExpressionSyntax node)
{
// Add this call to our collection
_functionCalls.Add(node);
// Continue visiting (in case there are nested calls)
base.VisitFunctionCallExpression(node);
}
}
// Usage
var root = syntaxTree.GetRoot();
var functionCalls = FunctionCallCollector.Collect(root);
foreach (var call in functionCalls)
{
Console.WriteLine($"Call: {call.Expression}");
}
Example: Finding Variable Usage
Collect all references to a specific variable:
public class VariableUsageFinder : LuaSyntaxWalker
{
private readonly string _variableName;
private readonly List<IdentifierNameSyntax> _usages;
private VariableUsageFinder(string variableName)
: base(SyntaxWalkerDepth.Node)
{
_variableName = variableName;
_usages = new List<IdentifierNameSyntax>();
}
public static List<IdentifierNameSyntax> Find(
SyntaxNode node,
string variableName)
{
var finder = new VariableUsageFinder(variableName);
finder.Visit(node);
return finder._usages;
}
public override void VisitIdentifierName(IdentifierNameSyntax node)
{
if (node.Name == _variableName)
{
_usages.Add(node);
}
base.VisitIdentifierName(node);
}
}
// Usage
var usages = VariableUsageFinder.Find(root, "x");
Console.WriteLine($"Variable 'x' is used {usages.Count} time(s)");
Example: Gathering Statistics
Collect statistics about a code file:
public class CodeStatistics : LuaSyntaxWalker
{
public int FunctionCount { get; private set; }
public int LocalVariableCount { get; private set; }
public int IfStatementCount { get; private set; }
public int LoopCount { get; private set; }
public int CommentCount { get; private set; }
public CodeStatistics() : base(SyntaxWalkerDepth.Trivia)
{
}
public override void VisitFunctionDeclarationStatement(
FunctionDeclarationStatementSyntax node)
{
FunctionCount++;
base.VisitFunctionDeclarationStatement(node);
}
public override void VisitLocalFunctionDeclarationStatement(
LocalFunctionDeclarationStatementSyntax node)
{
FunctionCount++;
base.VisitLocalFunctionDeclarationStatement(node);
}
public override void VisitLocalVariableDeclarationStatement(
LocalVariableDeclarationStatementSyntax node)
{
LocalVariableCount += node.Names.Count;
base.VisitLocalVariableDeclarationStatement(node);
}
public override void VisitIfStatement(IfStatementSyntax node)
{
IfStatementCount++;
base.VisitIfStatement(node);
}
public override void VisitWhileStatement(WhileStatementSyntax node)
{
LoopCount++;
base.VisitWhileStatement(node);
}
public override void VisitRepeatUntilStatement(
RepeatUntilStatementSyntax node)
{
LoopCount++;
base.VisitRepeatUntilStatement(node);
}
public override void VisitNumericForStatement(
NumericForStatementSyntax node)
{
LoopCount++;
base.VisitNumericForStatement(node);
}
public override void VisitGenericForStatement(
GenericForStatementSyntax node)
{
LoopCount++;
base.VisitGenericForStatement(node);
}
public override void VisitTrivia(SyntaxTrivia trivia)
{
if (trivia.IsKind(SyntaxKind.SingleLineCommentTrivia) ||
trivia.IsKind(SyntaxKind.MultiLineCommentTrivia))
{
CommentCount++;
}
base.VisitTrivia(trivia);
}
public void PrintReport()
{
Console.WriteLine("Code Statistics:");
Console.WriteLine($" Functions: {FunctionCount}");
Console.WriteLine($" Local variables: {LocalVariableCount}");
Console.WriteLine($" If statements: {IfStatementCount}");
Console.WriteLine($" Loops: {LoopCount}");
Console.WriteLine($" Comments: {CommentCount}");
}
}
// Usage
var stats = new CodeStatistics();
stats.Visit(root);
stats.PrintReport();
Example: Finding Unused Local Variables
A more complex example that tracks variable declarations and usage:
public class UnusedLocalFinder : LuaSyntaxWalker
{
private readonly HashSet<string> _declaredLocals = new();
private readonly HashSet<string> _usedVariables = new();
public UnusedLocalFinder() : base(SyntaxWalkerDepth.Node)
{
}
public IEnumerable<string> UnusedLocals =>
_declaredLocals.Except(_usedVariables);
public override void VisitLocalVariableDeclarationStatement(
LocalVariableDeclarationStatementSyntax node)
{
foreach (var name in node.Names)
{
_declaredLocals.Add(name.Name.Text);
}
base.VisitLocalVariableDeclarationStatement(node);
}
public override void VisitLocalFunctionDeclarationStatement(
LocalFunctionDeclarationStatementSyntax node)
{
_declaredLocals.Add(node.Name.Text);
base.VisitLocalFunctionDeclarationStatement(node);
}
public override void VisitIdentifierName(IdentifierNameSyntax node)
{
_usedVariables.Add(node.Name);
base.VisitIdentifierName(node);
}
}
// Usage
var finder = new UnusedLocalFinder();
finder.Visit(root);
foreach (var unused in finder.UnusedLocals)
{
Console.WriteLine($"Unused local variable: {unused}");
}
Visitor with Return Values
Use LuaSyntaxVisitor<TResult> to return values:
public class ExpressionEvaluator : LuaSyntaxVisitor<int?>
{
public override int? VisitLiteralExpression(
LiteralExpressionSyntax node)
{
if (node.Token.Value is double d)
{
return (int)d;
}
return null;
}
public override int? VisitBinaryExpression(
BinaryExpressionSyntax node)
{
var left = Visit(node.Left);
var right = Visit(node.Right);
if (left == null || right == null)
return null;
return node.Kind() switch
{
SyntaxKind.AddExpression => left + right,
SyntaxKind.SubtractExpression => left - right,
SyntaxKind.MultiplyExpression => left * right,
SyntaxKind.DivideExpression => right != 0 ? left / right : null,
_ => null
};
}
}
// Usage
var evaluator = new ExpressionEvaluator();
var result = evaluator.Visit(expression);
if (result.HasValue)
{
Console.WriteLine($"Result: {result.Value}");
}
Use the Right Depth
// Fast - only visits nodes
var walker = new MyWalker(SyntaxWalkerDepth.Node);
// Slower - visits nodes and tokens
var walker = new MyWalker(SyntaxWalkerDepth.Token);
// Slowest - visits everything
var walker = new MyWalker(SyntaxWalkerDepth.StructuredTrivia);
Early Exit
Stop walking when you’ve found what you need:
public class FirstFunctionFinder : LuaSyntaxWalker
{
public FunctionDeclarationStatementSyntax? FoundFunction { get; private set; }
public override void VisitFunctionDeclarationStatement(
FunctionDeclarationStatementSyntax node)
{
if (FoundFunction == null)
{
FoundFunction = node;
// Don't call base - stops walking
return;
}
base.VisitFunctionDeclarationStatement(node);
}
}
Use LINQ for Simple Cases
For simple queries, LINQ may be clearer:
// With walker
var collector = new FunctionCallCollector();
collector.Visit(root);
var calls = collector.FunctionCalls;
// With LINQ (simpler for one-off queries)
var calls = root.DescendantNodes()
.OfType<FunctionCallExpressionSyntax>()
.ToList();
Next Steps