Once you’ve parsed Lua code into a syntax tree, you can navigate and query it to find specific nodes, tokens, and trivia.
Understanding the Syntax Tree
A syntax tree consists of three types of elements:
- Nodes - Structural elements like statements and expressions (
IfStatementSyntax, FunctionCallExpressionSyntax)
- Tokens - Terminal symbols like keywords, identifiers, and operators (
local, function, +)
- Trivia - Non-structural text like whitespace, comments, and line breaks
Navigating the Tree
Syntax nodes provide several methods for navigation:
Parent and Children
using Loretta.CodeAnalysis.Lua.Syntax;
var root = syntaxTree.GetRoot();
// Get parent
var parent = node.Parent;
// Get children (nodes and tokens)
var children = node.ChildNodesAndTokens();
// Get only child nodes
foreach (var childNode in node.ChildNodes())
{
// Process child nodes
}
Descendant Nodes
To find all nodes of a certain type in a subtree:
// Get all descendant nodes
var allNodes = root.DescendantNodes();
// Get all function declarations
var functions = root.DescendantNodes()
.OfType<FunctionDeclarationStatementSyntax>();
foreach (var func in functions)
{
Console.WriteLine($"Found function: {func.Name}");
}
Ancestor Nodes
Traverse up the tree:
// Get all ancestors
var ancestors = node.Ancestors();
// Find the first ancestor of a specific type
var containingFunction = node.FirstAncestorOrSelf<FunctionDeclarationStatementSyntax>();
if (containingFunction != null)
{
Console.WriteLine($"This node is inside function: {containingFunction.Name}");
}
Finding Specific Nodes
Using SyntaxKind
Check node types using the IsKind method:
using Loretta.CodeAnalysis.Lua;
if (node.IsKind(SyntaxKind.IdentifierName))
{
var identifier = (IdentifierNameSyntax) node;
Console.WriteLine($"Identifier: {identifier.Name}");
}
else if (node.IsKind(SyntaxKind.MemberAccessExpression))
{
var memberAccess = (MemberAccessExpressionSyntax) node;
Console.WriteLine($"Member: {memberAccess.MemberName.Text}");
}
Finding Tokens
Use FindToken to locate a token at a specific position:
// Find token at position 100
var token = root.FindToken(100);
Console.WriteLine($"Token at position 100: {token.Text}");
Console.WriteLine($"Token kind: {token.Kind()}");
// Find token including trivia (whitespace/comments)
var tokenWithTrivia = root.FindToken(100, findInsideTrivia: true);
Pattern Matching
Combine DescendantNodes with LINQ for complex queries:
// Find all local variable declarations with specific names
var localX = root.DescendantNodes()
.OfType<LocalVariableDeclarationStatementSyntax>()
.Where(local => local.Names.Any(name => name.Name.Text == "x"));
// Find all assignments to table fields
var tableAssignments = root.DescendantNodes()
.OfType<AssignmentStatementSyntax>()
.Where(assignment => assignment.Variables.Any(
v => v.IsKind(SyntaxKind.MemberAccessExpression)));
Working with Tokens
Token Properties
var token = root.FindToken(position);
// Get token text
var text = token.Text;
var valueText = token.ValueText;
// Get token kind
var kind = token.Kind();
// Check if token is missing (inserted by error recovery)
if (token.IsMissing)
{
Console.WriteLine("This token was expected but not found");
}
// Get token span
var span = token.Span;
var fullSpan = token.FullSpan; // Includes trivia
Getting Adjacent Tokens
// Get next/previous tokens
var nextToken = token.GetNextToken();
var previousToken = token.GetPreviousToken();
// Get next token skipping zero-width tokens
var nextNonZeroWidth = token.GetNextToken(
predicate: t => t.Span.Length > 0);
Working with Trivia
Trivia represents whitespace, comments, and other non-structural text.
Leading and Trailing Trivia
// Tokens have leading and trailing trivia
var leadingTrivia = token.LeadingTrivia;
var trailingTrivia = token.TrailingTrivia;
foreach (var trivia in leadingTrivia)
{
if (trivia.IsKind(SyntaxKind.SingleLineCommentTrivia))
{
Console.WriteLine($"Comment: {trivia.ToString()}");
}
}
Trivia on Nodes
// Get all trivia in a node (including descendants)
var allTrivia = node.DescendantTrivia();
// Get first token's leading trivia
var nodeLeadingTrivia = node.GetLeadingTrivia();
var nodeTrailingTrivia = node.GetTrailingTrivia();
Complete Example: Finding Function Calls
Here’s a complete example that finds all function calls in code:
using Loretta.CodeAnalysis;
using Loretta.CodeAnalysis.Lua;
using Loretta.CodeAnalysis.Lua.Syntax;
using Loretta.CodeAnalysis.Text;
public static class FunctionCallFinder
{
public static void FindFunctionCalls(string code)
{
// Parse the code
var parseOptions = new LuaParseOptions(LuaSyntaxOptions.Lua51);
var syntaxTree = LuaSyntaxTree.ParseText(code, parseOptions);
var root = syntaxTree.GetRoot();
// Find all function calls
var functionCalls = root.DescendantNodes()
.OfType<FunctionCallExpressionSyntax>();
foreach (var call in functionCalls)
{
// Get the function being called
var expression = call.Expression;
if (expression.IsKind(SyntaxKind.IdentifierName))
{
// Simple function call: print()
var identifier = (IdentifierNameSyntax) expression;
Console.WriteLine($"Call to: {identifier.Name}");
}
else if (expression.IsKind(SyntaxKind.MemberAccessExpression))
{
// Member call: string.format()
var memberAccess = (MemberAccessExpressionSyntax) expression;
Console.WriteLine($"Call to: {memberAccess.Expression}.{memberAccess.MemberName.Text}");
}
else
{
// Complex expression
Console.WriteLine($"Call to: {expression}");
}
// Get the arguments
if (call.Argument is FunctionArgumentListSyntax argList)
{
Console.WriteLine($" Arguments: {argList.Arguments.Count}");
}
// Get the source location
var lineSpan = syntaxTree.GetLineSpan(call.Span);
Console.WriteLine($" Location: Line {lineSpan.StartLinePosition.Line + 1}");
}
}
}
// Usage
var code = @"
local x = string.byte('hello', 1)
print(x)
math.max(1, 2, 3)
";
FunctionCallFinder.FindFunctionCalls(code);
Output:
Call to: string.byte
Arguments: 2
Location: Line 2
Call to: print
Arguments: 1
Location: Line 3
Call to: math.max
Arguments: 3
Location: Line 4
Checking Node Types
Before casting, check the node type:
// Type checking with pattern matching
if (node is FunctionCallExpressionSyntax functionCall)
{
// Use functionCall directly
Console.WriteLine($"Function: {functionCall.Expression}");
}
// Or use IsKind
if (node.IsKind(SyntaxKind.FunctionCallExpression))
{
var functionCall = (FunctionCallExpressionSyntax) node;
}
// Check multiple kinds
if (node.IsKind(SyntaxKind.IdentifierName) ||
node.IsKind(SyntaxKind.MemberAccessExpression))
{
// Process either type
}
The IsKind method is an extension method from Loretta.CodeAnalysis.LuaExtensions. Make sure to include using Loretta.CodeAnalysis.Lua;
Getting Text Spans
Every node and token has position information:
// Span excludes trivia
var span = node.Span;
var start = span.Start;
var end = span.End;
var length = span.Length;
// FullSpan includes trivia
var fullSpan = node.FullSpan;
// Get the text
var nodeText = node.ToString(); // Excludes trivia
var fullText = node.ToFullString(); // Includes trivia
// Get line position
var lineSpan = syntaxTree.GetLineSpan(span);
Console.WriteLine($"Starts at line {lineSpan.StartLinePosition.Line + 1}, " +
$"column {lineSpan.StartLinePosition.Character + 1}");
Next Steps