207 lines
7.1 KiB
C#
207 lines
7.1 KiB
C#
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
|
|
} |