Skip to main content
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
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

Build docs developers (and LLMs) love