Files
nub-lang/src/compiler/Nub.Lang/Frontend/Diagnostics/Diagnostic.cs
nub31 955869a3cf ...
2025-05-26 19:16:56 +02:00

205 lines
6.6 KiB
C#

using System.Text;
using Nub.Lang.Frontend.Lexing;
using Nub.Lang.Frontend.Parsing;
namespace Nub.Lang.Frontend.Diagnostics;
public enum DiagnosticSeverity
{
Info,
Warning,
Error
}
public class Diagnostic
{
public class DiagnosticBuilder
{
private readonly DiagnosticSeverity _severity;
private readonly string _message;
private SourceText? _sourceFile;
private SourceSpan? _span;
private string? _help;
public DiagnosticBuilder(DiagnosticSeverity severity, string message)
{
_severity = severity;
_message = message;
}
public DiagnosticBuilder At(Token token)
{
_sourceFile = token.SourceText;
_span = SourceLocationCalculator.GetSpan(token);
return this;
}
public DiagnosticBuilder At(Node node)
{
if (!node.Tokens.Any())
{
throw new ArgumentException("Node has no tokens", nameof(node));
}
_sourceFile = node.Tokens[0].SourceText;
_span = SourceLocationCalculator.GetSpan(node);
return this;
}
public DiagnosticBuilder At(SourceText sourceText, SourceSpan span)
{
_sourceFile = sourceText;
_span = span;
return this;
}
public DiagnosticBuilder WithHelp(string help)
{
_help = help;
return this;
}
public Diagnostic Build() => new(_severity, _message, _sourceFile, _span, _help);
}
public static DiagnosticBuilder Error(string message) => new(DiagnosticSeverity.Error, message);
public static DiagnosticBuilder Warning(string message) => new(DiagnosticSeverity.Warning, message);
public static DiagnosticBuilder Info(string message) => new(DiagnosticSeverity.Info, message);
public DiagnosticSeverity Severity { get; }
public string Message { get; }
public SourceText? SourceFile { get; }
public SourceSpan? Span { get; }
public string? Help { get; }
private Diagnostic(DiagnosticSeverity severity, string message, SourceText? sourceFile, SourceSpan? span, string? help)
{
Severity = severity;
Message = message;
SourceFile = sourceFile;
Span = span;
Help = help;
}
public string Format()
{
var sb = new StringBuilder();
var severityText = GetSeverityText(Severity);
sb.Append(severityText);
if (SourceFile.HasValue)
{
var locationText = $" at {SourceFile.Value.Path}:{Span}";
sb.Append(ConsoleColors.Colorize(locationText, ConsoleColors.Gray));
}
sb.Append(": ");
sb.AppendLine(ConsoleColors.Colorize(Message, ConsoleColors.BrightWhite));
if (SourceFile.HasValue && Span.HasValue)
{
AppendSourceContext(sb, SourceFile.Value, Span.Value);
}
if (!string.IsNullOrEmpty(Help))
{
sb.AppendLine();
var helpText = $"help: {Help}";
sb.AppendLine(ConsoleColors.Colorize(helpText, ConsoleColors.Cyan));
}
return sb.ToString();
}
private static string GetSeverityText(DiagnosticSeverity severity)
{
return severity switch
{
DiagnosticSeverity.Error => ConsoleColors.Colorize("error", ConsoleColors.Bold + ConsoleColors.Red),
DiagnosticSeverity.Warning => ConsoleColors.Colorize("warning", ConsoleColors.Bold + ConsoleColors.Yellow),
DiagnosticSeverity.Info => ConsoleColors.Colorize("info", ConsoleColors.Bold + ConsoleColors.Blue),
_ => throw new ArgumentOutOfRangeException(nameof(severity), severity, "Unknown diagnostic severity")
};
}
private static void AppendSourceContext(StringBuilder sb, SourceText sourceText, SourceSpan span)
{
var lines = sourceText.Content.Split('\n');
var startLine = span.Start.Line;
var endLine = span.End.Line;
const int contextLines = 3;
if (startLine < 1 || startLine > lines.Length || endLine < 1 || endLine > lines.Length)
{
return;
}
var maxLineNum = Math.Min(endLine + contextLines, lines.Length);
var lineNumWidth = maxLineNum.ToString().Length;
var contextStart = Math.Max(1, startLine - contextLines);
for (var lineNum = contextStart; lineNum < startLine; lineNum++)
{
AppendContextLine(sb, lineNum, lines[lineNum - 1], lineNumWidth);
}
for (var lineNum = startLine; lineNum <= endLine; lineNum++)
{
var line = lines[lineNum - 1];
AppendContextLine(sb, lineNum, line, lineNumWidth);
AppendErrorIndicators(sb, span, lineNum, line, lineNumWidth);
}
var contextEnd = Math.Min(lines.Length, endLine + contextLines);
for (var lineNum = endLine + 1; lineNum <= contextEnd; lineNum++)
{
AppendContextLine(sb, lineNum, lines[lineNum - 1], lineNumWidth);
}
}
private static void AppendContextLine(StringBuilder sb, int lineNum, string line, int lineNumWidth)
{
var lineNumStr = lineNum.ToString().PadLeft(lineNumWidth);
sb.Append(ConsoleColors.Colorize(lineNumStr, ConsoleColors.Gray));
sb.Append(ConsoleColors.Colorize(" | ", ConsoleColors.Gray));
sb.AppendLine(line);
}
private static void AppendErrorIndicators(StringBuilder sb, SourceSpan span, int lineNum, string line, int lineNumWidth)
{
sb.Append(new string(' ', lineNumWidth + 3));
var indicators = GetIndicatorsForLine(span, lineNum, line);
sb.AppendLine(ConsoleColors.Colorize(indicators, ConsoleColors.Red));
}
private static string GetIndicatorsForLine(SourceSpan span, int lineNum, string line)
{
const char indicator = '^';
if (lineNum == span.Start.Line && lineNum == span.End.Line)
{
var startCol = Math.Max(0, span.Start.Column - 1);
var endCol = Math.Min(line.Length, span.End.Column - 1);
var length = Math.Max(1, endCol - startCol);
return new string(' ', startCol) + new string(indicator, length);
}
if (lineNum == span.Start.Line)
{
var startCol = Math.Max(0, span.Start.Column - 1);
return new string(' ', startCol) + new string(indicator, Math.Max(0, line.Length - startCol));
}
if (lineNum == span.End.Line)
{
var endCol = Math.Min(line.Length, span.End.Column - 1);
return new string(indicator, Math.Max(0, endCol));
}
return new string(indicator, line.Length);
}
}