using System.Text; using Nub.Lang.Frontend.Lexing; using Nub.Lang.Frontend.Parsing; namespace Nub.Lang.Diagnostics; public class Diagnostic { public class DiagnosticBuilder { private readonly DiagnosticSeverity _severity; private readonly string _message; private string? _help; private SourceSpan? _sourceSpan; public DiagnosticBuilder(DiagnosticSeverity severity, string message) { _severity = severity; _message = message; } public DiagnosticBuilder At(Token token) { _sourceSpan = token.Span; return this; } public DiagnosticBuilder At(Node node) { _sourceSpan = SourceSpan.Merge(node.Tokens.Select(t => t.Span)); return this; } public DiagnosticBuilder At(SourceSpan span) { _sourceSpan = span; return this; } public DiagnosticBuilder WithHelp(string help) { _help = help; return this; } public Diagnostic Build() => new(_severity, _message, _sourceSpan, _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 SourceSpan? Span { get; } public string? Help { get; } private Diagnostic(DiagnosticSeverity severity, string message, SourceSpan? span, string? help) { Severity = severity; Message = message; Span = span; Help = help; } public string Format() { var sb = new StringBuilder(); var severityText = GetSeverityText(Severity); sb.Append(severityText); if (Span.HasValue) { var locationText = $" at {Span.Value.Text.Name}:{Span}"; sb.Append(ConsoleColors.Colorize(locationText, ConsoleColors.Faint)); } sb.Append(": "); sb.Append(ConsoleColors.Colorize(Message, ConsoleColors.BrightWhite)); if (Span.HasValue) { sb.AppendLine(); AppendSourceContext(sb, Span.Value); } if (!string.IsNullOrEmpty(Help)) { sb.AppendLine(); sb.Append(ConsoleColors.Colorize($"help: {Help}", 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, SourceSpan span) { var lines = span.Text.Content.Split('\n'); var startLine = span.Start.Line; var endLine = span.End.Line; const int contextLines = 3; var lineNumWidth = Math.Min(endLine + contextLines, lines.Length).ToString().Length; var contextStart = Math.Max(1, startLine - contextLines); var contextEnd = Math.Min(lines.Length, endLine + contextLines); var contextWidth = 0; for (var i = contextStart; i < contextEnd; i++) { if (lines[i - 1].Length > contextWidth) { contextWidth = lines[i - 1].Length; } } sb.AppendLine(ConsoleColors.Colorize('╭' + new string('─', lineNumWidth + 2) + '┬' + new string('─', contextWidth + 2) + '╮', ConsoleColors.Faint)); for (var lineNum = contextStart; lineNum < startLine; lineNum++) { AppendContextLine(sb, lineNum, lines[lineNum - 1], lineNumWidth, contextWidth); } for (var lineNum = startLine; lineNum <= endLine; lineNum++) { AppendContextLine(sb, lineNum, lines[lineNum - 1], lineNumWidth, contextWidth); AppendErrorIndicators(sb, span, lineNum, lines[lineNum - 1], lineNumWidth, contextWidth); } for (var lineNum = endLine + 1; lineNum <= contextEnd; lineNum++) { AppendContextLine(sb, lineNum, lines[lineNum - 1], lineNumWidth, contextWidth); } sb.Append(ConsoleColors.Colorize('╰' + new string('─', lineNumWidth + 2) + '┴' + new string('─', contextWidth + 2) + '╯', ConsoleColors.Faint)); } private static void AppendContextLine(StringBuilder sb, int lineNum, string line, int lineNumWidth, int contextWidth) { sb.Append(ConsoleColors.Colorize('│' + " ", ConsoleColors.Faint)); var lineNumStr = lineNum.ToString().PadLeft(lineNumWidth); sb.Append(ConsoleColors.Colorize(lineNumStr, ConsoleColors.Faint)); sb.Append(ConsoleColors.Colorize(" │ ", ConsoleColors.Faint)); sb.Append(ConsoleColors.ColorizeSource(line.PadRight(contextWidth))); sb.Append(ConsoleColors.Colorize(" " + '│', ConsoleColors.Faint)); sb.AppendLine(); } private static void AppendErrorIndicators(StringBuilder sb, SourceSpan span, int lineNum, string line, int lineNumWidth, int contextWidth) { sb.Append(ConsoleColors.Colorize('│' + " ", ConsoleColors.Faint)); sb.Append(new string(' ', lineNumWidth)); sb.Append(ConsoleColors.Colorize(" │ ", ConsoleColors.Faint)); var indicators = GetIndicatorsForLine(span, lineNum, line); sb.Append(ConsoleColors.Colorize(indicators.PadRight(contextWidth), ConsoleColors.Red)); sb.Append(ConsoleColors.Colorize(" " + '│', ConsoleColors.Faint)); sb.AppendLine(); } 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); } } public enum DiagnosticSeverity { Info, Warning, Error }