diff --git a/docs/CHANGELOG-v3.md b/docs/CHANGELOG-v3.md index f28156904d..ba030b55c7 100644 --- a/docs/CHANGELOG-v3.md +++ b/docs/CHANGELOG-v3.md @@ -35,6 +35,9 @@ What's changed since pre-release v3.0.0-B0351: - Engineering: - Migrate samples into PSRule repository by @BernieWhite. [#2614](https://github.com/microsoft/PSRule/issues/2614) +- Bug fixes: + - Fixed string formatting of semantic version and constraints by @BernieWhite. + [#1828](https://github.com/microsoft/PSRule/issues/1828) ## v3.0.0-B0351 (pre-release) diff --git a/src/PSRule.Types/Data/SemanticVersion.cs b/src/PSRule.Types/Data/SemanticVersion.cs index 0f0138bfb9..5ea754bba6 100644 --- a/src/PSRule.Types/Data/SemanticVersion.cs +++ b/src/PSRule.Types/Data/SemanticVersion.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Diagnostics; +using System.Text; namespace PSRule.Data; @@ -13,8 +14,8 @@ public static class SemanticVersion private const char MINOR = '^'; private const char PATCH = '~'; private const char EQUAL = '='; - private const char VUPPER = 'V'; - private const char VLOWER = 'v'; + private const char V_UPPER = 'V'; + private const char V_LOWER = 'v'; private const char GREATER = '>'; private const char LESS = '<'; private const char DOT = '.'; @@ -77,22 +78,22 @@ internal enum ConstraintModifier public sealed class VersionConstraint : ISemanticVersionConstraint { private List? _Constraints; - private readonly string _Value; + private string? _ConstraintString; + private readonly bool _IncludePrerelease; /// /// A version constraint that accepts any version including pre-releases. /// - public static readonly VersionConstraint Any = new(string.Empty, includePrerelease: true); + public static readonly VersionConstraint Any = new(includePrerelease: true); /// /// A version constraint that accepts any stable version. /// - public static readonly VersionConstraint AnyStable = new(string.Empty, includePrerelease: false); + public static readonly VersionConstraint AnyStable = new(includePrerelease: false); - internal VersionConstraint(string value, bool includePrerelease) + internal VersionConstraint(bool includePrerelease) { - _Value = value; _IncludePrerelease = includePrerelease; } @@ -138,17 +139,18 @@ public bool Accepts(Version? version) /// public override string ToString() { - return _Value; + return _ConstraintString ??= GetConstraintString(); } /// public override int GetHashCode() { - return _Value.GetHashCode(); + return ToString().GetHashCode(); } internal void Join(int major, int minor, int patch, PR prid, ComparisonOperator flag, JoinOperator join, bool includePrerelease) { + _ConstraintString = null; _Constraints ??= []; _Constraints.Add(new ConstraintExpression( major, @@ -160,6 +162,36 @@ internal void Join(int major, int minor, int patch, PR prid, ComparisonOperator includePrerelease )); } + + private string GetConstraintString() + { + if (_Constraints == null || _Constraints.Count == 0) + return string.Empty; + + var sb = new StringBuilder(); + for (var i = 0; i < _Constraints.Count; i++) + { + if (i > 0) + { + sb.Append(SPACE); + + if (_Constraints[i].Join == JoinOperator.Or) + { + sb.Append(PIPE); + sb.Append(PIPE); + } + else if (_Constraints[i].Join == JoinOperator.And) + { + sb.Append(SPACE); + } + + sb.Append(SPACE); + } + + sb.Append(_Constraints[i].ToString()); + } + return sb.ToString(); + } } [DebuggerDisplay("{_Major}.{_Minor}.{_Patch}")] @@ -172,6 +204,8 @@ internal sealed class ConstraintExpression : ISemanticVersionConstraint private readonly PR _PRID; private readonly bool _IncludePrerelease; + private string? _ExpressionString; + internal ConstraintExpression(int major, int minor, int patch, PR prid, ComparisonOperator flag, JoinOperator join, bool includePrerelease) { _Flag = flag == ComparisonOperator.None ? ComparisonOperator.Equals : flag; @@ -187,6 +221,16 @@ internal ConstraintExpression(int major, int minor, int patch, PR prid, Comparis public JoinOperator Join { get; } + public override string ToString() + { + return _ExpressionString ??= GetExpressionString(); + } + + public override int GetHashCode() + { + return ToString().GetHashCode(); + } + public static bool TryParse(string value, out ISemanticVersionConstraint constraint) { return TryParseConstraint(value, out constraint); @@ -233,7 +277,7 @@ public bool Accepts(int major, int minor, int patch, PR? prid) return false; // Fail when not less - if (GaurdLess(major, minor, patch, prid)) + if (GuardLess(major, minor, patch, prid)) return false; // Fail with not less or equal to @@ -245,7 +289,7 @@ private bool GuardLessOrEqual(int major, int minor, int patch, PR? prid) return _Flag == (ComparisonOperator.LessThan | ComparisonOperator.Equals) && !(LT(major, minor, patch, prid) || EQ(major, minor, patch, prid)); } - private bool GaurdLess(int major, int minor, int patch, PR? prid) + private bool GuardLess(int major, int minor, int patch, PR? prid) { return _Flag == ComparisonOperator.LessThan && !LT(major, minor, patch, prid); } @@ -338,6 +382,56 @@ private static bool IsStable(PR? prid) { return prid == null || prid.Stable; } + + private string GetExpressionString() + { + var sb = new StringBuilder(); + switch (_Flag) + { + case ComparisonOperator.Equals: + sb.Append(EQUAL); + break; + + case ComparisonOperator.PatchUplift: + sb.Append(PATCH); + break; + + case ComparisonOperator.MinorUplift: + sb.Append(MINOR); + break; + + case ComparisonOperator.GreaterThan: + sb.Append(GREATER); + break; + + case ComparisonOperator.LessThan: + sb.Append(LESS); + break; + + case ComparisonOperator.GreaterThan | ComparisonOperator.Equals: + sb.Append(GREATER); + sb.Append(EQUAL); + break; + + case ComparisonOperator.LessThan | ComparisonOperator.Equals: + sb.Append(LESS); + sb.Append(EQUAL); + break; + } + + sb.Append(_Major); + sb.Append(DOT); + sb.Append(_Minor); + sb.Append(DOT); + sb.Append(_Patch); + if (_PRID != null && !string.IsNullOrEmpty(_PRID.Value)) + { + sb.Append(DASH); + sb.Append(_PRID.Value); + } + + return sb.ToString(); + } } /// @@ -376,9 +470,9 @@ public sealed class Version : IComparable, IEquatable internal Version(int major, int minor, int patch, PR prerelease, string build) { - Major = major; - Minor = minor; - Patch = patch; + Major = major >= 0 ? major : 0; + Minor = minor >= 0 ? minor : 0; + Patch = patch >= 0 ? patch : 0; Prerelease = prerelease; Build = build; } @@ -557,7 +651,7 @@ internal PR(string? value) public bool Stable => _Identifiers == null; /// - /// Compare the pre-release identifer to another pre-release identifier. + /// Compare the pre-release identifier to another pre-release identifier. /// public int CompareTo(PR? other) { @@ -714,7 +808,7 @@ private bool HasFlag(string value) private void SkipLeading() { - if (!EOF && (_Current == VUPPER || _Current == VLOWER)) + if (!EOF && (_Current == V_UPPER || _Current == V_LOWER)) Next(); } @@ -900,7 +994,7 @@ private static bool IsLetter(char c) /// public static bool TryParseConstraint(string value, out ISemanticVersionConstraint constraint, bool includePrerelease = false) { - var c = new VersionConstraint(value, includePrerelease); + var c = new VersionConstraint(includePrerelease); constraint = c; if (string.IsNullOrEmpty(value)) return true; diff --git a/tests/PSRule.Types.Tests/Data/SemanticVersionTests.cs b/tests/PSRule.Types.Tests/Data/SemanticVersionTests.cs index ad93401731..d182ab42ff 100644 --- a/tests/PSRule.Types.Tests/Data/SemanticVersionTests.cs +++ b/tests/PSRule.Types.Tests/Data/SemanticVersionTests.cs @@ -12,7 +12,7 @@ public sealed class SemanticVersionTests /// Test parsing of versions. /// [Fact] - public void Version() + public void SemanticVersion_WithTryParseVersion_ShouldParseSuccessfully() { Assert.True(SemanticVersion.TryParseVersion("1.2.3-alpha.3+7223b39", out var actual1)); Assert.Equal(1, actual1!.Major); @@ -38,7 +38,7 @@ public void Version() /// Test ordering of versions by comparison. /// [Fact] - public void VersionOrder() + public void SemanticVersion_WithCompareTo_ShouldCrossCompare() { Assert.True(SemanticVersion.TryParseVersion("1.0.0", out var actual1)); Assert.True(SemanticVersion.TryParseVersion("1.2.0", out var actual2)); @@ -59,7 +59,7 @@ public void VersionOrder() /// Test parsing of constraints. /// [Fact] - public void Constraint() + public void SemanticVersion_WithTryParseConstraint_ShouldAcceptMatchingVersions() { // Versions Assert.True(SemanticVersion.TryParseVersion("1.2.3", out var version1)); @@ -191,7 +191,7 @@ public void Constraint() /// Test parsing and order of pre-releases. /// [Fact] - public void Prerelease() + public void SemanticVersion_WithPrerelease_ShouldCrossCompare() { var actual1 = new SemanticVersion.PR(null); var actual2 = new SemanticVersion.PR("alpha"); @@ -220,20 +220,46 @@ public void Prerelease() [InlineData("1.2.3-alpha.3+7223b39")] [InlineData("3.4.5-alpha.9")] [InlineData("3.4.5+7223b39")] - public void ToString_WhenValid_ShouldReturnString(string version) + public void SemanticVersion_WithToString_ShouldReturnString(string version) { Assert.True(SemanticVersion.TryParseVersion(version, out var actual)); Assert.Equal(version, actual!.ToString()); } + /// + /// Test represented as a string with ToString() formats the string correctly. + /// + [Theory] + [InlineData("1", "1.0.0")] + [InlineData("1.2", "1.2.0")] + [InlineData("v1", "1.0.0")] + [InlineData("v1.2", "1.2.0")] + [InlineData("v1.2.3", "1.2.3")] + public void SemanticVersion_WithPartialVersion_ShouldReturnCompletedString(string version, string expected) + { + Assert.True(SemanticVersion.TryParseVersion(version, out var actual)); + Assert.Equal(expected, actual!.ToString()); + } + [Theory] [InlineData("1.2.3")] [InlineData("1.2.3-alpha.3+7223b39")] [InlineData("3.4.5-alpha.9")] [InlineData("3.4.5+7223b39")] - public void ToShortString_WhenValid_ShouldReturnString(string version) + public void SemanticVersion_WithToShortString_ShouldReturnString(string version) { Assert.True(SemanticVersion.TryParseVersion(version, out var actual)); Assert.Equal(string.Join(".", actual!.Major, actual.Minor, actual.Patch), actual!.ToShortString()); } + + [Theory] + [InlineData("1.2.3", "=1.2.3")] + [InlineData("=1.2.3", "=1.2.3")] + [InlineData(">=1.2.3", ">=1.2.3")] + [InlineData("1.2.3 ||>=3.4.5-0 || 3.4.5", "=1.2.3 || >=3.4.5-0 || =3.4.5")] + public void SemanticVersion_WithTryParseConstraint_ShouldReturnString(string constraint, string expected) + { + Assert.True(SemanticVersion.TryParseConstraint(constraint, out var actual)); + Assert.Equal(expected, actual!.ToString()); + } }