fix: make ==/Equals ignore build metadata for precedence

Previously SemanticVersion was a record struct whose ==/Equals/
GetHashCode were synthesized from all fields, so versions that
differ only in build metadata were not equal. Range matching
already used precedence (ignoring metadata), creating an
inconsistency.

Change the type to a plain readonly struct implementing
IEquatable<SemanticVersion> and define ==/Equals/GetHashCode
in terms of SemanticVersionComparer.Priority, so build
metadata is ignored. The custom ToString already existed, so no
display behaviour is lost.

Add a failing-then-passing test.
This commit is contained in:
Louis Seubert 2026-07-11 21:57:45 +02:00
commit 95eb4f05a2
7 changed files with 54 additions and 5 deletions

View file

@ -16,6 +16,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
### Changed ### Changed
- `SemanticVersion` `==`/`Equals`/`GetHashCode` now ignore build metadata, matching precedence comparison rules.
### Fixed ### Fixed
- Reject invalid SemVer identifiers when parsing versions. - Reject invalid SemVer identifiers when parsing versions.

View file

@ -212,6 +212,19 @@ internal sealed class SemanticVersionTests
await Assert.That(SemanticVersion.TryParse(input, out _)).IsTrue(); await Assert.That(SemanticVersion.TryParse(input, out _)).IsTrue();
} }
[Test]
public async Task I_can_treat_versions_as_equal_when_only_build_metadata_differs()
{
var a = SemanticVersion.Parse("1.2.3");
var b = SemanticVersion.Parse("1.2.3+build.7");
await Assert.That(a == b).IsTrue();
await Assert.That(a.GetHashCode()).IsEqualTo(b.GetHashCode());
var c = SemanticVersion.Parse("1.2.3-alpha");
await Assert.That(a == c).IsFalse();
}
[Test] [Test]
public async Task I_can_handle_invalid_json_token() public async Task I_can_handle_invalid_json_token()
{ {

View file

@ -3,7 +3,7 @@
namespace Geekeey.SemVer; namespace Geekeey.SemVer;
public readonly partial record struct SemanticVersion : IComparable, IComparable<SemanticVersion> public readonly partial struct SemanticVersion : IComparable, IComparable<SemanticVersion>
{ {
/// <inheritdoc /> /// <inheritdoc />
public int CompareTo(object? obj) public int CompareTo(object? obj)

View file

@ -5,7 +5,7 @@ using System.Runtime.CompilerServices;
namespace Geekeey.SemVer; namespace Geekeey.SemVer;
public readonly partial record struct SemanticVersion : ISpanFormattable public readonly partial struct SemanticVersion : ISpanFormattable
{ {
/// <inheritdoc /> /// <inheritdoc />
public override string ToString() public override string ToString()

View file

@ -7,7 +7,7 @@ using System.Text.Json.Serialization;
namespace Geekeey.SemVer; namespace Geekeey.SemVer;
[JsonConverter(typeof(SemanticVersionJsonConverter))] [JsonConverter(typeof(SemanticVersionJsonConverter))]
public readonly partial record struct SemanticVersion public readonly partial struct SemanticVersion
{ {
internal sealed class SemanticVersionJsonConverter : JsonConverter<SemanticVersion> internal sealed class SemanticVersionJsonConverter : JsonConverter<SemanticVersion>
{ {

View file

@ -6,7 +6,7 @@ using System.Globalization;
namespace Geekeey.SemVer; namespace Geekeey.SemVer;
public readonly partial record struct SemanticVersion : ISpanParsable<SemanticVersion> public readonly partial struct SemanticVersion : ISpanParsable<SemanticVersion>
{ {
#region IParsable #region IParsable

View file

@ -6,7 +6,7 @@ namespace Geekeey.SemVer;
/// <summary> /// <summary>
/// Represents a semantic version that adheres to the Semantic Versioning 2.0.0 specification. /// Represents a semantic version that adheres to the Semantic Versioning 2.0.0 specification.
/// </summary> /// </summary>
public readonly partial record struct SemanticVersion public readonly partial struct SemanticVersion : IEquatable<SemanticVersion>
{ {
/// <summary> /// <summary>
/// Converts a <see cref="Version"/> into the equivalent semantic version. /// Converts a <see cref="Version"/> into the equivalent semantic version.
@ -105,4 +105,38 @@ public readonly partial record struct SemanticVersion
/// details. It does not affect the precedence of the version. /// details. It does not affect the precedence of the version.
/// </summary> /// </summary>
public string? Metadata { get; } public string? Metadata { get; }
/// <inheritdoc />
public bool Equals(SemanticVersion other)
{
return SemanticVersionComparer.Priority.Equals(this, other);
}
/// <inheritdoc />
public override bool Equals(object? obj)
{
return obj is SemanticVersion other && Equals(other);
}
/// <inheritdoc />
public override int GetHashCode()
{
return SemanticVersionComparer.Priority.GetHashCode(this);
}
/// <summary>
/// Determines whether two versions have equal precedence.
/// </summary>
public static bool operator ==(SemanticVersion left, SemanticVersion right)
{
return left.Equals(right);
}
/// <summary>
/// Determines whether two versions differ in precedence.
/// </summary>
public static bool operator !=(SemanticVersion left, SemanticVersion right)
{
return !left.Equals(right);
}
} }