Skip to main content

Overview

The LuaSyntaxRewriter class enables you to transform Lua syntax trees by visiting nodes and returning modified versions. It automatically reconstructs the tree with your changes while preserving the overall structure. This is the primary tool for code transformations in Loretta.

Namespace

Loretta.CodeAnalysis.Lua

Syntax

public abstract partial class LuaSyntaxRewriter : LuaSyntaxVisitor<SyntaxNode?>

When to Use

Use LuaSyntaxRewriter when you need to:
  • Transform or modify syntax trees
  • Replace specific nodes with different nodes
  • Rename identifiers
  • Refactor code programmatically
  • Add or remove nodes from the tree
  • Preserve trivia (whitespace, comments) during transformations

Key Concepts

Immutability

Syntax trees in Loretta are immutable. When you modify a node:
  1. A new node is created with the changes
  2. All parent nodes up to the root are recreated
  3. The original tree remains unchanged
This enables safe concurrent access and undo/redo functionality.

Automatic Tree Reconstruction

The rewriter automatically:
  • Visits all nodes in the tree
  • Reconstructs parent nodes when children change
  • Preserves trivia (comments, whitespace) by default
  • Maintains tree consistency

Constructor

public LuaSyntaxRewriter(bool visitIntoStructuredTrivia = false)
Parameters:
  • visitIntoStructuredTrivia: Whether to visit into structured trivia nodes (rarely needed)

Key Properties

VisitIntoStructuredTrivia

public virtual bool VisitIntoStructuredTrivia { get; }
Indicates whether the rewriter enters structured trivia.

Key Methods

Visit

public override SyntaxNode? Visit(SyntaxNode? node)
Visits a node and returns the transformed version. Returns null if the node should be removed.

VisitToken

public virtual SyntaxToken VisitToken(SyntaxToken token)
Visits a token and returns the transformed version. Override to modify tokens (keywords, identifiers, operators).

VisitTrivia

public virtual SyntaxTrivia VisitTrivia(SyntaxTrivia trivia)
Visits trivia and returns the transformed version. Override to modify comments or whitespace.

VisitList

public virtual SyntaxList<TNode> VisitList<TNode>(SyntaxList<TNode> list) where TNode : SyntaxNode
public virtual SeparatedSyntaxList<TNode> VisitList<TNode>(SeparatedSyntaxList<TNode> list) where TNode : SyntaxNode
public virtual SyntaxTokenList VisitList(SyntaxTokenList list)
public virtual SyntaxTriviaList VisitList(SyntaxTriviaList list)
Visits lists of nodes, tokens, or trivia. Rarely need to override these directly.

Examples

Example 1: Rename All Identifiers

using Loretta.CodeAnalysis.Lua;
using Loretta.CodeAnalysis.Lua.Syntax;

public class IdentifierRenamer : LuaSyntaxRewriter
{
    private readonly string _oldName;
    private readonly string _newName;

    public IdentifierRenamer(string oldName, string newName)
    {
        _oldName = oldName;
        _newName = newName;
    }

    public override SyntaxNode? VisitIdentifierName(IdentifierNameSyntax node)
    {
        if (node.Name == _oldName)
        {
            // Create a new identifier with the new name
            var newIdentifier = SyntaxFactory.IdentifierName(_newName)
                                             .WithTriviaFrom(node); // Preserve trivia
            return newIdentifier;
        }

        return base.VisitIdentifierName(node);
    }
}

// Usage
var code = @"
local oldVar = 10
print(oldVar)
";

var tree = LuaSyntaxTree.ParseText(code, new LuaParseOptions(LuaSyntaxOptions.All));
var rewriter = new IdentifierRenamer("oldVar", "newVar");
var newRoot = rewriter.Visit(tree.GetRoot());

Console.WriteLine(newRoot.ToFullString());
// Output:
// local newVar = 10
// print(newVar)

Example 2: Convert Global Functions to Locals

This example is from the Creating a Localizer tutorial:
public class FunctionLocalizer : LuaSyntaxRewriter
{
    private readonly Dictionary<string, IdentifierNameSyntax> _replacements;

    public FunctionLocalizer(Dictionary<string, IdentifierNameSyntax> replacements)
    {
        _replacements = replacements;
    }

    public override SyntaxNode? VisitCompilationUnit(CompilationUnitSyntax node)
    {
        // Visit all statements first
        var statements = VisitList(node.Statements.Statements);

        // Create local declarations for the replacements
        var names = _replacements.Keys.Select(k => 
            SyntaxFactory.LocalDeclarationName(SyntaxFactory.IdentifierName(k)));
        var values = _replacements.Keys.Select(k => 
            SyntaxFactory.IdentifierName(k.Replace("_", ".")));

        var localDeclaration = SyntaxFactory.LocalVariableDeclarationStatement(
            SyntaxFactory.SeparatedList(names),
            SyntaxFactory.SeparatedList<ExpressionSyntax>(values));

        localDeclaration = localDeclaration.NormalizeWhitespace()
            .WithTrailingTrivia(SyntaxFactory.EndOfLine(Environment.NewLine));

        // Insert at the beginning
        statements = statements.Insert(0, localDeclaration);

        return node.WithStatements(node.Statements.WithStatements(statements));
    }

    public override SyntaxNode? VisitMemberAccessExpression(MemberAccessExpressionSyntax node)
    {
        // Convert string.format to string_format
        var key = $"{node.Expression}_{node.MemberName.Text}";
        if (_replacements.TryGetValue(key, out var replacement))
        {
            return replacement.WithTriviaFrom(node);
        }

        return base.VisitMemberAccessExpression(node);
    }
}

Example 3: Add Return Statement to Functions

public class ReturnAdder : LuaSyntaxRewriter
{
    private readonly ExpressionSyntax _returnValue;

    public ReturnAdder(ExpressionSyntax returnValue)
    {
        _returnValue = returnValue;
    }

    public override SyntaxNode? VisitAnonymousFunctionExpression(AnonymousFunctionExpressionSyntax node)
    {
        // Visit the function body first
        var newBody = (StatementListSyntax)Visit(node.Body)!;

        // Check if last statement is already a return
        var statements = newBody.Statements;
        if (statements.Any() && statements.Last() is ReturnStatementSyntax)
        {
            return node.WithBody(newBody);
        }

        // Add return statement
        var returnStmt = SyntaxFactory.ReturnStatement(
            SyntaxFactory.Token(SyntaxKind.ReturnKeyword),
            SyntaxFactory.SeparatedList(new[] { _returnValue }));

        statements = statements.Add(returnStmt);
        newBody = newBody.WithStatements(statements);

        return node.WithBody(newBody);
    }
}

// Usage
var code = @"
local fn = function(x)
    print(x)
end
";

var tree = LuaSyntaxTree.ParseText(code, new LuaParseOptions(LuaSyntaxOptions.All));
var returnValue = SyntaxFactory.LiteralExpression(
    SyntaxKind.TrueLiteralExpression,
    SyntaxFactory.Token(SyntaxKind.TrueKeyword));

var rewriter = new ReturnAdder(returnValue);
var newRoot = rewriter.Visit(tree.GetRoot());

Console.WriteLine(newRoot.ToFullString());
// Output:
// local fn = function(x)
//     print(x)
// return true
// end

Example 4: Remove All Comments

public class CommentRemover : LuaSyntaxRewriter
{
    public override SyntaxTrivia VisitTrivia(SyntaxTrivia trivia)
    {
        // Remove comment trivia
        if (trivia.Kind() == SyntaxKind.SingleLineCommentTrivia ||
            trivia.Kind() == SyntaxKind.MultiLineCommentTrivia)
        {
            return default; // Return empty trivia
        }

        return base.VisitTrivia(trivia);
    }
}

// Usage
var code = @"
-- This is a comment
local x = 10 -- inline
/* Multi
   line */
print(x)
";

var tree = LuaSyntaxTree.ParseText(code, new LuaParseOptions(LuaSyntaxOptions.All));
var remover = new CommentRemover();
var newRoot = remover.Visit(tree.GetRoot());

Console.WriteLine(newRoot.ToFullString());
// Output:
// 
// local x = 10 
// 
// print(x)

Example 5: Transform Binary Operators

public class OperatorTransformer : LuaSyntaxRewriter
{
    public override SyntaxNode? VisitBinaryExpression(BinaryExpressionSyntax node)
    {
        // Visit children first
        var left = (ExpressionSyntax)Visit(node.Left)!;
        var right = (ExpressionSyntax)Visit(node.Right)!;

        // Transform != to ~=
        if (node.OperatorToken.Kind() == SyntaxKind.BangEqualsToken)
        {
            var newOperator = SyntaxFactory.Token(SyntaxKind.TildeEqualsToken)
                                           .WithTriviaFrom(node.OperatorToken);
            return node.Update(left, newOperator, right);
        }

        // Return with potentially updated children
        if (left != node.Left || right != node.Right)
        {
            return node.Update(left, node.OperatorToken, right);
        }

        return node;
    }
}

// Usage
var code = "local x = (a != b)";

var tree = LuaSyntaxTree.ParseText(code, new LuaParseOptions(LuaSyntaxOptions.All));
var transformer = new OperatorTransformer();
var newRoot = transformer.Visit(tree.GetRoot());

Console.WriteLine(newRoot.ToFullString());
// Output: local x = (a ~= b)

Tips for Preserving Trivia

Use WithTriviaFrom

Always preserve trivia when replacing nodes:
var newNode = SyntaxFactory.IdentifierName("newName")
                           .WithTriviaFrom(oldNode);

Use NormalizeWhitespace

Normalize whitespace for generated nodes:
var statement = SyntaxFactory.LocalVariableDeclarationStatement(...)
                             .NormalizeWhitespace();

Add Line Breaks Explicitly

Add line breaks after generated statements:
var statement = statement.WithTrailingTrivia(
    statement.GetTrailingTrivia()
             .Add(SyntaxFactory.EndOfLine(Environment.NewLine)));

Preserve Original Formatting

When updating nodes, use the Update method to preserve formatting:
public override SyntaxNode? VisitBinaryExpression(BinaryExpressionSyntax node)
{
    var newLeft = (ExpressionSyntax)Visit(node.Left)!;
    var newRight = (ExpressionSyntax)Visit(node.Right)!;
    
    // Use Update to preserve trivia
    return node.Update(newLeft, node.OperatorToken, newRight);
}

Common Patterns

Pattern 1: Conditional Transformation

public override SyntaxNode? VisitIdentifierName(IdentifierNameSyntax node)
{
    if (ShouldTransform(node))
    {
        return CreateTransformed(node);
    }
    return base.VisitIdentifierName(node);
}

Pattern 2: Transform and Visit Children

public override SyntaxNode? VisitFunctionDeclarationStatement(FunctionDeclarationStatementSyntax node)
{
    // Visit children first
    var newBody = (StatementListSyntax)Visit(node.Body)!;
    
    // Then transform
    return node.WithBody(newBody).WithSomeModification();
}

Pattern 3: Remove Nodes

public override SyntaxNode? VisitEmptyStatement(EmptyStatementSyntax node)
{
    // Return null to remove the node
    return null;
}

Pattern 4: Add Nodes to Lists

public override SyntaxNode? VisitStatementList(StatementListSyntax node)
{
    var statements = VisitList(node.Statements);
    
    // Add a new statement
    var newStatement = SyntaxFactory.ExpressionStatement(...);
    statements = statements.Add(newStatement);
    
    return node.WithStatements(statements);
}

Important Notes

Performance Considerations

  • The rewriter creates many intermediate objects
  • For large trees, consider caching frequently used nodes
  • Use VisitList methods efficiently

Error Handling

  • Always check for null when calling Visit
  • Handle cases where child nodes might be removed
  • Validate tree structure after transformation

Testing Transformations

Always test your rewriter:
[Test]
public void TestRenamer()
{
    var code = "local x = 10";
    var tree = LuaSyntaxTree.ParseText(code, new LuaParseOptions(LuaSyntaxOptions.All));
    var rewriter = new IdentifierRenamer("x", "y");
    var newRoot = rewriter.Visit(tree.GetRoot());
    
    Assert.That(newRoot.ToFullString(), Is.EqualTo("local y = 10"));
}

See Also

Build docs developers (and LLMs) love