wip
This commit is contained in:
parent
73156ba97f
commit
8c93cf18a0
4 changed files with 133 additions and 111 deletions
|
|
@ -121,6 +121,9 @@ internal sealed class SemanticVersionRangeTests
|
||||||
[Arguments("^5.*", "5.1.1", "6.1.0", ">=5.0.0 <6.0.0")]
|
[Arguments("^5.*", "5.1.1", "6.1.0", ">=5.0.0 <6.0.0")]
|
||||||
[Arguments("5.*", "5.1.1", "6.1.0", ">=5.0.0 <6.0.0")]
|
[Arguments("5.*", "5.1.1", "6.1.0", ">=5.0.0 <6.0.0")]
|
||||||
[Arguments(">=5.*", "5.1.1", "6.1.0", ">=5.0.0 <6.0.0")]
|
[Arguments(">=5.*", "5.1.1", "6.1.0", ">=5.0.0 <6.0.0")]
|
||||||
|
[Arguments(">5.*", "5.0.1", "5.0.0", ">5.0.0 <6.0.0")]
|
||||||
|
[Arguments("<=5.*", "5.9.9", "6.0.0", "<6.0.0")]
|
||||||
|
[Arguments("<5.*", "4.9.9", "5.0.0", "<5.0.0")]
|
||||||
public async Task I_can_handle_ranges_with_wildcard(string range, string inside, string outside, string expected)
|
public async Task I_can_handle_ranges_with_wildcard(string range, string inside, string outside, string expected)
|
||||||
{
|
{
|
||||||
var r = SemanticVersionRange.Parse(range);
|
var r = SemanticVersionRange.Parse(range);
|
||||||
|
|
@ -163,7 +166,7 @@ internal sealed class SemanticVersionRangeTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
[Arguments("^1.2.3", "1.2.3-alpha", true)]
|
[Arguments("^1.2.3", "1.2.3-alpha", false)]
|
||||||
[Arguments("^1.2.3-alpha", "1.2.3-beta", 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.2.4", true)]
|
||||||
[Arguments("^1.2.3-alpha", "1.3.0-alpha", true)]
|
[Arguments("^1.2.3-alpha", "1.3.0-alpha", true)]
|
||||||
|
|
@ -174,6 +177,15 @@ internal sealed class SemanticVersionRangeTests
|
||||||
await Assert.That(r.Contains(v)).IsEqualTo(expected);
|
await Assert.That(r.Contains(v)).IsEqualTo(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Arguments(">=1.0.0 !2.0.0")]
|
||||||
|
[Arguments("1.2.3 - ")]
|
||||||
|
public async Task I_can_not_parse_invalid_ranges(string range)
|
||||||
|
{
|
||||||
|
await Assert.That(() => SemanticVersionRange.Parse(range))
|
||||||
|
.Throws<FormatException>();
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task I_can_serialize_to_json()
|
public async Task I_can_serialize_to_json()
|
||||||
{
|
{
|
||||||
|
|
@ -206,7 +218,6 @@ internal sealed class SemanticVersionRangeTests
|
||||||
[Arguments("[1.2.3,2.0.0)", "ns", "^1.2.3")]
|
[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.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.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("*", "m", "(,)")]
|
||||||
[Arguments("(,)", "ns", "*")]
|
[Arguments("(,)", "ns", "*")]
|
||||||
public async Task I_can_convert_range_formats(string range, string format, string expected)
|
public async Task I_can_convert_range_formats(string range, string format, string expected)
|
||||||
|
|
|
||||||
|
|
@ -123,8 +123,16 @@ public readonly partial record struct SemanticVersionRange : ISpanFormattable
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (group is { Lower: { Op: ComparatorOp.Gte or ComparatorOp.Gt, Version: var lo }, Upper: { Op: ComparatorOp.Lt or ComparatorOp.Lte, Version: var hi } upper })
|
if (group is not { Lower: { Op: ComparatorOp.Gte or ComparatorOp.Gt, Version: var lo } })
|
||||||
{
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group is not { Upper: { Op: ComparatorOp.Lt or ComparatorOp.Lte, Version: var hi } upper })
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (upper.Op is ComparatorOp.Lt && hi is { Minor: 0, Patch: 0, Prerelease: not { Length: > 0 } })
|
if (upper.Op is ComparatorOp.Lt && hi is { Minor: 0, Patch: 0, Prerelease: not { Length: > 0 } })
|
||||||
{
|
{
|
||||||
if (lo is { Major: > 0 } && hi.Major == lo.Major + 1)
|
if (lo is { Major: > 0 } && hi.Major == lo.Major + 1)
|
||||||
|
|
@ -150,7 +158,6 @@ public readonly partial record struct SemanticVersionRange : ISpanFormattable
|
||||||
return buf.TryWrite('~') && buf.TryWrite(lo);
|
return buf.TryWrite('~') && buf.TryWrite(lo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -200,11 +207,6 @@ public readonly partial record struct SemanticVersionRange : ISpanFormattable
|
||||||
return buf.TryWrite('[') && buf.TryWrite(version) && buf.TryWrite(']');
|
return buf.TryWrite('[') && buf.TryWrite(version) && buf.TryWrite(']');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (group.Comparators.Any(c => c.Op is ComparatorOp.Neq) || group.Comparators.Length > 2)
|
|
||||||
{
|
|
||||||
return TryFormatNormalNpm(ref buf, group);
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf.TryWrite(group.Lower?.Op == ComparatorOp.Gte ? '[' : '(') &&
|
return buf.TryWrite(group.Lower?.Op == ComparatorOp.Gte ? '[' : '(') &&
|
||||||
(!group.Lower.HasValue || buf.TryWrite(group.Lower.Value.Version)) &&
|
(!group.Lower.HasValue || buf.TryWrite(group.Lower.Value.Version)) &&
|
||||||
buf.TryWrite(',') &&
|
buf.TryWrite(',') &&
|
||||||
|
|
|
||||||
|
|
@ -206,24 +206,21 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
|
||||||
private static bool TryParseHyphenRange(ReadOnlySpan<char> s, out ComparatorGroup group)
|
private static bool TryParseHyphenRange(ReadOnlySpan<char> s, out ComparatorGroup group)
|
||||||
{
|
{
|
||||||
group = default;
|
group = default;
|
||||||
// Look for " - " (space-hyphen-space)
|
for (var i = 0; i < s.Length; i++)
|
||||||
for (var i = 1; i < s.Length - 1; i++)
|
|
||||||
{
|
{
|
||||||
if (s[i] == '-' && s[i - 1] == ' ' && i + 1 < s.Length && s[i + 1] == ' ')
|
if (s[i..] is [' ', '-', ' ', ..])
|
||||||
{
|
{
|
||||||
var lo = s[..(i - 1)].Trim();
|
if (!SemanticVersion.TryParse(s[..(i + 0)].Trim(), null, out var lo))
|
||||||
var hi = s[(i + 2)..].Trim();
|
|
||||||
if (!SemanticVersion.TryParse(lo, null, out var loV))
|
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!SemanticVersion.TryParse(hi, null, out var hiV))
|
if (!SemanticVersion.TryParse(s[(i + 3)..].Trim(), null, out var hi))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
group = new ComparatorGroup([new Comparator(ComparatorOp.Gte, loV), new Comparator(ComparatorOp.Lte, hiV)]);
|
group = new ComparatorGroup([new Comparator(ComparatorOp.Gte, lo), new Comparator(ComparatorOp.Lte, hi)]);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -276,6 +273,7 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
|
||||||
}
|
}
|
||||||
|
|
||||||
s = s[1..];
|
s = s[1..];
|
||||||
|
|
||||||
if (!SemanticVersion.TryParsePartially(s, out var lo, out var wildcard))
|
if (!SemanticVersion.TryParsePartially(s, out var lo, out var wildcard))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -299,55 +297,10 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
|
||||||
private static bool TryParseComparator(ReadOnlySpan<char> token, out Comparator result)
|
private static bool TryParseComparator(ReadOnlySpan<char> token, out Comparator result)
|
||||||
{
|
{
|
||||||
result = default;
|
result = default;
|
||||||
token = token.Trim();
|
|
||||||
if (token.IsEmpty)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse operator
|
if (!TryParseComparatorPrefix(token.Trim(), out var op, out token))
|
||||||
ComparatorOp op;
|
|
||||||
int opLen;
|
|
||||||
if (token.Length >= 2 && token[0] == '!' && token[1] == '=')
|
|
||||||
{
|
{
|
||||||
op = ComparatorOp.Neq;
|
if (!SemanticVersion.TryParse(token, null, out var version))
|
||||||
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -355,47 +308,74 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
|
||||||
result = new Comparator(op, version);
|
result = new Comparator(op, version);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!SemanticVersion.TryParse(token, null, out var version))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private static ReadOnlySpan<char> StripOp(ReadOnlySpan<char> s, out ComparatorOp op)
|
result = new Comparator(op, version);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseComparatorPrefix(ReadOnlySpan<char> s, out ComparatorOp op, out ReadOnlySpan<char> remainder)
|
||||||
{
|
{
|
||||||
op = ComparatorOp.Eq;
|
if (s.IsEmpty)
|
||||||
var i = 0;
|
|
||||||
if (s.Length >= 2)
|
|
||||||
{
|
{
|
||||||
if (s[0] == '>' && s[1] == '=')
|
op = default;
|
||||||
|
remainder = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (s)
|
||||||
{
|
{
|
||||||
|
case ['!', '=', ..]:
|
||||||
|
op = ComparatorOp.Neq;
|
||||||
|
remainder = s[2..];
|
||||||
|
return true;
|
||||||
|
case ['>', '=', ..]:
|
||||||
op = ComparatorOp.Gte;
|
op = ComparatorOp.Gte;
|
||||||
return s[2..];
|
remainder = s[2..];
|
||||||
}
|
return true;
|
||||||
|
case ['<', '=', ..]:
|
||||||
if (s[0] == '<' && s[1] == '=')
|
|
||||||
{
|
|
||||||
op = ComparatorOp.Lte;
|
op = ComparatorOp.Lte;
|
||||||
return s[2..];
|
remainder = s[2..];
|
||||||
|
return true;
|
||||||
|
case ['>', ..]:
|
||||||
|
op = ComparatorOp.Gt;
|
||||||
|
remainder = s[1..];
|
||||||
|
return true;
|
||||||
|
case ['<', ..]:
|
||||||
|
op = ComparatorOp.Lt;
|
||||||
|
remainder = s[1..];
|
||||||
|
return true;
|
||||||
|
case ['=', ..]:
|
||||||
|
op = ComparatorOp.Eq;
|
||||||
|
remainder = s[1..];
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
remainder = s;
|
||||||
|
op = default;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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> s, List<Comparator> comparators)
|
private static bool TryExpandWildcardComparators(ReadOnlySpan<char> s, List<Comparator> comparators)
|
||||||
{
|
{
|
||||||
s = StripOp(s.Trim(), out var op);
|
s = s.Trim();
|
||||||
|
if (s is ['*' or 'x' or 'X'])
|
||||||
if (op is not ComparatorOp.Eq)
|
|
||||||
{
|
{
|
||||||
return false;
|
// bare wildcards only make sense when it is an exact version comparison.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var op = ComparatorOp.Eq;
|
||||||
|
if (TryParseComparatorPrefix(s, out var parsedOp, out var remainder))
|
||||||
|
{
|
||||||
|
op = parsedOp;
|
||||||
|
s = remainder;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!SemanticVersion.TryParsePartially(s, out var lo, out var wildcard))
|
if (!SemanticVersion.TryParsePartially(s, out var lo, out var wildcard))
|
||||||
|
|
@ -405,7 +385,8 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
|
||||||
|
|
||||||
if (s is ['*' or 'x' or 'X'])
|
if (s is ['*' or 'x' or 'X'])
|
||||||
{
|
{
|
||||||
return true;
|
// bare wildcards only make sense when it is an exact version comparison.
|
||||||
|
return op == ComparatorOp.Eq;
|
||||||
}
|
}
|
||||||
|
|
||||||
SemanticVersion hi;
|
SemanticVersion hi;
|
||||||
|
|
@ -419,8 +400,27 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
|
||||||
hi = new SemanticVersion(lo.Major, lo.Minor + 1, 0);
|
hi = new SemanticVersion(lo.Major, lo.Minor + 1, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch (op)
|
||||||
|
{
|
||||||
|
case ComparatorOp.Eq:
|
||||||
|
case ComparatorOp.Gte:
|
||||||
comparators.Add(new Comparator(ComparatorOp.Gte, lo));
|
comparators.Add(new Comparator(ComparatorOp.Gte, lo));
|
||||||
comparators.Add(new Comparator(ComparatorOp.Lt, hi));
|
comparators.Add(new Comparator(ComparatorOp.Lt, hi));
|
||||||
|
break;
|
||||||
|
case ComparatorOp.Gt:
|
||||||
|
comparators.Add(new Comparator(ComparatorOp.Gt, lo));
|
||||||
|
comparators.Add(new Comparator(ComparatorOp.Lt, hi));
|
||||||
|
break;
|
||||||
|
case ComparatorOp.Lte:
|
||||||
|
comparators.Add(new Comparator(ComparatorOp.Lt, hi));
|
||||||
|
break;
|
||||||
|
case ComparatorOp.Lt:
|
||||||
|
comparators.Add(new Comparator(ComparatorOp.Lt, lo));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,10 @@
|
||||||
|
|
||||||
namespace Geekeey.SemVer;
|
namespace Geekeey.SemVer;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a semantic version range, which is a set of version constraints
|
||||||
|
/// used to match specific semantic versions based on defined ranges or patterns.
|
||||||
|
/// </summary>
|
||||||
public readonly partial record struct SemanticVersionRange
|
public readonly partial record struct SemanticVersionRange
|
||||||
{
|
{
|
||||||
// OR of AND-groups. null == empty range (matches nothing).
|
// OR of AND-groups. null == empty range (matches nothing).
|
||||||
|
|
@ -13,6 +17,11 @@ public readonly partial record struct SemanticVersionRange
|
||||||
_groups = groups;
|
_groups = groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether the specified version is contained in the range.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="version">The version to check.</param>
|
||||||
|
/// <returns><c>true</c> if the version is contained in the range, otherwise <c>false</c>.</returns>
|
||||||
public bool Contains(SemanticVersion version)
|
public bool Contains(SemanticVersion version)
|
||||||
{
|
{
|
||||||
if (_groups is null || _groups.Length == 0)
|
if (_groups is null || _groups.Length == 0)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue