Skip to main content

What are Diagnostics?

Diagnostics in Loretta represent errors, warnings, and informational messages produced during parsing and analysis. They provide detailed information about issues in Lua code, including:
  • Syntax errors (missing tokens, unexpected characters)
  • Warnings (deprecated features, potential issues)
  • Informational messages (suggestions, hints)

The Diagnostic Class

The Diagnostic class represents a single diagnostic message:
public abstract class Diagnostic
{
    public abstract DiagnosticSeverity Severity { get; }
    public abstract string Id { get; }
    public abstract string GetMessage();
    public abstract Location Location { get; }
    // ... other members
}
Key properties:
  • Severity - How severe the diagnostic is (Error, Warning, Info, Hidden)
  • Id - A unique identifier for the diagnostic (e.g., “LUA0001”)
  • GetMessage() - Human-readable description of the issue
  • Location - Where in the source code the issue occurs

DiagnosticSeverity

Diagnostics have one of four severity levels:
public enum DiagnosticSeverity
{
    Hidden = 0,   // Not surfaced through normal means
    Info = 1,      // Informational, not a problem
    Warning = 2,   // Suspicious but allowed
    Error = 3      // Not allowed by language rules
}

Severity Levels Explained

Error - Code that violates Lua syntax rules:
var code = "local x = ";
var tree = LuaSyntaxTree.ParseText(code);
var diagnostics = tree.GetDiagnostics();

var error = diagnostics.First();
Console.WriteLine($"{error.Severity}: {error.GetMessage()}");
// Output: Error: Unexpected end of file
Warning - Code that may indicate a problem:
var code = "local x = 0b1010"; // Binary numbers in Lua 5.1
var tree = LuaSyntaxTree.ParseText(
    code,
    new LuaParseOptions(LuaSyntaxOptions.Lua51)
);

var diagnostics = tree.GetDiagnostics();
foreach (var diagnostic in diagnostics)
{
    Console.WriteLine($"{diagnostic.Severity}: {diagnostic.GetMessage()}");
}
Info - Helpful information that doesn’t indicate a problem Hidden - Used internally, not typically shown to users

Getting Diagnostics from Trees

You can retrieve diagnostics from various parts of the syntax tree:

All Diagnostics in a Tree

var code = @"
local x = 10
local y = 
local z = 20
";

var tree = LuaSyntaxTree.ParseText(code);

// Get all diagnostics
var diagnostics = tree.GetDiagnostics();

foreach (var diagnostic in diagnostics)
{
    Console.WriteLine($"{diagnostic.Severity}: {diagnostic.GetMessage()}");
    Console.WriteLine($"  Location: {diagnostic.Location.GetLineSpan()}");
}

Diagnostics for a Specific Node

var code = "local x = 10 + ";
var tree = LuaSyntaxTree.ParseText(code);
var root = tree.GetRoot();

// Get diagnostics for a specific node
var firstStatement = root.ChildNodes().First();
var nodeDiagnostics = tree.GetDiagnostics(firstStatement);

foreach (var diagnostic in nodeDiagnostics)
{
    Console.WriteLine(diagnostic.GetMessage());
}

Diagnostics for a Token

var code = "local x = @invalid";
var tree = LuaSyntaxTree.ParseText(code);
var root = tree.GetRoot();

// Find the invalid token
var tokens = root.DescendantTokens();
foreach (var token in tokens)
{
    var tokenDiagnostics = tree.GetDiagnostics(token);
    if (tokenDiagnostics.Any())
    {
        Console.WriteLine($"Token '{token.Text}' has diagnostics:");
        foreach (var diagnostic in tokenDiagnostics)
        {
            Console.WriteLine($"  {diagnostic.GetMessage()}");
        }
    }
}

Diagnostic Locations

Each diagnostic has a Location that indicates where the issue occurs:
var code = @"
local x = 10
local y = 
local z = 20
";

var tree = LuaSyntaxTree.ParseText(code);
var diagnostics = tree.GetDiagnostics();

foreach (var diagnostic in diagnostics)
{
    var location = diagnostic.Location;
    
    // Get the source span (character positions)
    var span = location.SourceSpan;
    Console.WriteLine($"Position: {span.Start}-{span.End}");
    
    // Get line and column information
    var lineSpan = location.GetLineSpan();
    var linePos = lineSpan.StartLinePosition;
    Console.WriteLine($"Line {linePos.Line + 1}, Column {linePos.Character + 1}");
    
    // Get the file path (if available)
    Console.WriteLine($"File: {lineSpan.Path}");
}

Location Properties

  • SourceSpan - The TextSpan covering the diagnostic location
  • GetLineSpan() - Returns line and column information
  • SourceTree - The syntax tree containing the diagnostic

Diagnostic IDs

Each diagnostic has a unique ID that identifies the type of error:
var code = "local x = ";
var tree = LuaSyntaxTree.ParseText(code);
var diagnostic = tree.GetDiagnostics().First();

Console.WriteLine($"Diagnostic ID: {diagnostic.Id}");
Console.WriteLine($"Message: {diagnostic.GetMessage()}");

// You can use IDs to filter or categorize diagnostics
var errorDiagnostics = tree.GetDiagnostics()
    .Where(d => d.Severity == DiagnosticSeverity.Error)
    .GroupBy(d => d.Id);

foreach (var group in errorDiagnostics)
{
    Console.WriteLine($"{group.Key}: {group.Count()} occurrences");
}

DiagnosticDescriptor

A DiagnosticDescriptor provides metadata about a diagnostic type:
public sealed class DiagnosticDescriptor
{
    public string Id { get; }
    public LocalizableString Title { get; }
    public LocalizableString MessageFormat { get; }
    public string Category { get; }
    public DiagnosticSeverity DefaultSeverity { get; }
    public bool IsEnabledByDefault { get; }
    public LocalizableString Description { get; }
    public string HelpLinkUri { get; }
}
Descriptors are typically used when creating custom diagnostics:
using Loretta.CodeAnalysis;

// Create a diagnostic descriptor
var descriptor = new DiagnosticDescriptor(
    id: "CUSTOM001",
    title: "Custom Warning",
    messageFormat: "This is a custom warning: {0}",
    category: "Usage",
    defaultSeverity: DiagnosticSeverity.Warning,
    isEnabledByDefault: true,
    description: "A custom diagnostic for demonstration"
);

// Create a diagnostic using the descriptor
var location = Location.None; // or a real location from the tree
var diagnostic = Diagnostic.Create(
    descriptor,
    location,
    "example message"
);

Console.WriteLine(diagnostic.GetMessage());
// Output: This is a custom warning: example message

Filtering and Analyzing Diagnostics

Filter by Severity

var tree = LuaSyntaxTree.ParseText(code);
var diagnostics = tree.GetDiagnostics();

// Only errors
var errors = diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error);
Console.WriteLine($"Found {errors.Count()} errors");

// Warnings and errors
var issues = diagnostics.Where(d => 
    d.Severity == DiagnosticSeverity.Error ||
    d.Severity == DiagnosticSeverity.Warning
);

Group by Location

var diagnostics = tree.GetDiagnostics();

// Group diagnostics by line number
var byLine = diagnostics
    .GroupBy(d => d.Location.GetLineSpan().StartLinePosition.Line);

foreach (var lineGroup in byLine.OrderBy(g => g.Key))
{
    Console.WriteLine($"Line {lineGroup.Key + 1}:");
    foreach (var diagnostic in lineGroup)
    {
        Console.WriteLine($"  {diagnostic.GetMessage()}");
    }
}

Check if Code is Valid

var tree = LuaSyntaxTree.ParseText(code);
var hasErrors = tree.GetDiagnostics()
    .Any(d => d.Severity == DiagnosticSeverity.Error);

if (hasErrors)
{
    Console.WriteLine("Code has syntax errors");
}
else
{
    Console.WriteLine("Code is syntactically valid");
}

Complete Diagnostic Example

Here’s a comprehensive example showing diagnostic analysis:
using Loretta.CodeAnalysis;
using Loretta.CodeAnalysis.Lua;
using Loretta.CodeAnalysis.Text;

class DiagnosticAnalyzer
{
    static void AnalyzeCode(string code, string fileName = "script.lua")
    {
        var tree = LuaSyntaxTree.ParseText(
            code,
            new LuaParseOptions(LuaSyntaxOptions.Lua54),
            fileName
        );
        
        var diagnostics = tree.GetDiagnostics().ToList();
        
        Console.WriteLine($"Analysis of {fileName}:");
        Console.WriteLine($"Total diagnostics: {diagnostics.Count}");
        Console.WriteLine();
        
        // Count by severity
        var bySeverity = diagnostics.GroupBy(d => d.Severity);
        foreach (var group in bySeverity)
        {
            Console.WriteLine($"{group.Key}: {group.Count()}");
        }
        Console.WriteLine();
        
        // Show all diagnostics with details
        foreach (var diagnostic in diagnostics)
        {
            var lineSpan = diagnostic.Location.GetLineSpan();
            var line = lineSpan.StartLinePosition.Line + 1;
            var col = lineSpan.StartLinePosition.Character + 1;
            
            Console.WriteLine($"[{diagnostic.Severity}] {fileName}:{line}:{col}");
            Console.WriteLine($"  {diagnostic.Id}: {diagnostic.GetMessage()}");
            
            // Show the problematic code
            if (tree.TryGetText(out var sourceText))
            {
                var span = diagnostic.Location.SourceSpan;
                var lineText = sourceText.Lines[lineSpan.StartLinePosition.Line].ToString();
                Console.WriteLine($"  > {lineText}");
                Console.WriteLine($"    {new string(' ', col - 1)}^");
            }
            Console.WriteLine();
        }
    }
    
    static void Main()
    {
        var code = @"
local x = 10
local y =     -- missing value
local z = 20 +  -- incomplete expression

function test(
    -- missing closing paren
    print('test')
";
        
        AnalyzeCode(code, "example.lua");
    }
}
Output:
Analysis of example.lua:
Total diagnostics: 3

Error: 3

[Error] example.lua:3:14
  LUA0001: Unexpected token
  > local y =     -- missing value
                ^

[Error] example.lua:4:21
  LUA0001: Unexpected end of line
  > local z = 20 +  -- incomplete expression
                     ^

[Error] example.lua:7:5
  LUA0002: Expected ')' to close '('
  >     print('test')
      ^

Best Practices

Always Check for Errors

Before processing a syntax tree, check if it has errors:
var tree = LuaSyntaxTree.ParseText(code);
if (tree.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error))
{
    // Handle errors before proceeding
    Console.WriteLine("Cannot process code with syntax errors");
    return;
}

// Safe to proceed with analysis
var root = tree.GetRoot();
// ...

Provide User-Friendly Error Messages

var diagnostics = tree.GetDiagnostics()
    .Where(d => d.Severity == DiagnosticSeverity.Error)
    .ToList();

if (diagnostics.Any())
{
    Console.WriteLine($"Found {diagnostics.Count} error(s):\n");
    
    foreach (var diagnostic in diagnostics)
    {
        var lineSpan = diagnostic.Location.GetLineSpan();
        Console.WriteLine(
            $"  Line {lineSpan.StartLinePosition.Line + 1}: {diagnostic.GetMessage()}"
        );
    }
}

Use Diagnostic IDs for Specific Handling

var diagnostics = tree.GetDiagnostics();

foreach (var diagnostic in diagnostics)
{
    switch (diagnostic.Id)
    {
        case "LUA0001":
            // Handle specific error type
            HandleUnexpectedToken(diagnostic);
            break;
        default:
            // Generic handling
            Console.WriteLine(diagnostic.GetMessage());
            break;
    }
}

See Also

  • Parsing - How diagnostics are generated during parsing
  • Syntax Trees - Understanding the structure that produces diagnostics

Build docs developers (and LLMs) love