Skip to content

Commit

Permalink
Fixed string formatting of semantic version and constraints #1828
Browse files Browse the repository at this point in the history
  • Loading branch information
BernieWhite committed Dec 21, 2024
1 parent b36b04f commit 690d29f
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 23 deletions.
3 changes: 3 additions & 0 deletions docs/CHANGELOG-v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
128 changes: 111 additions & 17 deletions src/PSRule.Types/Data/SemanticVersion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using System.Diagnostics;
using System.Text;

namespace PSRule.Data;

Expand All @@ -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 = '.';
Expand Down Expand Up @@ -77,22 +78,22 @@ internal enum ConstraintModifier
public sealed class VersionConstraint : ISemanticVersionConstraint
{
private List<ConstraintExpression>? _Constraints;
private readonly string _Value;
private string? _ConstraintString;

private readonly bool _IncludePrerelease;

/// <summary>
/// A version constraint that accepts any version including pre-releases.
/// </summary>
public static readonly VersionConstraint Any = new(string.Empty, includePrerelease: true);
public static readonly VersionConstraint Any = new(includePrerelease: true);

/// <summary>
/// A version constraint that accepts any stable version.
/// </summary>
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;
}

Expand Down Expand Up @@ -138,17 +139,18 @@ public bool Accepts(Version? version)
/// <inheritdoc/>
public override string ToString()
{
return _Value;
return _ConstraintString ??= GetConstraintString();
}

/// <inheritdoc/>
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,
Expand All @@ -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}")]
Expand All @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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
Expand All @@ -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);
}
Expand Down Expand Up @@ -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();
}
}

/// <summary>
Expand Down Expand Up @@ -376,9 +470,9 @@ public sealed class Version : IComparable<Version>, IEquatable<Version>

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;
}
Expand Down Expand Up @@ -557,7 +651,7 @@ internal PR(string? value)
public bool Stable => _Identifiers == null;

/// <summary>
/// Compare the pre-release identifer to another pre-release identifier.
/// Compare the pre-release identifier to another pre-release identifier.
/// </summary>
public int CompareTo(PR? other)
{
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -900,7 +994,7 @@ private static bool IsLetter(char c)
/// </summary>
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;
Expand Down
38 changes: 32 additions & 6 deletions tests/PSRule.Types.Tests/Data/SemanticVersionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public sealed class SemanticVersionTests
/// Test parsing of versions.
/// </summary>
[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);
Expand All @@ -38,7 +38,7 @@ public void Version()
/// Test ordering of versions by comparison.
/// </summary>
[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));
Expand All @@ -59,7 +59,7 @@ public void VersionOrder()
/// Test parsing of constraints.
/// </summary>
[Fact]
public void Constraint()
public void SemanticVersion_WithTryParseConstraint_ShouldAcceptMatchingVersions()
{
// Versions
Assert.True(SemanticVersion.TryParseVersion("1.2.3", out var version1));
Expand Down Expand Up @@ -191,7 +191,7 @@ public void Constraint()
/// Test parsing and order of pre-releases.
/// </summary>
[Fact]
public void Prerelease()
public void SemanticVersion_WithPrerelease_ShouldCrossCompare()
{
var actual1 = new SemanticVersion.PR(null);
var actual2 = new SemanticVersion.PR("alpha");
Expand Down Expand Up @@ -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());
}

/// <summary>
/// Test <see cref="SemanticVersion"/> represented as a string with <c>ToString()</c> formats the string correctly.
/// </summary>
[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());
}
}

0 comments on commit 690d29f

Please sign in to comment.