wip
This commit is contained in:
parent
74035d5f13
commit
fdf1019b7a
8 changed files with 775 additions and 896 deletions
|
|
@ -109,6 +109,16 @@ internal sealed class SemanticVersionRangeTests
|
|||
await Assert.That(r.Contains(v)).IsEqualTo(expected);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task I_can_handle_two_constraints_without_combining_them()
|
||||
{
|
||||
var range = SemanticVersionRange.Parse(">=1.0.0 !=2.0.0");
|
||||
|
||||
await Assert.That(range.Contains(SemanticVersion.Parse("1.5.0"))).IsTrue();
|
||||
await Assert.That(range.Contains(SemanticVersion.Parse("2.0.0"))).IsFalse();
|
||||
await Assert.That(range.ToString("n", null)).IsEqualTo(">=1.0.0 !=2.0.0");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Arguments("[1.2.3]", "1.2.3", true)]
|
||||
[Arguments("[1.2.3]", "1.2.4", false)]
|
||||
|
|
@ -121,6 +131,8 @@ internal sealed class SemanticVersionRangeTests
|
|||
[Arguments("[1.2.3,)", "9.9.9", true)]
|
||||
[Arguments("(,1.4.0]", "1.4.0", true)]
|
||||
[Arguments("(,1.4.0]", "0.0.0", true)]
|
||||
[Arguments("(,)", "0.0.0", true)]
|
||||
[Arguments("(,)", "9.9.9", true)]
|
||||
public async Task I_can_satisfy_maven_ranges(string range, string version, bool expected)
|
||||
{
|
||||
var r = SemanticVersionRange.Parse(range);
|
||||
|
|
@ -140,10 +152,10 @@ internal sealed class SemanticVersionRangeTests
|
|||
}
|
||||
|
||||
[Test]
|
||||
[Arguments("^1.2.3", "1.2.3-alpha", false)]
|
||||
[Arguments("^1.2.3", "1.2.3-alpha", true)]
|
||||
[Arguments("^1.2.3-alpha", "1.2.3-beta", true)]
|
||||
[Arguments("^1.2.3-alpha", "1.2.4", true)]
|
||||
[Arguments("^1.2.3-alpha", "1.3.0-alpha", false)]
|
||||
[Arguments("^1.2.3-alpha", "1.3.0-alpha", true)]
|
||||
public async Task I_can_satisfy_prerelease_rules(string range, string version, bool expected)
|
||||
{
|
||||
var r = SemanticVersionRange.Parse(range);
|
||||
|
|
@ -183,7 +195,9 @@ internal sealed class SemanticVersionRangeTests
|
|||
[Arguments("[1.2.3,2.0.0)", "ns", "^1.2.3")]
|
||||
[Arguments("[1.2.3,1.3.0)", "ns", "~1.2.3")]
|
||||
[Arguments("[1.2,1.3],[1.5,)", "n", ">=1.2.0 <=1.3.0 || >=1.5.0")]
|
||||
[Arguments(">=1.0.0 !=2.0.0", "m", ">=1.0.0 !=2.0.0")]
|
||||
[Arguments("*", "m", "(,)")]
|
||||
[Arguments("(,)", "ns", "*")]
|
||||
public async Task I_can_convert_range_formats(string range, string format, string expected)
|
||||
{
|
||||
var value = SemanticVersionRange.Parse(range);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,12 @@ namespace Geekeey.SemVer;
|
|||
|
||||
public readonly partial record struct SemanticVersion : ISpanFormattable
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return ToString(null, null);
|
||||
}
|
||||
|
||||
#region IFormattable
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -49,37 +55,52 @@ public readonly partial record struct SemanticVersion : ISpanFormattable
|
|||
/// </remarks>
|
||||
public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider)
|
||||
{
|
||||
charsWritten = 0;
|
||||
|
||||
if (format.IsEmpty)
|
||||
{
|
||||
format = "s";
|
||||
}
|
||||
|
||||
if (format is not "s" and not "f" and not "r")
|
||||
{
|
||||
throw new FormatException($"The format string '{format}' is not supported.");
|
||||
}
|
||||
|
||||
if (!destination.TryWrite($"{Major}.{Minor}.{Patch}", out var written))
|
||||
{
|
||||
charsWritten = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
var builder = new SpanStringBuilder(destination, provider);
|
||||
builder.AppendFormatted(Major);
|
||||
builder.AppendLiteral(".");
|
||||
builder.AppendFormatted(Minor);
|
||||
builder.AppendLiteral(".");
|
||||
builder.AppendFormatted(Patch);
|
||||
destination = destination[written..];
|
||||
charsWritten += written;
|
||||
|
||||
if (Prerelease is { Length: > 0 } && format is "s" or "f")
|
||||
{
|
||||
builder.AppendLiteral("-");
|
||||
builder.AppendFormatted(Prerelease);
|
||||
if (!destination.TryWrite(provider, $"-{Prerelease}", out written))
|
||||
{
|
||||
charsWritten = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
destination = destination[written..];
|
||||
charsWritten += written;
|
||||
}
|
||||
|
||||
if (Metadata is { Length: > 0 } && format is "f")
|
||||
{
|
||||
builder.AppendLiteral("+");
|
||||
builder.AppendFormatted(Metadata);
|
||||
if (!destination.TryWrite(provider, $"+{Metadata}", out written))
|
||||
{
|
||||
charsWritten = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
destination = destination[written..];
|
||||
charsWritten += written;
|
||||
}
|
||||
|
||||
return builder.TryComplete(out charsWritten);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
|
|||
|
|
@ -102,10 +102,4 @@ public readonly partial record struct SemanticVersion
|
|||
/// details. It does not affect the precedence of the version.
|
||||
/// </summary>
|
||||
public string? Metadata { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return ToString(null, null);
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,12 @@ namespace Geekeey.SemVer;
|
|||
|
||||
public readonly partial record struct SemanticVersionRange : ISpanFormattable
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return ToString(null, null);
|
||||
}
|
||||
|
||||
#region IFormattable
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -38,6 +44,8 @@ public readonly partial record struct SemanticVersionRange : ISpanFormattable
|
|||
/// <inheritdoc />
|
||||
public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider)
|
||||
{
|
||||
charsWritten = 0;
|
||||
|
||||
if (format.IsEmpty)
|
||||
{
|
||||
format = "ns";
|
||||
|
|
@ -45,31 +53,173 @@ public readonly partial record struct SemanticVersionRange : ISpanFormattable
|
|||
|
||||
if (format is not "m" and not "n" and not "ns")
|
||||
{
|
||||
charsWritten = 0;
|
||||
return false;
|
||||
throw new FormatException($"The format string '{format}' is not supported.");
|
||||
}
|
||||
|
||||
var builder = new SpanStringBuilder(destination, provider);
|
||||
var buf = new SpanBuffer(destination);
|
||||
var groups = _groups ?? [];
|
||||
|
||||
if (_sets is not { Length: > 0 } sets)
|
||||
if (format is "m")
|
||||
{
|
||||
builder.AppendLiteral(format is "m" ? "(,)" : "*");
|
||||
for (var i = 0; i < groups.Length; i++)
|
||||
{
|
||||
if (i > 0 && !buf.TryWrite(','))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryFormatMaven(ref buf, groups[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var i = 0; i < sets.Length; i++)
|
||||
for (var i = 0; i < groups.Length; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
if (i > 0 && !buf.TryWrite(" || "))
|
||||
{
|
||||
builder.AppendLiteral(format is "m" ? "," : " || ");
|
||||
return false;
|
||||
}
|
||||
|
||||
sets[i].AppendFormatted(ref builder, format);
|
||||
if (format is "ns")
|
||||
{
|
||||
if (!TryFormatSimpleNpm(ref buf, groups[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!TryFormatNormalNpm(ref buf, groups[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return builder.TryComplete(out charsWritten);
|
||||
charsWritten = buf.Written;
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static bool TryWriteComparator(ref SpanBuffer buf, Comparator c)
|
||||
{
|
||||
ReadOnlySpan<char> op = c.Op switch
|
||||
{
|
||||
ComparatorOp.Neq => "!=",
|
||||
ComparatorOp.Lt => "<",
|
||||
ComparatorOp.Lte => "<=",
|
||||
ComparatorOp.Gt => ">",
|
||||
ComparatorOp.Gte => ">=",
|
||||
_ => []
|
||||
};
|
||||
|
||||
if (!op.IsEmpty && !buf.TryWrite(op))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return buf.TryWrite(c.Version);
|
||||
}
|
||||
|
||||
private static bool TryFormatSimpleNpm(ref SpanBuffer buf, ComparatorGroup group)
|
||||
{
|
||||
var c = group.Comparators;
|
||||
var checkpoint = buf.Written;
|
||||
|
||||
if (c.Length is 0)
|
||||
{
|
||||
return buf.TryWrite('*');
|
||||
}
|
||||
|
||||
if (c is [{ Op: ComparatorOp.Eq }])
|
||||
{
|
||||
return buf.TryWrite(c[0].Version) || buf.Reset(checkpoint);
|
||||
}
|
||||
|
||||
if (c.Length is not 2)
|
||||
{
|
||||
return buf.Reset(checkpoint);
|
||||
}
|
||||
|
||||
var lower = group.Lower!.Value;
|
||||
var upper = group.Upper!.Value;
|
||||
|
||||
if (lower.Op is ComparatorOp.Gte or ComparatorOp.Gt && upper.Op is ComparatorOp.Lt or ComparatorOp.Lte)
|
||||
{
|
||||
var lo = lower.Version;
|
||||
var hi = upper.Version;
|
||||
|
||||
if (upper.Op == ComparatorOp.Lt && hi is { Minor: 0, Patch: 0, Prerelease: not { Length: > 0 } })
|
||||
{
|
||||
if ((lo.Major > 0 && hi.Major == lo.Major + 1) ||
|
||||
(lo is { Major: 0, Minor: > 0 } && hi.Major == 0 && hi.Minor == lo.Minor + 1) ||
|
||||
(lo is { Major: 0, Minor: 0 } && hi is { Major: 0, Minor: 0 } && hi.Patch == lo.Patch + 1))
|
||||
{
|
||||
return (buf.TryWrite('^') && buf.TryWrite(lo)) || buf.Reset(checkpoint);
|
||||
}
|
||||
}
|
||||
|
||||
if (upper.Op == ComparatorOp.Lt &&
|
||||
hi is { Patch: 0, Prerelease: not { Length: > 0 } } &&
|
||||
hi.Major == lo.Major &&
|
||||
hi.Minor == lo.Minor + 1)
|
||||
{
|
||||
return (buf.TryWrite('~') && buf.TryWrite(lo)) || buf.Reset(checkpoint);
|
||||
}
|
||||
}
|
||||
|
||||
return buf.Reset(checkpoint);
|
||||
}
|
||||
|
||||
private static bool TryFormatNormalNpm(ref SpanBuffer buf, ComparatorGroup group)
|
||||
{
|
||||
var comps = group.Comparators;
|
||||
if (comps.Length is 0)
|
||||
{
|
||||
return buf.TryWrite('*');
|
||||
}
|
||||
|
||||
for (var j = 0; j < comps.Length; j++)
|
||||
{
|
||||
if (j > 0 && !buf.TryWrite(' '))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryWriteComparator(ref buf, comps[j]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryFormatMaven(ref SpanBuffer buf, ComparatorGroup group)
|
||||
{
|
||||
if (group.Comparators is [{ Op: ComparatorOp.Eq, Version: var version }])
|
||||
{
|
||||
var exactCheckpoint = buf.Written;
|
||||
return (buf.TryWrite('[') && buf.TryWrite(version) && buf.TryWrite(']')) || buf.Reset(exactCheckpoint);
|
||||
}
|
||||
|
||||
if (group.Comparators.Any(c => c.Op is ComparatorOp.Neq) || group.Comparators.Length > 2)
|
||||
{
|
||||
return TryFormatNormalNpm(ref buf, group);
|
||||
}
|
||||
|
||||
var checkpoint = buf.Written;
|
||||
|
||||
return (buf.TryWrite(group.Lower?.Op == ComparatorOp.Gte ? '[' : '(') &&
|
||||
(!group.Lower.HasValue || buf.TryWrite(group.Lower.Value.Version)) &&
|
||||
buf.TryWrite(',') &&
|
||||
(!group.Upper.HasValue || buf.TryWrite(group.Upper.Value.Version)) &&
|
||||
buf.TryWrite(group.Upper?.Op == ComparatorOp.Lte ? ']' : ')')) ||
|
||||
buf.Reset(checkpoint);
|
||||
}
|
||||
}
|
||||
|
|
@ -82,348 +82,30 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
|
|||
return false;
|
||||
}
|
||||
|
||||
return s[0] is '[' or '(' ? TryParseJava(s, out result) : TryParseNode(s, out result);
|
||||
return s[0] is '[' or '(' ? TryParseMaven(s, out result) : TryParseNpm(s, out result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static bool TryParseJava(ReadOnlySpan<char> s, out SemanticVersionRange result)
|
||||
private static bool TryParseNpm(ReadOnlySpan<char> s, out SemanticVersionRange result)
|
||||
{
|
||||
var sets = new List<ConstraintSet>();
|
||||
|
||||
foreach (var range in new JavaSetGrouping(s))
|
||||
{
|
||||
if (s[range] is not { IsEmpty: false } part)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryParseJavaSet(part, out var set))
|
||||
{
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
sets.Add(set);
|
||||
}
|
||||
|
||||
result = new SemanticVersionRange([.. sets]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseJavaSet(ReadOnlySpan<char> s, out ConstraintSet set)
|
||||
{
|
||||
set = default;
|
||||
|
||||
if (s.IsEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (s is not ['[' or '(', .., ']' or ')'])
|
||||
{
|
||||
if (!SemanticVersion.TryParsePartially(s, out var version, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
set = new ConstraintSet([new Constraint(lower: (version, true), upper: null)]);
|
||||
return true;
|
||||
}
|
||||
|
||||
var inclusiveStart = s[0] is '[';
|
||||
var inclusiveEnd = s[^1] is ']';
|
||||
var text = s[1..^1];
|
||||
var comma = text.IndexOf(',');
|
||||
|
||||
if (comma < 0)
|
||||
{
|
||||
if (!inclusiveStart || !inclusiveEnd || !SemanticVersion.TryParsePartially(text.Trim(), out var version, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
set = new ConstraintSet([new Constraint(Comparator.Equal, version)]);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!TryCreateBound(text[..comma].Trim(), inclusiveStart, out var lower) ||
|
||||
!TryCreateBound(text[(comma + 1)..].Trim(), inclusiveEnd, out var upper))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
set = new ConstraintSet(lower is null && upper is null ? [] : [new Constraint(lower: lower, upper: upper)]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryCreateBound(ReadOnlySpan<char> s, bool inclusive, out (SemanticVersion Version, bool Inclusive)? bound)
|
||||
{
|
||||
if (s.IsEmpty)
|
||||
{
|
||||
bound = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!SemanticVersion.TryParsePartially(s, out var version, out _))
|
||||
{
|
||||
bound = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
bound = (version, inclusive);
|
||||
return true;
|
||||
}
|
||||
|
||||
private ref struct JavaSetGrouping
|
||||
{
|
||||
private readonly ReadOnlySpan<char> _span;
|
||||
private int _currentStart;
|
||||
private int _currentEnd;
|
||||
|
||||
public JavaSetGrouping(ReadOnlySpan<char> readOnlySpan)
|
||||
{
|
||||
_span = readOnlySpan;
|
||||
_currentStart = 0;
|
||||
_currentEnd = -1;
|
||||
}
|
||||
|
||||
public readonly Range Current => _currentStart.._currentEnd;
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
_currentStart = _currentEnd is -1 ? 0 : _currentEnd + 1;
|
||||
|
||||
if (_currentStart >= _span.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var depth = 0;
|
||||
var i = _currentStart;
|
||||
|
||||
while (i < _span.Length)
|
||||
{
|
||||
var ch = _span[i];
|
||||
|
||||
if (ch is '[' or '(')
|
||||
{
|
||||
depth++;
|
||||
}
|
||||
else if (ch is ']' or ')')
|
||||
{
|
||||
depth--;
|
||||
}
|
||||
else if (ch is ',' && depth is 0)
|
||||
{
|
||||
_currentEnd = i;
|
||||
return true;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
_currentEnd = _span.Length;
|
||||
return true;
|
||||
}
|
||||
|
||||
public readonly JavaSetGrouping GetEnumerator()
|
||||
{
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseNode(ReadOnlySpan<char> s, out SemanticVersionRange result)
|
||||
{
|
||||
var sets = new List<ConstraintSet>();
|
||||
result = default;
|
||||
var groups = new List<ComparatorGroup>();
|
||||
|
||||
foreach (var range in new NodeSetGrouping(s))
|
||||
{
|
||||
if (s[range] is not { IsEmpty: false } part)
|
||||
if (!TryParseNpmGroup(s[range].Trim(), out var group))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryParseNodeSet(part, out var set))
|
||||
{
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
sets.Add(set);
|
||||
groups.Add(group);
|
||||
}
|
||||
|
||||
result = new SemanticVersionRange([.. sets]);
|
||||
result = new SemanticVersionRange([.. groups]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseNodeSet(ReadOnlySpan<char> s, out ConstraintSet set)
|
||||
{
|
||||
if (s.IndexOf(" - ") is not -1 and var hyphen)
|
||||
{
|
||||
if (!SemanticVersion.TryParse(s[..hyphen].Trim(), out var lower))
|
||||
{
|
||||
set = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!SemanticVersion.TryParse(s[(hyphen + 3)..].Trim(), out var upper))
|
||||
{
|
||||
set = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
set = new ConstraintSet([new Constraint(lower: (lower, true), upper: (upper, true))]);
|
||||
return true;
|
||||
}
|
||||
|
||||
var list = new List<Constraint>(2);
|
||||
var current = s;
|
||||
while (TryReadToken(ref current, out var token))
|
||||
{
|
||||
scoped ReadOnlySpan<char> instruction = [];
|
||||
|
||||
if (token is ">=" or "<=" or ">" or "<" or "~" or "^" or "=" or "!=")
|
||||
{
|
||||
instruction = token;
|
||||
|
||||
if (!TryReadToken(ref current, out token))
|
||||
{
|
||||
set = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (token.StartsWith(">="))
|
||||
{
|
||||
instruction = ">=";
|
||||
token = token[2..];
|
||||
}
|
||||
else if (token.StartsWith("<="))
|
||||
{
|
||||
instruction = "<=";
|
||||
token = token[2..];
|
||||
}
|
||||
else if (token.StartsWith("!="))
|
||||
{
|
||||
instruction = "!=";
|
||||
token = token[2..];
|
||||
}
|
||||
else if (token.StartsWith(">"))
|
||||
{
|
||||
instruction = ">";
|
||||
token = token[1..];
|
||||
}
|
||||
else if (token.StartsWith("<"))
|
||||
{
|
||||
instruction = "<";
|
||||
token = token[1..];
|
||||
}
|
||||
else if (token.StartsWith("^"))
|
||||
{
|
||||
instruction = "^";
|
||||
token = token[1..];
|
||||
}
|
||||
else if (token.StartsWith("~"))
|
||||
{
|
||||
instruction = "~";
|
||||
token = token[1..];
|
||||
}
|
||||
else if (token.StartsWith("="))
|
||||
{
|
||||
instruction = "=";
|
||||
token = token[1..];
|
||||
}
|
||||
}
|
||||
|
||||
if (token is "*" or "x" or "X")
|
||||
{
|
||||
set = new ConstraintSet([default]);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!TryParseNpmConstraint(instruction, token.Trim(), list))
|
||||
{
|
||||
set = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
set = new ConstraintSet([.. list]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadToken(ref ReadOnlySpan<char> s, out ReadOnlySpan<char> token)
|
||||
{
|
||||
s = s.TrimStart();
|
||||
if (s.IsEmpty)
|
||||
{
|
||||
token = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
var separator = s.IndexOf(' ');
|
||||
if (separator < 0)
|
||||
{
|
||||
token = s;
|
||||
s = default;
|
||||
return true;
|
||||
}
|
||||
|
||||
token = s[..separator];
|
||||
s = s[separator..];
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseNpmConstraint(ReadOnlySpan<char> op, ReadOnlySpan<char> s, ICollection<Constraint> constraints)
|
||||
{
|
||||
if (!SemanticVersion.TryParsePartially(s.Trim(), out var version, out var components))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (op)
|
||||
{
|
||||
case "^":
|
||||
constraints.Add(new Constraint(lower: (version, true), upper: (GetCaretUpperBound(version, components), false)));
|
||||
return true;
|
||||
case "~":
|
||||
constraints.Add(new Constraint(lower: (version, true), upper: (GetTildeUpperBound(version, components), false)));
|
||||
return true;
|
||||
case ">":
|
||||
constraints.Add(new Constraint(Comparator.Greater, version));
|
||||
return true;
|
||||
case ">=":
|
||||
constraints.Add(new Constraint(Comparator.GreaterOrEqual, version));
|
||||
return true;
|
||||
case "<":
|
||||
constraints.Add(new Constraint(Comparator.Less, version));
|
||||
return true;
|
||||
case "<=":
|
||||
constraints.Add(new Constraint(Comparator.LessOrEqual, version));
|
||||
return true;
|
||||
case "!=":
|
||||
constraints.Add(new Constraint(Comparator.NotEqual, version));
|
||||
return true;
|
||||
default:
|
||||
switch (components)
|
||||
{
|
||||
case 3:
|
||||
constraints.Add(new Constraint(Comparator.Equal, version));
|
||||
return true;
|
||||
case 2:
|
||||
constraints.Add(new Constraint(lower: (version, true), upper: (new SemanticVersion(version.Major, version.Minor + 1, 0), false)));
|
||||
return true;
|
||||
case 1:
|
||||
constraints.Add(new Constraint(lower: (version, true), upper: (new SemanticVersion(version.Major + 1, 0, 0), false)));
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ref struct NodeSetGrouping
|
||||
{
|
||||
private readonly ReadOnlySpan<char> _span;
|
||||
|
|
@ -459,4 +141,549 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
|
|||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseNpmGroup(ReadOnlySpan<char> s, out ComparatorGroup group)
|
||||
{
|
||||
group = default;
|
||||
if (s.IsEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Hyphen range: "1.0.0 - 2.0.0" OR Caret: "^x.y.z" OR Tilde: "~x.y.z"
|
||||
if (TryParseHyphenRange(s, out group) || TryParseCaretRange(s, out group) || TryParseTildeRange(s, out group))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Space-separated AND comparators
|
||||
var comparators = new List<Comparator>();
|
||||
var tokens = 0;
|
||||
while (!s.IsEmpty)
|
||||
{
|
||||
// Skip spaces
|
||||
s = s.TrimStart();
|
||||
if (s.IsEmpty)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Find end of this token (next space)
|
||||
var end = s.IndexOf(' ');
|
||||
var token = end >= 0 ? s[..end] : s;
|
||||
tokens++;
|
||||
|
||||
if (IsWildcardToken(token))
|
||||
{
|
||||
if (!TryExpandWildcardComparators(token, comparators))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!TryParseComparator(token, out var comp))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
comparators.Add(comp);
|
||||
}
|
||||
|
||||
s = end >= 0 ? s[(end + 1)..] : [];
|
||||
}
|
||||
|
||||
if (comparators.Count == 0 && tokens == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
group = new ComparatorGroup([.. comparators]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseHyphenRange(ReadOnlySpan<char> s, out ComparatorGroup group)
|
||||
{
|
||||
group = default;
|
||||
// Look for " - " (space-hyphen-space)
|
||||
for (var i = 1; i < s.Length - 1; i++)
|
||||
{
|
||||
if (s[i] == '-' && s[i - 1] == ' ' && i + 1 < s.Length && s[i + 1] == ' ')
|
||||
{
|
||||
var lo = s[..(i - 1)].Trim();
|
||||
var hi = s[(i + 2)..].Trim();
|
||||
if (!SemanticVersion.TryParse(lo, null, out var loV))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!SemanticVersion.TryParse(hi, null, out var hiV))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
group = new ComparatorGroup([new Comparator(ComparatorOp.Gte, loV), new Comparator(ComparatorOp.Lte, hiV)]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryParseCaretRange(ReadOnlySpan<char> s, out ComparatorGroup group)
|
||||
{
|
||||
group = default;
|
||||
if (s.IsEmpty || s[0] is not '^')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
s = s[1..];
|
||||
// if (!TryParseWildcardVersion(s, out var version, out var minorWild, out var patchWild))
|
||||
// {
|
||||
// return false;
|
||||
// }
|
||||
if (!SemanticVersion.TryParsePartially(s, out var version, out var comp))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var lo = comp is 0
|
||||
? new SemanticVersion(version.Major, 0, 0)
|
||||
: comp is 1
|
||||
? new SemanticVersion(version.Major, version.Minor, 0)
|
||||
: version;
|
||||
SemanticVersion hi;
|
||||
|
||||
if (version.Major > 0)
|
||||
{
|
||||
hi = new SemanticVersion(version.Major + 1, 0, 0);
|
||||
}
|
||||
else if (comp is not 1 && version.Minor > 0)
|
||||
{
|
||||
hi = new SemanticVersion(0, version.Minor + 1, 0);
|
||||
}
|
||||
else if (comp is not 2 and not 1)
|
||||
{
|
||||
hi = new SemanticVersion(0, 0, version.Patch + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
hi = new SemanticVersion(0, 1, 0);
|
||||
}
|
||||
|
||||
group = new ComparatorGroup([new Comparator(ComparatorOp.Gte, lo), new Comparator(ComparatorOp.Lt, hi)]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseTildeRange(ReadOnlySpan<char> s, out ComparatorGroup group)
|
||||
{
|
||||
group = default;
|
||||
if (s.IsEmpty || s[0] is not '~')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
s = s[1..];
|
||||
if (!SemanticVersion.TryParsePartially(s, out var lo, out var comp))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
SemanticVersion hi;
|
||||
|
||||
if (comp is 1)
|
||||
{
|
||||
hi = new SemanticVersion(lo.Major + 1, 0, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
hi = new SemanticVersion(lo.Major, lo.Minor + 1, 0);
|
||||
}
|
||||
|
||||
group = new ComparatorGroup([new Comparator(ComparatorOp.Gte, lo), new Comparator(ComparatorOp.Lt, hi)]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseComparator(ReadOnlySpan<char> token, out Comparator result)
|
||||
{
|
||||
result = default;
|
||||
token = token.Trim();
|
||||
if (token.IsEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse operator
|
||||
ComparatorOp op;
|
||||
int opLen;
|
||||
if (token.Length >= 2 && token[0] == '!' && token[1] == '=')
|
||||
{
|
||||
op = ComparatorOp.Neq;
|
||||
opLen = 2;
|
||||
}
|
||||
else if (token.Length >= 2 && token[0] == '>' && token[1] == '=')
|
||||
{
|
||||
op = ComparatorOp.Gte;
|
||||
opLen = 2;
|
||||
}
|
||||
else if (token.Length >= 2 && token[0] == '<' && token[1] == '=')
|
||||
{
|
||||
op = ComparatorOp.Lte;
|
||||
opLen = 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (token[0])
|
||||
{
|
||||
case '>':
|
||||
op = ComparatorOp.Gt;
|
||||
opLen = 1;
|
||||
break;
|
||||
case '<':
|
||||
op = ComparatorOp.Lt;
|
||||
opLen = 1;
|
||||
break;
|
||||
case '=':
|
||||
op = ComparatorOp.Eq;
|
||||
opLen = 1;
|
||||
break;
|
||||
default:
|
||||
op = ComparatorOp.Eq;
|
||||
opLen = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var vStr = token[opLen..];
|
||||
if (!SemanticVersion.TryParse(vStr, null, out var version))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = new Comparator(op, version);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsWildcardToken(ReadOnlySpan<char> s)
|
||||
{
|
||||
// Skip operator chars
|
||||
var i = 0;
|
||||
while (i < s.Length && (s[i] is '>' or '<' or '=' or '~' or '^'))
|
||||
{
|
||||
i++;
|
||||
}
|
||||
|
||||
for (; i < s.Length; i++)
|
||||
{
|
||||
if (s[i] is '*' or 'x' or 'X')
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static ReadOnlySpan<char> StripOp(ReadOnlySpan<char> s, out ComparatorOp op)
|
||||
{
|
||||
op = ComparatorOp.Eq;
|
||||
var i = 0;
|
||||
if (s.Length >= 2)
|
||||
{
|
||||
if (s[0] == '>' && s[1] == '=')
|
||||
{
|
||||
op = ComparatorOp.Gte;
|
||||
return s[2..];
|
||||
}
|
||||
|
||||
if (s[0] == '<' && s[1] == '=')
|
||||
{
|
||||
op = ComparatorOp.Lte;
|
||||
return s[2..];
|
||||
}
|
||||
}
|
||||
|
||||
while (i < s.Length && (s[i] is '>' or '<' or '='))
|
||||
{
|
||||
op = s[i] switch
|
||||
{
|
||||
'>' => ComparatorOp.Gt,
|
||||
'<' => ComparatorOp.Lt,
|
||||
_ => ComparatorOp.Eq,
|
||||
};
|
||||
i++;
|
||||
}
|
||||
|
||||
return s[i..];
|
||||
}
|
||||
|
||||
private static bool TryExpandWildcardComparators(ReadOnlySpan<char> token, List<Comparator> comparators)
|
||||
{
|
||||
var s = StripOp(token.Trim(), out var op);
|
||||
if (op is not ComparatorOp.Eq)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryParseWildcardVersion(s, out var version, out var minorWild, out var patchWild))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!minorWild && !patchWild)
|
||||
{
|
||||
comparators.Add(new Comparator(ComparatorOp.Eq, version));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (s is ['*' or 'x' or 'X'])
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
comparators.Add(new Comparator(ComparatorOp.Gte, new SemanticVersion(version.Major, version.Minor, version.Patch)));
|
||||
var upper = minorWild
|
||||
? new SemanticVersion(version.Major + 1, 0, 0)
|
||||
: new SemanticVersion(version.Major, version.Minor + 1, 0);
|
||||
comparators.Add(new Comparator(ComparatorOp.Lt, upper));
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseWildcardVersion(ReadOnlySpan<char> s, out SemanticVersion version, out bool minorWild, out bool patchWild)
|
||||
{
|
||||
version = default;
|
||||
minorWild = patchWild = false;
|
||||
if (s.IsEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle bare * or x/X
|
||||
if (s is ['*' or 'x' or 'X'])
|
||||
{
|
||||
minorWild = patchWild = true;
|
||||
version = new SemanticVersion(0, 0, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
ulong major = 0, minor = 0, patch = 0;
|
||||
var pos = 0;
|
||||
if (!TryParseNonNegativeInt(s, ref pos, out major))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pos >= s.Length)
|
||||
{
|
||||
minorWild = patchWild = true;
|
||||
version = new SemanticVersion(major, 0, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (s[pos] != '.')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
pos++;
|
||||
|
||||
if (pos < s.Length && (s[pos] == '*' || s[pos] == 'x' || s[pos] == 'X'))
|
||||
{
|
||||
minorWild = patchWild = true;
|
||||
pos++;
|
||||
version = new SemanticVersion(major, 0, 0);
|
||||
return pos == s.Length;
|
||||
}
|
||||
|
||||
if (!TryParseNonNegativeInt(s, ref pos, out minor))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pos >= s.Length)
|
||||
{
|
||||
patchWild = true;
|
||||
version = new SemanticVersion(major, minor, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (s[pos] != '.')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
pos++;
|
||||
|
||||
if (pos < s.Length && (s[pos] == '*' || s[pos] == 'x' || s[pos] == 'X'))
|
||||
{
|
||||
patchWild = true;
|
||||
pos++;
|
||||
version = new SemanticVersion(major, minor, 0);
|
||||
return pos == s.Length;
|
||||
}
|
||||
|
||||
if (!TryParseNonNegativeInt(s, ref pos, out patch))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pos == s.Length)
|
||||
{
|
||||
version = new SemanticVersion(major, minor, patch);
|
||||
return true;
|
||||
}
|
||||
|
||||
return (s[pos] == '-' || s[pos] == '+') && SemanticVersion.TryParse(s, null, out version);
|
||||
}
|
||||
|
||||
internal static bool TryParseNonNegativeInt(ReadOnlySpan<char> s, ref int pos, out ulong value)
|
||||
{
|
||||
value = 0;
|
||||
var start = pos;
|
||||
while (pos < s.Length && char.IsAsciiDigit(s[pos]))
|
||||
{
|
||||
pos++;
|
||||
}
|
||||
|
||||
if (pos == start)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pos - start > 1 && s[start] == '0')
|
||||
{
|
||||
return false; // no leading zeros
|
||||
}
|
||||
|
||||
return ulong.TryParse(s[start..pos], out value);
|
||||
}
|
||||
|
||||
|
||||
// ── Maven parsing ────────────────────────────────────────────────────────
|
||||
|
||||
private static bool TryParseMaven(ReadOnlySpan<char> s, out SemanticVersionRange result)
|
||||
{
|
||||
result = default;
|
||||
var groups = new List<ComparatorGroup>();
|
||||
|
||||
while (!s.IsEmpty)
|
||||
{
|
||||
s = s.Trim();
|
||||
if (s.IsEmpty)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (s[0] is not '[' and not '(')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find matching close bracket
|
||||
var close = FindMavenClose(s);
|
||||
if (close < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var bracket = s[..(close + 1)];
|
||||
if (!TryParseMavenGroup(bracket, out var g))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
groups.Add(g);
|
||||
|
||||
s = s[(close + 1)..].Trim();
|
||||
if (!s.IsEmpty && s[0] == ',')
|
||||
{
|
||||
s = s[1..]; // OR separator between groups
|
||||
}
|
||||
}
|
||||
|
||||
if (groups.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = new SemanticVersionRange([.. groups]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int FindMavenClose(ReadOnlySpan<char> s)
|
||||
{
|
||||
for (var i = 1; i < s.Length; i++)
|
||||
{
|
||||
if (s[i] is ']' or ')')
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static bool TryParseMavenGroup(ReadOnlySpan<char> s, out ComparatorGroup group)
|
||||
{
|
||||
group = default;
|
||||
var lInclusive = s[0] == '[';
|
||||
var rInclusive = s[^1] == ']';
|
||||
|
||||
var inner = s[1..^1];
|
||||
var commaIdx = inner.IndexOf(',');
|
||||
if (commaIdx < 0)
|
||||
{
|
||||
if (!lInclusive || !rInclusive || !SemanticVersion.TryParsePartially(inner.Trim(), out var exact, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
group = new ComparatorGroup([new Comparator(ComparatorOp.Eq, exact)]);
|
||||
return true;
|
||||
}
|
||||
|
||||
var loStr = inner[..commaIdx].Trim();
|
||||
var hiStr = inner[(commaIdx + 1)..].Trim();
|
||||
|
||||
var comps = new List<Comparator>();
|
||||
|
||||
if (!loStr.IsEmpty)
|
||||
{
|
||||
if (!SemanticVersion.TryParsePartially(loStr, out var lo, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
comps.Add(new Comparator(lInclusive ? ComparatorOp.Gte : ComparatorOp.Gt, lo));
|
||||
}
|
||||
|
||||
if (!hiStr.IsEmpty)
|
||||
{
|
||||
if (!SemanticVersion.TryParsePartially(hiStr, out var hi, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
comps.Add(new Comparator(rInclusive ? ComparatorOp.Lte : ComparatorOp.Lt, hi));
|
||||
}
|
||||
|
||||
if (comps.Count == 0)
|
||||
{
|
||||
group = new ComparatorGroup([]);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for exact match [a,a]
|
||||
if (comps is [{ Op: ComparatorOp.Gte }, { Op: ComparatorOp.Lte }] &&
|
||||
comps[0].Version == comps[1].Version)
|
||||
{
|
||||
group = new ComparatorGroup([new Comparator(ComparatorOp.Eq, comps[0].Version)]);
|
||||
return true;
|
||||
}
|
||||
|
||||
group = new ComparatorGroup([.. comps]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,586 +1,91 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace Geekeey.SemVer;
|
||||
|
||||
public readonly partial record struct SemanticVersionRange
|
||||
{
|
||||
private readonly ImmutableArray<ConstraintSet> _sets;
|
||||
// OR of AND-groups. null == empty range (matches nothing).
|
||||
private readonly ComparatorGroup[]? _groups;
|
||||
|
||||
private SemanticVersionRange(ImmutableArray<ConstraintSet> sets)
|
||||
internal SemanticVersionRange(ComparatorGroup[] groups)
|
||||
{
|
||||
_sets = sets;
|
||||
_groups = groups;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified semantic version is contained within the range defined by the current instance.
|
||||
/// </summary>
|
||||
/// <param name="version">The semantic version to check against the range.</param>
|
||||
/// <returns>
|
||||
/// True if the version satisfies at least one of the constraint sets in the range; otherwise, false.
|
||||
/// If the range has no constraint sets, the method returns true.
|
||||
/// </returns>
|
||||
public bool Contains(SemanticVersion version)
|
||||
{
|
||||
if (_sets.Length is 0)
|
||||
if (_groups is null || _groups.Length == 0)
|
||||
{
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var set in _sets)
|
||||
{
|
||||
if (set.Satisfies(version))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return _groups.Any(group => group.Includes(version));
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
internal enum ComparatorOp { Eq, Neq, Lt, Lte, Gt, Gte }
|
||||
|
||||
internal readonly struct Comparator
|
||||
{
|
||||
public readonly ComparatorOp Op;
|
||||
public readonly SemanticVersion Version;
|
||||
|
||||
public Comparator(ComparatorOp op, SemanticVersion version)
|
||||
{
|
||||
Op = op;
|
||||
Version = version;
|
||||
}
|
||||
|
||||
public bool Includes(SemanticVersion v)
|
||||
{
|
||||
return Op switch
|
||||
{
|
||||
ComparatorOp.Eq => v == Version,
|
||||
ComparatorOp.Neq => v != Version,
|
||||
ComparatorOp.Lt => v < Version,
|
||||
ComparatorOp.Lte => v <= Version,
|
||||
ComparatorOp.Gt => v > Version,
|
||||
ComparatorOp.Gte => v >= Version,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return ToString(null, null);
|
||||
return Op switch
|
||||
{
|
||||
ComparatorOp.Neq => $"!={Version}",
|
||||
ComparatorOp.Lt => $"<{Version}",
|
||||
ComparatorOp.Lte => $"<={Version}",
|
||||
ComparatorOp.Gt => $">{Version}",
|
||||
ComparatorOp.Gte => $">={Version}",
|
||||
_ => $"{Version}",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct ConstraintSet
|
||||
// One AND-group of comparators (all must be satisfied).
|
||||
internal readonly struct ComparatorGroup(Comparator[] comparators)
|
||||
{
|
||||
public readonly Comparator[] Comparators = comparators;
|
||||
|
||||
public Comparator? Upper => Comparators.Where(c => c.Op is ComparatorOp.Lt or ComparatorOp.Lte)
|
||||
.Select(it => (Comparator?)it).FirstOrDefault();
|
||||
|
||||
public Comparator? Lower => Comparators.Where(c => c.Op is ComparatorOp.Gt or ComparatorOp.Gte)
|
||||
.Select(it => (Comparator?)it).FirstOrDefault();
|
||||
|
||||
public bool Includes(SemanticVersion v)
|
||||
{
|
||||
public ConstraintSet(ImmutableArray<Constraint> constraints)
|
||||
foreach (var c in Comparators)
|
||||
{
|
||||
Constraints = constraints;
|
||||
}
|
||||
|
||||
public ImmutableArray<Constraint> Constraints { get; }
|
||||
|
||||
public bool Satisfies(SemanticVersion version)
|
||||
{
|
||||
if (version.Prerelease is not null)
|
||||
{
|
||||
var allowsPrerelease = false;
|
||||
foreach (var constraint in Constraints)
|
||||
{
|
||||
if (!constraint.CanMatchPrerelease(version))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
allowsPrerelease = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!allowsPrerelease)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var constraint in Constraints)
|
||||
{
|
||||
if (!constraint.Satisfies(version))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void AppendFormatted(ref SpanStringBuilder builder, ReadOnlySpan<char> format)
|
||||
{
|
||||
var effectiveCount = 0;
|
||||
Constraint single = default;
|
||||
|
||||
foreach (var constraint in Constraints.Where(constraint => !constraint.IsAny))
|
||||
{
|
||||
effectiveCount++;
|
||||
single = constraint;
|
||||
}
|
||||
|
||||
if (effectiveCount is 0)
|
||||
{
|
||||
builder.AppendLiteral(format is "m" ? "(,)" : "*");
|
||||
return;
|
||||
}
|
||||
|
||||
if (effectiveCount is 1)
|
||||
{
|
||||
var snapshot = builder;
|
||||
if (single.TryFormat(ref snapshot, format is "m", format is "ns", bareEquals: format is not "m"))
|
||||
{
|
||||
builder = snapshot;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!snapshot.Success)
|
||||
{
|
||||
builder = snapshot;
|
||||
return;
|
||||
}
|
||||
|
||||
AppendConstraints(ref builder, bareEquals: false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (format is "m" or "ns")
|
||||
{
|
||||
var snapshot = builder;
|
||||
if (TryCombineBounds(out var combined) &&
|
||||
combined.TryFormat(ref snapshot, format is "m", format is "ns", bareEquals: format is not "m"))
|
||||
{
|
||||
builder = snapshot;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!snapshot.Success)
|
||||
{
|
||||
builder = snapshot;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
AppendConstraints(ref builder, bareEquals: format is not "m");
|
||||
}
|
||||
|
||||
private bool TryCombineBounds(out Constraint combined)
|
||||
{
|
||||
combined = default;
|
||||
|
||||
(SemanticVersion Version, bool Inclusive)? lower = null;
|
||||
(SemanticVersion Version, bool Inclusive)? upper = null;
|
||||
var effectiveCount = 0;
|
||||
|
||||
foreach (var constraint in Constraints)
|
||||
{
|
||||
if (constraint.IsAny)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
effectiveCount++;
|
||||
|
||||
if (constraint.TryGetLowerBound(out var nextLower))
|
||||
{
|
||||
if (lower is not null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
lower = nextLower;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (constraint.TryGetUpperBound(out var nextUpper))
|
||||
{
|
||||
if (upper is not null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
upper = nextUpper;
|
||||
continue;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (effectiveCount is not 2 || lower is null && upper is null)
|
||||
if (!c.Includes(v))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
combined = lower is null && upper is null
|
||||
? default
|
||||
: new Constraint(lower: lower, upper: upper);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void AppendConstraints(ref SpanStringBuilder builder, bool bareEquals)
|
||||
{
|
||||
var written = 0;
|
||||
foreach (var constraint in Constraints)
|
||||
{
|
||||
if (constraint.IsAny)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (written > 0)
|
||||
{
|
||||
builder.AppendFormatted(' ');
|
||||
}
|
||||
|
||||
constraint.AppendConstraints(ref builder, bareEquals);
|
||||
|
||||
written++;
|
||||
}
|
||||
|
||||
if (written is 0)
|
||||
{
|
||||
builder.AppendLiteral("*");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct Constraint
|
||||
{
|
||||
private readonly Comparator _comparator;
|
||||
private readonly SemanticVersion _version;
|
||||
private readonly (SemanticVersion Version, bool Inclusive)? _lower;
|
||||
private readonly (SemanticVersion Version, bool Inclusive)? _upper;
|
||||
|
||||
public Constraint(Comparator comparator, SemanticVersion version)
|
||||
{
|
||||
_comparator = comparator;
|
||||
_version = version;
|
||||
_lower = null;
|
||||
_upper = null;
|
||||
}
|
||||
|
||||
public Constraint((SemanticVersion Version, bool Inclusive)? lower, (SemanticVersion Version, bool Inclusive)? upper)
|
||||
{
|
||||
_comparator = Comparator.Any;
|
||||
_version = default;
|
||||
_lower = lower;
|
||||
_upper = upper;
|
||||
}
|
||||
|
||||
public bool IsAny => _comparator is Comparator.Any && _lower is null && _upper is null;
|
||||
|
||||
public bool Satisfies(SemanticVersion version)
|
||||
{
|
||||
if (IsAny)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_comparator is not Comparator.Any)
|
||||
{
|
||||
return _comparator switch
|
||||
{
|
||||
Comparator.Equal => version.CompareTo(_version) is 0,
|
||||
Comparator.NotEqual => version.CompareTo(_version) is not 0,
|
||||
Comparator.Greater => version.CompareTo(_version) > 0,
|
||||
Comparator.GreaterOrEqual => version.CompareTo(_version) >= 0,
|
||||
Comparator.Less => version.CompareTo(_version) < 0,
|
||||
Comparator.LessOrEqual => version.CompareTo(_version) <= 0,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
return SatisfiesInterval(version);
|
||||
}
|
||||
|
||||
public bool CanMatchPrerelease(SemanticVersion version)
|
||||
{
|
||||
if (_comparator is not Comparator.Any)
|
||||
{
|
||||
return HasMatchingPrerelease(_version, version);
|
||||
}
|
||||
|
||||
return (_lower is { } lower && HasMatchingPrerelease(lower.Version, version)) ||
|
||||
(_upper is { } upper && HasMatchingPrerelease(upper.Version, version));
|
||||
}
|
||||
|
||||
public bool TryGetLowerBound(out (SemanticVersion Version, bool Inclusive) lower)
|
||||
{
|
||||
if (_lower is { } intervalLower)
|
||||
{
|
||||
lower = intervalLower;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_comparator is not Comparator.Any)
|
||||
{
|
||||
switch (_comparator)
|
||||
{
|
||||
case Comparator.Greater:
|
||||
lower = (_version, false);
|
||||
return true;
|
||||
case Comparator.GreaterOrEqual:
|
||||
lower = (_version, true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
lower = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryGetUpperBound(out (SemanticVersion Version, bool Inclusive) upper)
|
||||
{
|
||||
if (_upper is { } intervalUpper)
|
||||
{
|
||||
upper = intervalUpper;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_comparator is not Comparator.Any)
|
||||
{
|
||||
switch (_comparator)
|
||||
{
|
||||
case Comparator.Less:
|
||||
upper = (_version, false);
|
||||
return true;
|
||||
case Comparator.LessOrEqual:
|
||||
upper = (_version, true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
upper = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryFormat(ref SpanStringBuilder builder, bool maven, bool shortNpm, bool bareEquals)
|
||||
{
|
||||
if (IsAny)
|
||||
{
|
||||
builder.AppendLiteral(maven ? "(,)" : "*");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_comparator is not Comparator.Any)
|
||||
{
|
||||
return maven
|
||||
? TryAppendMaven(ref builder)
|
||||
: AppendComparator(ref builder, bareEquals);
|
||||
}
|
||||
|
||||
if (maven)
|
||||
{
|
||||
AppendMavenRange(ref builder, _lower, _upper);
|
||||
return true;
|
||||
}
|
||||
|
||||
return shortNpm && TryGetShortNpmOperator(out var shortOperator, out var shortVersion)
|
||||
? AppendShortNpm(ref builder, shortOperator, shortVersion)
|
||||
: AppendIntervalConstraints(ref builder);
|
||||
}
|
||||
|
||||
public void AppendConstraints(ref SpanStringBuilder builder, bool bareEquals)
|
||||
{
|
||||
if (IsAny)
|
||||
{
|
||||
builder.AppendLiteral("*");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_comparator is not Comparator.Any)
|
||||
{
|
||||
_ = AppendComparator(ref builder, bareEquals);
|
||||
return;
|
||||
}
|
||||
|
||||
_ = AppendIntervalConstraints(ref builder);
|
||||
}
|
||||
|
||||
private bool SatisfiesInterval(SemanticVersion version)
|
||||
{
|
||||
if (_lower is { } lower)
|
||||
{
|
||||
var lowerComparison = version.CompareTo(lower.Version);
|
||||
if (lowerComparison < 0 || lowerComparison is 0 && !lower.Inclusive)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (_upper is { } upper)
|
||||
{
|
||||
var upperComparison = version.CompareTo(upper.Version);
|
||||
if (upperComparison > 0 || upperComparison is 0 && !upper.Inclusive)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryAppendMaven(ref SpanStringBuilder builder)
|
||||
{
|
||||
return _comparator switch
|
||||
{
|
||||
Comparator.Equal => AppendMavenExact(ref builder, _version),
|
||||
Comparator.Greater => AppendMavenRange(ref builder, (_version, false), null),
|
||||
Comparator.GreaterOrEqual => AppendMavenRange(ref builder, (_version, true), null),
|
||||
Comparator.Less => AppendMavenRange(ref builder, null, (_version, false)),
|
||||
Comparator.LessOrEqual => AppendMavenRange(ref builder, null, (_version, true)),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
private bool AppendComparator(ref SpanStringBuilder builder, bool bareEquals)
|
||||
{
|
||||
switch (_comparator)
|
||||
{
|
||||
case Comparator.Equal when bareEquals:
|
||||
builder.AppendFormatted(_version, "f");
|
||||
return true;
|
||||
case Comparator.Equal:
|
||||
builder.AppendLiteral("=");
|
||||
builder.AppendFormatted(_version, "f");
|
||||
return true;
|
||||
case Comparator.Greater:
|
||||
builder.AppendLiteral(">");
|
||||
builder.AppendFormatted(_version, "f");
|
||||
return true;
|
||||
case Comparator.GreaterOrEqual:
|
||||
builder.AppendLiteral(">=");
|
||||
builder.AppendFormatted(_version, "f");
|
||||
return true;
|
||||
case Comparator.Less:
|
||||
builder.AppendLiteral("<");
|
||||
builder.AppendFormatted(_version, "f");
|
||||
return true;
|
||||
case Comparator.LessOrEqual:
|
||||
builder.AppendLiteral("<=");
|
||||
builder.AppendFormatted(_version, "f");
|
||||
return true;
|
||||
case Comparator.NotEqual:
|
||||
builder.AppendLiteral("!=");
|
||||
builder.AppendFormatted(_version, "f");
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool AppendIntervalConstraints(ref SpanStringBuilder builder)
|
||||
{
|
||||
var written = 0;
|
||||
|
||||
if (_lower is { } lower)
|
||||
{
|
||||
builder.AppendLiteral(lower.Inclusive ? ">=" : ">");
|
||||
builder.AppendFormatted(lower.Version, "f");
|
||||
|
||||
written++;
|
||||
}
|
||||
|
||||
if (_upper is { } upper)
|
||||
{
|
||||
if (written > 0)
|
||||
{
|
||||
builder.AppendFormatted(' ');
|
||||
}
|
||||
|
||||
builder.AppendLiteral(upper.Inclusive ? "<=" : "<");
|
||||
builder.AppendFormatted(upper.Version, "f");
|
||||
|
||||
written++;
|
||||
}
|
||||
|
||||
if (written > 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
builder.AppendLiteral("*");
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool AppendMavenExact(ref SpanStringBuilder builder, SemanticVersion version)
|
||||
{
|
||||
builder.AppendFormatted('[');
|
||||
builder.AppendFormatted(version, "f");
|
||||
builder.AppendFormatted(']');
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool AppendMavenRange(ref SpanStringBuilder builder, (SemanticVersion Version, bool Inclusive)? lower, (SemanticVersion Version, bool Inclusive)? upper)
|
||||
{
|
||||
builder.AppendFormatted(lower is { Inclusive: true } ? '[' : '(');
|
||||
|
||||
if (lower is { } lowerBound)
|
||||
{
|
||||
builder.AppendFormatted(lowerBound.Version, "f");
|
||||
}
|
||||
|
||||
builder.AppendFormatted(',');
|
||||
|
||||
if (upper is { } upperBound)
|
||||
{
|
||||
builder.AppendFormatted(upperBound.Version, "f");
|
||||
}
|
||||
|
||||
builder.AppendFormatted(upper is { Inclusive: true } ? ']' : ')');
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool AppendShortNpm(ref SpanStringBuilder builder, char shortOperator, SemanticVersion shortVersion)
|
||||
{
|
||||
builder.AppendFormatted(shortOperator);
|
||||
builder.AppendFormatted(shortVersion, "f");
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryGetShortNpmOperator(out char shortOperator, out SemanticVersion shortVersion)
|
||||
{
|
||||
if (_lower is { Inclusive: true } lower && _upper is { Inclusive: false } upper)
|
||||
{
|
||||
if (upper.Version == GetCaretUpperBound(lower.Version))
|
||||
{
|
||||
shortOperator = '^';
|
||||
shortVersion = lower.Version;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (upper.Version == GetTildeUpperBound(lower.Version))
|
||||
{
|
||||
shortOperator = '~';
|
||||
shortVersion = lower.Version;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
shortOperator = default;
|
||||
shortVersion = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool HasMatchingPrerelease(SemanticVersion candidate, SemanticVersion version)
|
||||
{
|
||||
return candidate.Prerelease is not null &&
|
||||
candidate.Major == version.Major &&
|
||||
candidate.Minor == version.Minor &&
|
||||
candidate.Patch == version.Patch;
|
||||
}
|
||||
}
|
||||
|
||||
private static SemanticVersion GetCaretUpperBound(SemanticVersion version, int components = 3)
|
||||
{
|
||||
return components switch
|
||||
{
|
||||
1 => new SemanticVersion(version.Major + 1, 0, 0),
|
||||
2 when version.Major is 0 => new SemanticVersion(0, version.Minor + 1, 0),
|
||||
2 => new SemanticVersion(version.Major + 1, 0, 0),
|
||||
_ when version.Major > 0 => new SemanticVersion(version.Major + 1, 0, 0),
|
||||
_ when version.Minor > 0 => new SemanticVersion(0, version.Minor + 1, 0),
|
||||
_ => new SemanticVersion(0, 0, version.Patch + 1),
|
||||
};
|
||||
}
|
||||
|
||||
private static SemanticVersion GetTildeUpperBound(SemanticVersion version, int components = 3)
|
||||
{
|
||||
return components switch
|
||||
{
|
||||
>= 2 => new SemanticVersion(version.Major, version.Minor + 1, 0),
|
||||
_ => new SemanticVersion(version.Major + 1, 0, 0),
|
||||
};
|
||||
}
|
||||
|
||||
private enum Comparator
|
||||
{
|
||||
Any,
|
||||
Equal,
|
||||
Greater,
|
||||
GreaterOrEqual,
|
||||
Less,
|
||||
LessOrEqual,
|
||||
NotEqual,
|
||||
return true;
|
||||
}
|
||||
}
|
||||
50
src/semver/SpanBuffer.cs
Normal file
50
src/semver/SpanBuffer.cs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
namespace Geekeey.SemVer;
|
||||
|
||||
internal ref struct SpanBuffer(Span<char> buffer)
|
||||
{
|
||||
private readonly Span<char> _buffer = buffer;
|
||||
public int Written { get; private set; }
|
||||
|
||||
public bool TryWrite(ReadOnlySpan<char> value)
|
||||
{
|
||||
if (!value.TryCopyTo(_buffer[Written..]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Written += value.Length;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryWrite<T>(T v) where T : ISpanFormattable
|
||||
{
|
||||
if (!v.TryFormat(_buffer[Written..], out var n, [], null))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Written += n;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryWrite<T>(T v, ReadOnlySpan<char> fmt) where T : ISpanFormattable
|
||||
{
|
||||
if (!v.TryFormat(_buffer[Written..], out var n, fmt, null))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Written += n;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Resets position to a saved checkpoint and returns false — useful in || chains.
|
||||
public bool Reset(int checkpoint)
|
||||
{
|
||||
Written = checkpoint;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
namespace Geekeey.SemVer;
|
||||
|
||||
internal ref struct SpanStringBuilder
|
||||
{
|
||||
private Span<char> _destination;
|
||||
private readonly IFormatProvider? _provider;
|
||||
|
||||
public SpanStringBuilder(Span<char> destination, IFormatProvider? provider = null)
|
||||
{
|
||||
_destination = destination;
|
||||
_provider = provider;
|
||||
CharsWritten = 0;
|
||||
Success = true;
|
||||
}
|
||||
|
||||
public int CharsWritten { get; private set; }
|
||||
|
||||
public bool Success { get; private set; }
|
||||
|
||||
public readonly bool TryComplete(out int charsWritten)
|
||||
{
|
||||
charsWritten = Success ? CharsWritten : 0;
|
||||
return Success;
|
||||
}
|
||||
|
||||
public void AppendLiteral(string? value)
|
||||
{
|
||||
AppendLiteral(value.AsSpan());
|
||||
}
|
||||
|
||||
public void AppendLiteral(ReadOnlySpan<char> value)
|
||||
{
|
||||
if (!Success)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value.TryCopyTo(_destination))
|
||||
{
|
||||
Success = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_destination = _destination[value.Length..];
|
||||
CharsWritten += value.Length;
|
||||
}
|
||||
|
||||
public void AppendFormatted(string? value)
|
||||
{
|
||||
AppendLiteral(value.AsSpan());
|
||||
}
|
||||
|
||||
public void AppendFormatted(ReadOnlySpan<char> value)
|
||||
{
|
||||
AppendLiteral(value);
|
||||
}
|
||||
|
||||
public void AppendFormatted<T>(T value) where T : ISpanFormattable
|
||||
{
|
||||
AppendFormatted(value, null);
|
||||
}
|
||||
|
||||
public void AppendFormatted<T>(T value, string? format) where T : ISpanFormattable
|
||||
{
|
||||
if (!Success)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value.TryFormat(_destination, out var written, format, _provider))
|
||||
{
|
||||
Success = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_destination = _destination[written..];
|
||||
CharsWritten += written;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue