...
This commit is contained in:
325
compiler/NubLang/Diagnostics/Diagnostic.cs
Normal file
325
compiler/NubLang/Diagnostics/Diagnostic.cs
Normal file
@@ -0,0 +1,325 @@
|
||||
using System.Text;
|
||||
using NubLang.Code;
|
||||
using NubLang.Parsing.Syntax;
|
||||
using NubLang.Tokenization;
|
||||
|
||||
namespace NubLang.Diagnostics;
|
||||
|
||||
public class Diagnostic
|
||||
{
|
||||
public class DiagnosticBuilder
|
||||
{
|
||||
private readonly DiagnosticSeverity _severity;
|
||||
private readonly string _message;
|
||||
private SourceFileSpan? _fileSpan;
|
||||
private string? _help;
|
||||
|
||||
public DiagnosticBuilder(DiagnosticSeverity severity, string message)
|
||||
{
|
||||
_severity = severity;
|
||||
_message = message;
|
||||
}
|
||||
|
||||
public DiagnosticBuilder At(SyntaxNode? node)
|
||||
{
|
||||
if (node != null)
|
||||
{
|
||||
var first = node.Tokens.FirstOrDefault();
|
||||
if (first?.FileSpan != null)
|
||||
{
|
||||
var span = SourceSpan.Merge(node.Tokens.Select(x => x.FileSpan?.Span ?? SourceSpan.Zero));
|
||||
At(new SourceFileSpan(first.FileSpan.SourceFile, span));
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public DiagnosticBuilder At(Token? token)
|
||||
{
|
||||
if (token != null)
|
||||
{
|
||||
At(token.FileSpan);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public DiagnosticBuilder At(SourceFileSpan? fileSpan)
|
||||
{
|
||||
if (fileSpan != null)
|
||||
{
|
||||
_fileSpan = fileSpan;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public DiagnosticBuilder WithHelp(string help)
|
||||
{
|
||||
_help = help;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Diagnostic Build() => new(_severity, _message, _help, _fileSpan);
|
||||
}
|
||||
|
||||
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 string? Help { get; }
|
||||
public SourceFileSpan? FileSpan { get; }
|
||||
|
||||
private Diagnostic(DiagnosticSeverity severity, string message, string? help, SourceFileSpan? fileSpan)
|
||||
{
|
||||
Severity = severity;
|
||||
Message = message;
|
||||
Help = help;
|
||||
FileSpan = fileSpan;
|
||||
}
|
||||
|
||||
public string FormatANSI()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.Append(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),
|
||||
_ => ConsoleColors.Colorize("unknown", ConsoleColors.Bold + ConsoleColors.White)
|
||||
});
|
||||
|
||||
if (FileSpan != null)
|
||||
{
|
||||
sb.Append(ConsoleColors.Colorize($" at {FileSpan.SourceFile.Path}:{FileSpan.Span}", ConsoleColors.Faint));
|
||||
}
|
||||
|
||||
sb.Append(": ");
|
||||
sb.Append(ConsoleColors.Colorize(Message, ConsoleColors.BrightWhite));
|
||||
|
||||
if (FileSpan != null)
|
||||
{
|
||||
sb.AppendLine();
|
||||
var text = FileSpan.SourceFile.GetText();
|
||||
|
||||
var lines = text.Split('\n');
|
||||
|
||||
var startLine = FileSpan.Span.Start.Line;
|
||||
var endLine = FileSpan.Span.End.Line;
|
||||
|
||||
const int CONTEXT_LINES = 3;
|
||||
|
||||
var contextStartLine = Math.Max(1, startLine - CONTEXT_LINES);
|
||||
var contextEndLine = Math.Min(lines.Length, endLine + CONTEXT_LINES);
|
||||
|
||||
var numberPadding = contextEndLine.ToString().Length;
|
||||
var codePadding = lines.Skip(contextStartLine - 1).Take(contextEndLine - contextStartLine).Max(x => x.Length);
|
||||
|
||||
sb.Append('╭');
|
||||
sb.Append(new string('─', numberPadding + 2));
|
||||
sb.Append('┬');
|
||||
sb.Append(new string('─', codePadding + 2));
|
||||
sb.Append('╮');
|
||||
sb.AppendLine();
|
||||
|
||||
var tokenizer = new Tokenizer(FileSpan.SourceFile);
|
||||
var tokens = tokenizer.Tokenize().ToList();
|
||||
|
||||
for (var i = contextStartLine; i <= contextEndLine; i++)
|
||||
{
|
||||
var line = lines[i - 1];
|
||||
|
||||
sb.Append("│ ");
|
||||
sb.Append(i.ToString().PadRight(numberPadding));
|
||||
sb.Append(" │ ");
|
||||
sb.Append(ApplySyntaxHighlighting(line.PadRight(codePadding), i, tokens));
|
||||
sb.Append(" │");
|
||||
sb.AppendLine();
|
||||
|
||||
if (i >= startLine && i <= endLine)
|
||||
{
|
||||
var markerStartColumn = 1;
|
||||
var markerEndColumn = line.Length + 1;
|
||||
|
||||
if (i == startLine)
|
||||
{
|
||||
markerStartColumn = FileSpan.Span.Start.Column;
|
||||
}
|
||||
|
||||
if (i == endLine)
|
||||
{
|
||||
markerEndColumn = FileSpan.Span.End.Column;
|
||||
}
|
||||
|
||||
var markerLength = markerEndColumn - markerStartColumn;
|
||||
var marker = new string('^', markerLength);
|
||||
|
||||
var markerColor = Severity switch
|
||||
{
|
||||
DiagnosticSeverity.Info => ConsoleColors.Blue,
|
||||
DiagnosticSeverity.Warning => ConsoleColors.Yellow,
|
||||
DiagnosticSeverity.Error => ConsoleColors.Red,
|
||||
_ => ConsoleColors.White
|
||||
};
|
||||
|
||||
sb.Append("│ ");
|
||||
sb.Append(new string(' ', numberPadding));
|
||||
sb.Append(" │ ");
|
||||
sb.Append(new string(' ', markerStartColumn - 1));
|
||||
sb.Append(ConsoleColors.Colorize(marker, markerColor));
|
||||
sb.Append(new string(' ', codePadding - markerEndColumn + 1));
|
||||
sb.Append(" │");
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
sb.Append('╰');
|
||||
sb.Append(new string('─', numberPadding + 2));
|
||||
sb.Append('┴');
|
||||
sb.Append(new string('─', codePadding + 2));
|
||||
sb.Append('╯');
|
||||
}
|
||||
|
||||
if (Help != null)
|
||||
{
|
||||
sb.AppendLine();
|
||||
sb.Append(ConsoleColors.Colorize($"help: {Help}", ConsoleColors.Cyan));
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string ApplySyntaxHighlighting(string line, int lineNumber, List<Token> tokens)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var lineTokens = tokens
|
||||
.Where(t => t.FileSpan.Span.Start.Line == lineNumber)
|
||||
.OrderBy(t => t.FileSpan.Span.Start.Column)
|
||||
.ToList();
|
||||
|
||||
if (lineTokens.Count == 0)
|
||||
{
|
||||
return line;
|
||||
}
|
||||
|
||||
var currentColumn = 1;
|
||||
|
||||
foreach (var token in lineTokens)
|
||||
{
|
||||
var tokenStart = token.FileSpan.Span.Start.Column;
|
||||
var tokenEnd = token.FileSpan.Span.End.Column;
|
||||
|
||||
if (tokenStart > currentColumn)
|
||||
{
|
||||
var beforeToken = line.Substring(currentColumn - 1, tokenStart - currentColumn);
|
||||
sb.Append(beforeToken);
|
||||
}
|
||||
|
||||
var tokenLength = tokenEnd - tokenStart;
|
||||
if (tokenStart - 1 + tokenLength <= line.Length)
|
||||
{
|
||||
var tokenText = line.Substring(tokenStart - 1, tokenLength);
|
||||
|
||||
var coloredToken = ColorizeToken(token, tokenText);
|
||||
sb.Append(coloredToken);
|
||||
}
|
||||
|
||||
currentColumn = tokenEnd;
|
||||
}
|
||||
|
||||
if (currentColumn <= line.Length)
|
||||
{
|
||||
var remaining = line[(currentColumn - 1)..];
|
||||
sb.Append(remaining);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string ColorizeToken(Token token, string tokenText)
|
||||
{
|
||||
switch (token)
|
||||
{
|
||||
case IdentifierToken:
|
||||
{
|
||||
return ConsoleColors.Colorize(tokenText, ConsoleColors.BrightWhite);
|
||||
}
|
||||
case LiteralToken literal:
|
||||
{
|
||||
if (literal.Kind == LiteralKind.String)
|
||||
{
|
||||
return ConsoleColors.Colorize(tokenText, ConsoleColors.Green);
|
||||
}
|
||||
|
||||
return ConsoleColors.Colorize(tokenText, ConsoleColors.Magenta);
|
||||
}
|
||||
case SymbolToken symbolToken:
|
||||
{
|
||||
switch (symbolToken.Symbol)
|
||||
{
|
||||
case Symbol.Func:
|
||||
case Symbol.Return:
|
||||
case Symbol.If:
|
||||
case Symbol.Else:
|
||||
case Symbol.While:
|
||||
case Symbol.Break:
|
||||
case Symbol.Continue:
|
||||
case Symbol.Struct:
|
||||
case Symbol.Let:
|
||||
case Symbol.Calls:
|
||||
case Symbol.Interface:
|
||||
case Symbol.For:
|
||||
case Symbol.Extern:
|
||||
{
|
||||
return ConsoleColors.Colorize(tokenText, ConsoleColors.Bold + ConsoleColors.Blue);
|
||||
}
|
||||
case Symbol.Assign:
|
||||
case Symbol.Bang:
|
||||
case Symbol.Equal:
|
||||
case Symbol.NotEqual:
|
||||
case Symbol.LessThan:
|
||||
case Symbol.LessThanOrEqual:
|
||||
case Symbol.GreaterThan:
|
||||
case Symbol.GreaterThanOrEqual:
|
||||
case Symbol.Plus:
|
||||
case Symbol.Minus:
|
||||
case Symbol.Star:
|
||||
case Symbol.ForwardSlash:
|
||||
case Symbol.Caret:
|
||||
case Symbol.Ampersand:
|
||||
{
|
||||
return ConsoleColors.Colorize(tokenText, ConsoleColors.Yellow);
|
||||
}
|
||||
case Symbol.Colon:
|
||||
case Symbol.OpenParen:
|
||||
case Symbol.CloseParen:
|
||||
case Symbol.OpenBrace:
|
||||
case Symbol.CloseBrace:
|
||||
case Symbol.OpenBracket:
|
||||
case Symbol.CloseBracket:
|
||||
case Symbol.Comma:
|
||||
case Symbol.Period:
|
||||
case Symbol.Semi:
|
||||
{
|
||||
return ConsoleColors.Colorize(tokenText, ConsoleColors.BrightBlack);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return tokenText;
|
||||
}
|
||||
}
|
||||
|
||||
public enum DiagnosticSeverity
|
||||
{
|
||||
Info,
|
||||
Warning,
|
||||
Error
|
||||
}
|
||||
Reference in New Issue
Block a user