274 lines
7.7 KiB
C#
274 lines
7.7 KiB
C#
using System.Diagnostics.CodeAnalysis;
|
|
|
|
namespace Nub.Lang;
|
|
|
|
/// <summary>
|
|
/// Represents a location in source code with line and column information.
|
|
/// Lines and columns are 1-based to match typical editor conventions.
|
|
/// </summary>
|
|
public readonly struct SourceLocation : IEquatable<SourceLocation>, IComparable<SourceLocation>
|
|
{
|
|
public SourceLocation(int line, int column)
|
|
{
|
|
if (line < 1)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(line), "Line must be >= 1");
|
|
}
|
|
|
|
if (column < 1)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(column), "Column must be >= 1");
|
|
}
|
|
|
|
Line = line;
|
|
Column = column;
|
|
}
|
|
|
|
public int Line { get; }
|
|
public int Column { get; }
|
|
|
|
public int CompareTo(SourceLocation other)
|
|
{
|
|
var lineComparison = Line.CompareTo(other.Line);
|
|
if (lineComparison == 0)
|
|
{
|
|
return Column.CompareTo(other.Column);
|
|
}
|
|
|
|
return lineComparison;
|
|
}
|
|
|
|
public override string ToString()
|
|
{
|
|
return $"{Line}:{Column}";
|
|
}
|
|
|
|
public bool Equals(SourceLocation other)
|
|
{
|
|
return Line == other.Line && Column == other.Column;
|
|
}
|
|
|
|
public override bool Equals([NotNullWhen(true)] object? obj)
|
|
{
|
|
return obj is SourceLocation other && Equals(other);
|
|
}
|
|
|
|
public override int GetHashCode()
|
|
{
|
|
return HashCode.Combine(Line, Column);
|
|
}
|
|
|
|
public static bool operator ==(SourceLocation left, SourceLocation right) => left.Equals(right);
|
|
public static bool operator !=(SourceLocation left, SourceLocation right) => !left.Equals(right);
|
|
public static bool operator <(SourceLocation left, SourceLocation right) => left.CompareTo(right) < 0;
|
|
public static bool operator >(SourceLocation left, SourceLocation right) => left.CompareTo(right) > 0;
|
|
public static bool operator <=(SourceLocation left, SourceLocation right) => left.CompareTo(right) <= 0;
|
|
public static bool operator >=(SourceLocation left, SourceLocation right) => left.CompareTo(right) >= 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents source text with a name (typically filename) and content.
|
|
/// Equality is based on both name and content for better semantics.
|
|
/// </summary>
|
|
public struct SourceText : IEquatable<SourceText>
|
|
{
|
|
private int _lines = -1;
|
|
|
|
public SourceText(string path, string content)
|
|
{
|
|
Path = path ?? throw new ArgumentNullException(nameof(path));
|
|
Content = content ?? throw new ArgumentNullException(nameof(content));
|
|
}
|
|
|
|
public string Path { get; }
|
|
public string Content { get; }
|
|
|
|
public int LineCount()
|
|
{
|
|
if (_lines == -1)
|
|
{
|
|
_lines = Content.Split('\n').Length + 1;
|
|
}
|
|
|
|
return _lines;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a specific line from the source text (1-based).
|
|
/// </summary>
|
|
public string GetLine(int lineNumber)
|
|
{
|
|
if (lineNumber < 1)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(lineNumber));
|
|
}
|
|
|
|
var lines = Content.Split('\n');
|
|
return lineNumber <= lines.Length ? lines[lineNumber - 1] : string.Empty;
|
|
}
|
|
|
|
public bool Equals(SourceText other)
|
|
{
|
|
return Path == other.Path && Content == other.Content;
|
|
}
|
|
|
|
public override bool Equals([NotNullWhen(true)] object? obj)
|
|
{
|
|
return obj is SourceText other && Equals(other);
|
|
}
|
|
|
|
public override int GetHashCode()
|
|
{
|
|
return HashCode.Combine(Path, Content);
|
|
}
|
|
|
|
public override string ToString()
|
|
{
|
|
return Path;
|
|
}
|
|
|
|
public static bool operator ==(SourceText left, SourceText right) => left.Equals(right);
|
|
public static bool operator !=(SourceText left, SourceText right) => !left.Equals(right);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents a span of source code from a start to end location within a source text.
|
|
/// </summary>
|
|
public readonly struct SourceSpan : IEquatable<SourceSpan>
|
|
{
|
|
public SourceSpan(SourceText text, SourceLocation start, SourceLocation end)
|
|
{
|
|
if (start > end)
|
|
{
|
|
throw new ArgumentException("Start location cannot be after end location");
|
|
}
|
|
|
|
if (end.Line > text.LineCount() || end.Line == text.LineCount() && end.Column > text.GetLine(text.LineCount()).Length + 1)
|
|
{
|
|
throw new ArgumentException("End location cannot be after text end location");
|
|
}
|
|
|
|
Text = text;
|
|
Start = start;
|
|
End = end;
|
|
}
|
|
|
|
public SourceText Text { get; }
|
|
public SourceLocation Start { get; }
|
|
public SourceLocation End { get; }
|
|
|
|
/// <summary>
|
|
/// Gets whether this span represents a single point (start == end).
|
|
/// </summary>
|
|
public bool IsEmpty => Start == End;
|
|
|
|
/// <summary>
|
|
/// Gets whether this span is contained within a single line.
|
|
/// </summary>
|
|
public bool IsSingleLine => Start.Line == End.Line;
|
|
|
|
/// <summary>
|
|
/// Gets the text content covered by this span.
|
|
/// </summary>
|
|
public string GetText()
|
|
{
|
|
if (IsEmpty)
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
var lines = Text.Content.Split('\n');
|
|
|
|
if (IsSingleLine)
|
|
{
|
|
var line = lines[Start.Line - 1];
|
|
var startCol = Math.Min(Start.Column - 1, line.Length);
|
|
var endCol = Math.Min(End.Column - 1, line.Length);
|
|
return line.Substring(startCol, Math.Max(0, endCol - startCol));
|
|
}
|
|
|
|
var result = new List<string>();
|
|
for (var i = Start.Line - 1; i < Math.Min(End.Line, lines.Length); i++)
|
|
{
|
|
var line = lines[i];
|
|
if (i == Start.Line - 1)
|
|
{
|
|
result.Add(line[Math.Min(Start.Column - 1, line.Length)..]);
|
|
}
|
|
else if (i == End.Line - 1)
|
|
{
|
|
result.Add(line[..Math.Min(End.Column - 1, line.Length)]);
|
|
}
|
|
else
|
|
{
|
|
result.Add(line);
|
|
}
|
|
}
|
|
|
|
return string.Join("\n", result);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Merges multiple source spans from the same file into a single span.
|
|
/// The result spans from the earliest start to the latest end.
|
|
/// </summary>
|
|
public static SourceSpan Merge(IEnumerable<SourceSpan> spans)
|
|
{
|
|
var spanArray = spans.ToArray();
|
|
if (spanArray.Length == 0)
|
|
{
|
|
throw new ArgumentException("Cannot merge empty collection of spans", nameof(spans));
|
|
}
|
|
|
|
var firstText = spanArray[0].Text;
|
|
if (spanArray.Any(s => !s.Text.Equals(firstText)))
|
|
{
|
|
throw new ArgumentException("All spans must be from the same source text", nameof(spans));
|
|
}
|
|
|
|
var minStart = spanArray.Min(s => s.Start);
|
|
var maxEnd = spanArray.Max(s => s.End);
|
|
|
|
return new SourceSpan(firstText, minStart, maxEnd);
|
|
}
|
|
|
|
public override string ToString()
|
|
{
|
|
if (IsEmpty)
|
|
{
|
|
return $"{Start}";
|
|
}
|
|
|
|
if (IsSingleLine)
|
|
{
|
|
return Start.Column == End.Column ? $"{Start}" : $"{Start.Line}:{Start.Column}-{End.Column}";
|
|
}
|
|
|
|
return $"{Start}-{End}";
|
|
}
|
|
|
|
public bool Equals(SourceSpan other)
|
|
{
|
|
return Text.Equals(other.Text) && Start.Equals(other.Start) && End.Equals(other.End);
|
|
}
|
|
|
|
public override bool Equals([NotNullWhen(true)] object? obj)
|
|
{
|
|
return obj is SourceSpan other && Equals(other);
|
|
}
|
|
|
|
public override int GetHashCode()
|
|
{
|
|
return HashCode.Combine(Text, Start, End);
|
|
}
|
|
|
|
public static bool operator ==(SourceSpan left, SourceSpan right)
|
|
{
|
|
return left.Equals(right);
|
|
}
|
|
|
|
public static bool operator !=(SourceSpan left, SourceSpan right)
|
|
{
|
|
return !left.Equals(right);
|
|
}
|
|
} |