fix: make SemanticVersionRange equality structural
The type was a record struct whose ==/Equals/GetHashCode were synthesized from all fields, including the _sets array, so two ranges that parse to the same constraint sets compared as unequal (reference-sensitive on the array). This also broke using ranges as dictionary keys, where lookup relies on value equality. Change the type to a plain readonly struct implementing IEquatable<SemanticVersionRange> with structural equality over the constraint sets (operation + version per comparator). The custom ToString already existed, so no display behaviour is lost. Add a failing-then-passing test.
This commit is contained in:
parent
95eb4f05a2
commit
b21bf0568e
6 changed files with 103 additions and 4 deletions
|
|
@ -17,6 +17,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|||
### Changed
|
||||
|
||||
- `SemanticVersion` `==`/`Equals`/`GetHashCode` now ignore build metadata, matching precedence comparison rules.
|
||||
- `SemanticVersionRange` equality (`==`, `Equals`, `GetHashCode`) is now structural and based
|
||||
on the constraint sets, so ranges that parse to the same comparators compare as equal
|
||||
(previously it was reference-sensitive on the backing array).
|
||||
|
||||
### Fixed
|
||||
|
||||
|
|
|
|||
|
|
@ -282,4 +282,14 @@ internal sealed class SemanticVersionRangeTests
|
|||
await Assert.That(success).IsFalse();
|
||||
await Assert.That(charsWritten).IsEqualTo(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task I_can_treat_structurally_equal_ranges_as_equal()
|
||||
{
|
||||
var a = SemanticVersionRange.Parse("^1.2.3");
|
||||
var b = SemanticVersionRange.Parse("[1.2.3,2.0.0)");
|
||||
|
||||
await Assert.That(a == b).IsTrue();
|
||||
await Assert.That(a.GetHashCode()).IsEqualTo(b.GetHashCode());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ using System.Runtime.CompilerServices;
|
|||
|
||||
namespace Geekeey.SemVer;
|
||||
|
||||
public readonly partial record struct SemanticVersionRange : ISpanFormattable
|
||||
public readonly partial struct SemanticVersionRange : ISpanFormattable
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ using System.Text.Json.Serialization;
|
|||
namespace Geekeey.SemVer;
|
||||
|
||||
[JsonConverter(typeof(SemanticVersionRangeJsonConverter))]
|
||||
public readonly partial record struct SemanticVersionRange
|
||||
public readonly partial struct SemanticVersionRange
|
||||
{
|
||||
internal sealed class SemanticVersionRangeJsonConverter : JsonConverter<SemanticVersionRange>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ using System.Diagnostics.CodeAnalysis;
|
|||
|
||||
namespace Geekeey.SemVer;
|
||||
|
||||
public readonly partial record struct SemanticVersionRange : ISpanParsable<SemanticVersionRange>
|
||||
public readonly partial struct SemanticVersionRange : ISpanParsable<SemanticVersionRange>
|
||||
{
|
||||
#region IParsable
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ namespace Geekeey.SemVer;
|
|||
/// 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 struct SemanticVersionRange : IEquatable<SemanticVersionRange>
|
||||
{
|
||||
// OR of AND-groups. null == empty range (matches nothing).
|
||||
private readonly ConstraintSet[]? _sets;
|
||||
|
|
@ -31,6 +31,92 @@ public readonly partial record struct SemanticVersionRange
|
|||
|
||||
return _sets.Any(set => set.Includes(version));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Equals(SemanticVersionRange other)
|
||||
{
|
||||
if (_sets is null || other._sets is null)
|
||||
{
|
||||
return _sets is null && other._sets is null;
|
||||
}
|
||||
|
||||
if (_sets.Length != other._sets.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < _sets.Length; i++)
|
||||
{
|
||||
if (!Equals(_sets[i], other._sets[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool Equals(ConstraintSet x, ConstraintSet y)
|
||||
{
|
||||
if (x.Constraints.Length != y.Constraints.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < x.Constraints.Length; i++)
|
||||
{
|
||||
if (x.Constraints[i].Operation != y.Constraints[i].Operation ||
|
||||
x.Constraints[i].Version != y.Constraints[i].Version)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is SemanticVersionRange other && Equals(other);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode()
|
||||
{
|
||||
if (_sets is null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var hash = new HashCode();
|
||||
foreach (var set in _sets)
|
||||
{
|
||||
foreach (var constraint in set.Constraints)
|
||||
{
|
||||
hash.Add(constraint.Operation);
|
||||
hash.Add(constraint.Version);
|
||||
}
|
||||
}
|
||||
|
||||
return hash.ToHashCode();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether two semantic version ranges are equal.
|
||||
/// </summary>
|
||||
public static bool operator ==(SemanticVersionRange left, SemanticVersionRange right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether two semantic version ranges differ.
|
||||
/// </summary>
|
||||
public static bool operator !=(SemanticVersionRange left, SemanticVersionRange right)
|
||||
{
|
||||
return !left.Equals(right);
|
||||
}
|
||||
}
|
||||
|
||||
internal enum Comparison { Eq, Neq, Lt, Lte, Gt, Gte }
|
||||
|
|
|
|||
Loading…
Reference in a new issue