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.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)
|
||||
{
|
||||
var r = SemanticVersionRange.Parse(range);
|
||||
|
|
@ -163,7 +166,7 @@ internal sealed class SemanticVersionRangeTests
|
|||
}
|
||||
|
||||
[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.4", 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);
|
||||
}
|
||||
|
||||
[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]
|
||||
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,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)
|
||||
|
|
|
|||
|
|
@ -123,32 +123,39 @@ public readonly partial record struct SemanticVersionRange : ISpanFormattable
|
|||
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 } })
|
||||
{
|
||||
if (upper.Op is ComparatorOp.Lt && hi is { Minor: 0, Patch: 0, Prerelease: not { Length: > 0 } })
|
||||
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 (lo is { Major: > 0 } && hi.Major == lo.Major + 1)
|
||||
{
|
||||
if (lo is { Major: > 0 } && hi.Major == lo.Major + 1)
|
||||
{
|
||||
return buf.TryWrite('^') && buf.TryWrite(lo);
|
||||
}
|
||||
|
||||
if (lo is { Major: 0, Minor: > 0 } && hi is { Major: 0 } && hi.Minor == lo.Minor + 1)
|
||||
{
|
||||
return buf.TryWrite('^') && buf.TryWrite(lo);
|
||||
}
|
||||
|
||||
if (lo is { Major: 0, Minor: 0 } && hi is { Major: 0, Minor: 0 } && hi.Patch == lo.Patch + 1)
|
||||
{
|
||||
return buf.TryWrite('^') && buf.TryWrite(lo);
|
||||
}
|
||||
return buf.TryWrite('^') && buf.TryWrite(lo);
|
||||
}
|
||||
|
||||
if (upper.Op is ComparatorOp.Lt && hi is { Patch: 0, Prerelease: not { Length: > 0 } })
|
||||
if (lo is { Major: 0, Minor: > 0 } && hi is { Major: 0 } && hi.Minor == lo.Minor + 1)
|
||||
{
|
||||
if (hi.Major == lo.Major && hi.Minor == lo.Minor + 1)
|
||||
{
|
||||
return buf.TryWrite('~') && buf.TryWrite(lo);
|
||||
}
|
||||
return buf.TryWrite('^') && buf.TryWrite(lo);
|
||||
}
|
||||
|
||||
if (lo is { Major: 0, Minor: 0 } && hi is { Major: 0, Minor: 0 } && hi.Patch == lo.Patch + 1)
|
||||
{
|
||||
return buf.TryWrite('^') && buf.TryWrite(lo);
|
||||
}
|
||||
}
|
||||
|
||||
if (upper.Op is ComparatorOp.Lt && hi is { Patch: 0, Prerelease: not { Length: > 0 } })
|
||||
{
|
||||
if (hi.Major == lo.Major && hi.Minor == lo.Minor + 1)
|
||||
{
|
||||
return buf.TryWrite('~') && buf.TryWrite(lo);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -200,11 +207,6 @@ public readonly partial record struct SemanticVersionRange : ISpanFormattable
|
|||
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 ? '[' : '(') &&
|
||||
(!group.Lower.HasValue || buf.TryWrite(group.Lower.Value.Version)) &&
|
||||
buf.TryWrite(',') &&
|
||||
|
|
|
|||
|
|
@ -206,24 +206,21 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
|
|||
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++)
|
||||
for (var i = 0; i < s.Length; i++)
|
||||
{
|
||||
if (s[i] == '-' && s[i - 1] == ' ' && i + 1 < s.Length && s[i + 1] == ' ')
|
||||
if (s[i..] is [' ', '-', ' ', ..])
|
||||
{
|
||||
var lo = s[..(i - 1)].Trim();
|
||||
var hi = s[(i + 2)..].Trim();
|
||||
if (!SemanticVersion.TryParse(lo, null, out var loV))
|
||||
if (!SemanticVersion.TryParse(s[..(i + 0)].Trim(), null, out var lo))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!SemanticVersion.TryParse(hi, null, out var hiV))
|
||||
if (!SemanticVersion.TryParse(s[(i + 3)..].Trim(), null, out var hi))
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -276,6 +273,7 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
|
|||
}
|
||||
|
||||
s = s[1..];
|
||||
|
||||
if (!SemanticVersion.TryParsePartially(s, out var lo, out var wildcard))
|
||||
{
|
||||
return false;
|
||||
|
|
@ -299,103 +297,85 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
|
|||
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] == '=')
|
||||
if (!TryParseComparatorPrefix(token.Trim(), out var op, out token))
|
||||
{
|
||||
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;
|
||||
if (!SemanticVersion.TryParse(token, null, out var version))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = new Comparator(op, version);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (token[0])
|
||||
if (!SemanticVersion.TryParse(token, null, out var version))
|
||||
{
|
||||
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;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var vStr = token[opLen..];
|
||||
if (!SemanticVersion.TryParse(vStr, null, out var version))
|
||||
result = new Comparator(op, version);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseComparatorPrefix(ReadOnlySpan<char> s, out ComparatorOp op, out ReadOnlySpan<char> remainder)
|
||||
{
|
||||
if (s.IsEmpty)
|
||||
{
|
||||
op = default;
|
||||
remainder = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
result = new Comparator(op, version);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static ReadOnlySpan<char> StripOp(ReadOnlySpan<char> s, out ComparatorOp op)
|
||||
{
|
||||
op = ComparatorOp.Eq;
|
||||
var i = 0;
|
||||
if (s.Length >= 2)
|
||||
switch (s)
|
||||
{
|
||||
if (s[0] == '>' && s[1] == '=')
|
||||
{
|
||||
case ['!', '=', ..]:
|
||||
op = ComparatorOp.Neq;
|
||||
remainder = s[2..];
|
||||
return true;
|
||||
case ['>', '=', ..]:
|
||||
op = ComparatorOp.Gte;
|
||||
return s[2..];
|
||||
}
|
||||
|
||||
if (s[0] == '<' && s[1] == '=')
|
||||
{
|
||||
remainder = s[2..];
|
||||
return true;
|
||||
case ['<', '=', ..]:
|
||||
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)
|
||||
{
|
||||
s = StripOp(s.Trim(), out var op);
|
||||
|
||||
if (op is not ComparatorOp.Eq)
|
||||
s = s.Trim();
|
||||
if (s is ['*' or 'x' or 'X'])
|
||||
{
|
||||
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))
|
||||
|
|
@ -405,7 +385,8 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
|
|||
|
||||
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;
|
||||
|
|
@ -419,8 +400,27 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
|
|||
hi = new SemanticVersion(lo.Major, lo.Minor + 1, 0);
|
||||
}
|
||||
|
||||
comparators.Add(new Comparator(ComparatorOp.Gte, lo));
|
||||
comparators.Add(new Comparator(ComparatorOp.Lt, hi));
|
||||
switch (op)
|
||||
{
|
||||
case ComparatorOp.Eq:
|
||||
case ComparatorOp.Gte:
|
||||
comparators.Add(new Comparator(ComparatorOp.Gte, lo));
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@
|
|||
|
||||
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
|
||||
{
|
||||
// OR of AND-groups. null == empty range (matches nothing).
|
||||
|
|
@ -13,6 +17,11 @@ public readonly partial record struct SemanticVersionRange
|
|||
_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)
|
||||
{
|
||||
if (_groups is null || _groups.Length == 0)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue