...
This commit is contained in:
@@ -2,55 +2,249 @@ using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Nub.Lang;
|
||||
|
||||
public readonly struct SourceSpan(SourceText text, SourceLocation start, SourceLocation end) : IEquatable<SourceSpan>
|
||||
/// <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 SourceText Text { get; } = text;
|
||||
public SourceLocation Start { get; } = start;
|
||||
public SourceLocation End { get; } = end;
|
||||
|
||||
/// <summary>
|
||||
/// Merges one or more <see cref="SourceSpan"/> from a single file to a single <see cref="SourceSpan"/>
|
||||
/// </summary>
|
||||
/// <param name="spanEnumerable">The spans to merged</param>
|
||||
/// <returns>The merged <see cref="SourceSpan"/></returns>
|
||||
public static SourceSpan Merge(IEnumerable<SourceSpan> spanEnumerable)
|
||||
public SourceLocation(int line, int column)
|
||||
{
|
||||
var spans = spanEnumerable.ToArray();
|
||||
if (spans.Length == 0)
|
||||
if (line < 1)
|
||||
{
|
||||
throw new ArgumentException("Cannot merge empty spans", nameof(spanEnumerable));
|
||||
throw new ArgumentOutOfRangeException(nameof(line), "Line must be >= 1");
|
||||
}
|
||||
|
||||
var files = spans.Select(s => s.Text).Distinct().ToArray();
|
||||
if (files.Length > 1)
|
||||
if (column < 1)
|
||||
{
|
||||
throw new ArgumentException("Cannot merge spans from multiple files", nameof(spanEnumerable));
|
||||
throw new ArgumentOutOfRangeException(nameof(column), "Column must be >= 1");
|
||||
}
|
||||
|
||||
var first = spans.OrderBy(s => s.Start.Line).ThenBy(s => s.Start.Column).First().Start;
|
||||
var last = spans.OrderByDescending(s => s.End.Line).ThenByDescending(s => s.End.Column).Last().End;
|
||||
Line = line;
|
||||
Column = column;
|
||||
}
|
||||
|
||||
return new SourceSpan(files[0], first, last);
|
||||
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()
|
||||
{
|
||||
if (Start.Line == End.Line)
|
||||
{
|
||||
if (Start.Column == End.Column)
|
||||
{
|
||||
return $"{Start.Line}:{Start.Column}";
|
||||
return $"{Line}:{Column}";
|
||||
}
|
||||
|
||||
return $"{Start.Line}:{Start.Column}-{End.Column}";
|
||||
}
|
||||
|
||||
return $"{Start.Line}:{Start.Column}-{End.Line}:{End.Column}";
|
||||
public bool Equals(SourceLocation other)
|
||||
{
|
||||
return Line == other.Line && Column == other.Column;
|
||||
}
|
||||
|
||||
public override bool Equals([NotNullWhen(true)] object? obj)
|
||||
{
|
||||
return obj is SourceSpan other && Equals(other);
|
||||
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 name, string content)
|
||||
{
|
||||
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||
Content = content ?? throw new ArgumentNullException(nameof(content));
|
||||
}
|
||||
|
||||
public string Name { 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 Name == other.Name && 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(Name, Content);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -58,6 +252,11 @@ public readonly struct SourceSpan(SourceText text, SourceLocation start, SourceL
|
||||
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);
|
||||
@@ -70,68 +269,6 @@ public readonly struct SourceSpan(SourceText text, SourceLocation start, SourceL
|
||||
|
||||
public static bool operator !=(SourceSpan left, SourceSpan right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct SourceText(string name, string content) : IEquatable<SourceText>
|
||||
{
|
||||
public string Name { get; } = name;
|
||||
public string Content { get; } = content;
|
||||
|
||||
public bool Equals(SourceText other)
|
||||
{
|
||||
return Content == other.Content;
|
||||
}
|
||||
|
||||
public override bool Equals([NotNullWhen(true)] object? obj)
|
||||
{
|
||||
return obj is SourceText other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Content.GetHashCode();
|
||||
}
|
||||
|
||||
public static bool operator ==(SourceText left, SourceText right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(SourceText left, SourceText right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct SourceLocation(int line, int column) : IEquatable<SourceLocation>
|
||||
{
|
||||
public int Line { get; } = line;
|
||||
public int Column { get; } = column;
|
||||
|
||||
public override bool Equals([NotNullWhen(true)] object? obj)
|
||||
{
|
||||
return obj is SourceLocation other && Equals(other);
|
||||
}
|
||||
|
||||
public bool Equals(SourceLocation other)
|
||||
{
|
||||
return Line == other.Line && Column == other.Column;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Line, Column);
|
||||
}
|
||||
|
||||
public static bool operator ==(SourceLocation left, SourceLocation right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(SourceLocation left, SourceLocation right)
|
||||
{
|
||||
return !(left == right);
|
||||
return !left.Equals(right);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user