From 95eb4f05a2d252f83e8ca0e8889e642a25ac7714 Mon Sep 17 00:00:00 2001 From: Louis Seubert Date: Sat, 11 Jul 2026 21:57:45 +0200 Subject: [PATCH] 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 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. --- CHANGELOG.md | 2 ++ src/semver.tests/SemanticVersionTests.cs | 13 ++++++++ src/semver/SemanticVersion.Comparison.cs | 2 +- src/semver/SemanticVersion.Formatting.cs | 2 +- src/semver/SemanticVersion.JsonConverter.cs | 2 +- src/semver/SemanticVersion.Parsing.cs | 2 +- src/semver/SemanticVersion.cs | 36 ++++++++++++++++++++- 7 files changed, 54 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4cc6a8..5db6072 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ 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. + ### Fixed - Reject invalid SemVer identifiers when parsing versions. diff --git a/src/semver.tests/SemanticVersionTests.cs b/src/semver.tests/SemanticVersionTests.cs index 1cb7eed..587d643 100644 --- a/src/semver.tests/SemanticVersionTests.cs +++ b/src/semver.tests/SemanticVersionTests.cs @@ -212,6 +212,19 @@ internal sealed class SemanticVersionTests 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] public async Task I_can_handle_invalid_json_token() { diff --git a/src/semver/SemanticVersion.Comparison.cs b/src/semver/SemanticVersion.Comparison.cs index 6e2a43b..8c6d7e4 100644 --- a/src/semver/SemanticVersion.Comparison.cs +++ b/src/semver/SemanticVersion.Comparison.cs @@ -3,7 +3,7 @@ namespace Geekeey.SemVer; -public readonly partial record struct SemanticVersion : IComparable, IComparable +public readonly partial struct SemanticVersion : IComparable, IComparable { /// public int CompareTo(object? obj) diff --git a/src/semver/SemanticVersion.Formatting.cs b/src/semver/SemanticVersion.Formatting.cs index 890a35d..5634b27 100644 --- a/src/semver/SemanticVersion.Formatting.cs +++ b/src/semver/SemanticVersion.Formatting.cs @@ -5,7 +5,7 @@ using System.Runtime.CompilerServices; namespace Geekeey.SemVer; -public readonly partial record struct SemanticVersion : ISpanFormattable +public readonly partial struct SemanticVersion : ISpanFormattable { /// public override string ToString() diff --git a/src/semver/SemanticVersion.JsonConverter.cs b/src/semver/SemanticVersion.JsonConverter.cs index a0c200c..c677e3a 100644 --- a/src/semver/SemanticVersion.JsonConverter.cs +++ b/src/semver/SemanticVersion.JsonConverter.cs @@ -7,7 +7,7 @@ using System.Text.Json.Serialization; namespace Geekeey.SemVer; [JsonConverter(typeof(SemanticVersionJsonConverter))] -public readonly partial record struct SemanticVersion +public readonly partial struct SemanticVersion { internal sealed class SemanticVersionJsonConverter : JsonConverter { diff --git a/src/semver/SemanticVersion.Parsing.cs b/src/semver/SemanticVersion.Parsing.cs index e24802e..ab43eb3 100644 --- a/src/semver/SemanticVersion.Parsing.cs +++ b/src/semver/SemanticVersion.Parsing.cs @@ -6,7 +6,7 @@ using System.Globalization; namespace Geekeey.SemVer; -public readonly partial record struct SemanticVersion : ISpanParsable +public readonly partial struct SemanticVersion : ISpanParsable { #region IParsable diff --git a/src/semver/SemanticVersion.cs b/src/semver/SemanticVersion.cs index bf25b83..22311b3 100644 --- a/src/semver/SemanticVersion.cs +++ b/src/semver/SemanticVersion.cs @@ -6,7 +6,7 @@ namespace Geekeey.SemVer; /// /// Represents a semantic version that adheres to the Semantic Versioning 2.0.0 specification. /// -public readonly partial record struct SemanticVersion +public readonly partial struct SemanticVersion : IEquatable { /// /// Converts a 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. /// public string? Metadata { get; } + + /// + public bool Equals(SemanticVersion other) + { + return SemanticVersionComparer.Priority.Equals(this, other); + } + + /// + public override bool Equals(object? obj) + { + return obj is SemanticVersion other && Equals(other); + } + + /// + public override int GetHashCode() + { + return SemanticVersionComparer.Priority.GetHashCode(this); + } + + /// + /// Determines whether two versions have equal precedence. + /// + public static bool operator ==(SemanticVersion left, SemanticVersion right) + { + return left.Equals(right); + } + + /// + /// Determines whether two versions differ in precedence. + /// + public static bool operator !=(SemanticVersion left, SemanticVersion right) + { + return !left.Equals(right); + } }