enable nullable annotations for semantic versions (#837)

This commit is contained in:
Jesse Plamondon-Willard 2022-04-07 00:56:00 -04:00
parent 3f9b412bed
commit ab6cf45b03
No known key found for this signature in database
GPG Key ID: CF8B1456B3E29F49
5 changed files with 91 additions and 75 deletions

View File

@ -1,5 +1,3 @@
#nullable disable
using System;
using System.Diagnostics.CodeAnalysis;
using Newtonsoft.Json;
@ -63,10 +61,10 @@ namespace SMAPI.Tests.Utilities
[TestCase("apple")]
[TestCase("-apple")]
[TestCase("-5")]
public void Constructor_FromString_WithInvalidValues(string input)
public void Constructor_FromString_WithInvalidValues(string? input)
{
if (input == null)
this.AssertAndLogException<ArgumentNullException>(() => new SemanticVersion(input));
this.AssertAndLogException<ArgumentNullException>(() => new SemanticVersion(input!));
else
this.AssertAndLogException<FormatException>(() => new SemanticVersion(input));
}
@ -93,7 +91,7 @@ namespace SMAPI.Tests.Utilities
[TestCase("1.2.3.4-some-tag.4 ")]
public void Constructor_FromString_Standard_DisallowsNonStandardVersion(string input)
{
Assert.Throws<FormatException>(() => new SemanticVersion(input));
Assert.Throws<FormatException>(() => _ = new SemanticVersion(input));
}
/// <summary>Assert the parsed version when constructed from standard parts.</summary>
@ -112,7 +110,7 @@ namespace SMAPI.Tests.Utilities
[TestCase(1, 2, 3, "some-tag.4 ", null, ExpectedResult = "1.2.3-some-tag.4")]
[TestCase(1, 2, 3, "some-tag.4 ", "build.004", ExpectedResult = "1.2.3-some-tag.4+build.004")]
[TestCase(1, 2, 0, null, "3.4.5-build.004", ExpectedResult = "1.2.0+3.4.5-build.004")]
public string Constructor_FromParts(int major, int minor, int patch, string prerelease, string build)
public string Constructor_FromParts(int major, int minor, int patch, string? prerelease, string? build)
{
// act
ISemanticVersion version = new SemanticVersion(major, minor, patch, prerelease, build);
@ -222,11 +220,11 @@ namespace SMAPI.Tests.Utilities
// null
[TestCase("1.0.0", null, ExpectedResult = 1)] // null is always less than any value per CompareTo remarks
public int CompareTo(string versionStrA, string versionStrB)
public int CompareTo(string versionStrA, string? versionStrB)
{
// arrange
ISemanticVersion versionA = new SemanticVersion(versionStrA);
ISemanticVersion versionB = versionStrB != null
ISemanticVersion? versionB = versionStrB != null
? new SemanticVersion(versionStrB)
: null;
@ -270,11 +268,11 @@ namespace SMAPI.Tests.Utilities
// null
[TestCase("1.0.0", null, ExpectedResult = false)] // null is always less than any value per CompareTo remarks
public bool IsOlderThan(string versionStrA, string versionStrB)
public bool IsOlderThan(string versionStrA, string? versionStrB)
{
// arrange
ISemanticVersion versionA = new SemanticVersion(versionStrA);
ISemanticVersion versionB = versionStrB != null
ISemanticVersion? versionB = versionStrB != null
? new SemanticVersion(versionStrB)
: null;
@ -319,11 +317,11 @@ namespace SMAPI.Tests.Utilities
// null
[TestCase("1.0.0", null, ExpectedResult = true)] // null is always less than any value per CompareTo remarks
public bool IsNewerThan(string versionStrA, string versionStrB)
public bool IsNewerThan(string versionStrA, string? versionStrB)
{
// arrange
ISemanticVersion versionA = new SemanticVersion(versionStrA);
ISemanticVersion versionB = versionStrB != null
ISemanticVersion? versionB = versionStrB != null
? new SemanticVersion(versionStrB)
: null;
@ -356,13 +354,13 @@ namespace SMAPI.Tests.Utilities
[TestCase("1.0-beta.2", "1.0-beta.10", "1.0-beta.3", ExpectedResult = false)]
[TestCase("1.0-beta-2", "1.0-beta-10", "1.0-beta-3", ExpectedResult = false)]
[TestCase("1.0.0", "1.0.0", null, ExpectedResult = false)] // null is always less than any value per CompareTo remarks
public bool IsBetween(string versionStr, string lowerStr, string upperStr)
public bool IsBetween(string versionStr, string? lowerStr, string? upperStr)
{
// arrange
ISemanticVersion lower = lowerStr != null
ISemanticVersion? lower = lowerStr != null
? new SemanticVersion(lowerStr)
: null;
ISemanticVersion upper = upperStr != null
ISemanticVersion? upper = upperStr != null
? new SemanticVersion(upperStr)
: null;
ISemanticVersion version = new SemanticVersion(versionStr);
@ -436,7 +434,7 @@ namespace SMAPI.Tests.Utilities
/// <param name="prerelease">The prerelease tag.</param>
/// <param name="build">The build metadata.</param>
/// <param name="nonStandard">Whether the version should be marked as non-standard.</param>
private void AssertParts(ISemanticVersion version, int major, int minor, int patch, string prerelease, string build, bool nonStandard)
private void AssertParts(ISemanticVersion version, int major, int minor, int patch, string? prerelease, string? build, bool nonStandard)
{
Assert.AreEqual(major, version.MajorVersion, "The major version doesn't match.");
Assert.AreEqual(minor, version.MinorVersion, "The minor version doesn't match.");
@ -449,9 +447,8 @@ namespace SMAPI.Tests.Utilities
/// <summary>Assert that the expected exception type is thrown, and log the action output and thrown exception.</summary>
/// <typeparam name="T">The expected exception type.</typeparam>
/// <param name="action">The action which may throw the exception.</param>
/// <param name="message">The message to log if the expected exception isn't thrown.</param>
[SuppressMessage("ReSharper", "UnusedParameter.Local", Justification = "The message argument is deliberately only used in precondition checks since this is an assertion method.")]
private void AssertAndLogException<T>(Func<object> action, string message = null)
private void AssertAndLogException<T>(Func<object> action)
where T : Exception
{
this.AssertAndLogException<T>(() =>
@ -466,7 +463,7 @@ namespace SMAPI.Tests.Utilities
/// <param name="action">The action which may throw the exception.</param>
/// <param name="message">The message to log if the expected exception isn't thrown.</param>
[SuppressMessage("ReSharper", "UnusedParameter.Local", Justification = "The message argument is deliberately only used in precondition checks since this is an assertion method.")]
private void AssertAndLogException<T>(Action action, string message = null)
private void AssertAndLogException<T>(Action action, string? message = null)
where T : Exception
{
try

View File

@ -1,5 +1,3 @@
#nullable disable
using System;
namespace StardewModdingAPI
@ -20,10 +18,10 @@ namespace StardewModdingAPI
int PatchVersion { get; }
/// <summary>An optional prerelease tag.</summary>
string PrereleaseTag { get; }
string? PrereleaseTag { get; }
/// <summary>Optional build metadata. This is ignored when determining version precedence.</summary>
string BuildMetadata { get; }
string? BuildMetadata { get; }
/*********
@ -34,32 +32,38 @@ namespace StardewModdingAPI
/// <summary>Get whether this version is older than the specified version.</summary>
/// <param name="other">The version to compare with this instance.</param>
bool IsOlderThan(ISemanticVersion other);
/// <remarks>Although the <paramref name="other"/> parameter is nullable, it isn't optional. A <c>null</c> version is considered earlier than every possible valid version, so passing <c>null</c> to <paramref name="other"/> will always return false.</remarks>
bool IsOlderThan(ISemanticVersion? other);
/// <summary>Get whether this version is older than the specified version.</summary>
/// <param name="other">The version to compare with this instance.</param>
/// <param name="other">The version to compare with this instance. A null value is never older.</param>
/// <exception cref="FormatException">The specified version is not a valid semantic version.</exception>
bool IsOlderThan(string other);
/// <remarks>Although the <paramref name="other"/> parameter is nullable, it isn't optional. A <c>null</c> version is considered earlier than every possible valid version, so passing <c>null</c> to <paramref name="other"/> will always return false.</remarks>
bool IsOlderThan(string? other);
/// <summary>Get whether this version is newer than the specified version.</summary>
/// <param name="other">The version to compare with this instance.</param>
bool IsNewerThan(ISemanticVersion other);
/// <param name="other">The version to compare with this instance. A null value is always older.</param>
/// <remarks>Although the <paramref name="other"/> parameter is nullable, it isn't optional. A <c>null</c> version is considered earlier than every possible valid version, so passing <c>null</c> to <paramref name="other"/> will always return true.</remarks>
bool IsNewerThan(ISemanticVersion? other);
/// <summary>Get whether this version is newer than the specified version.</summary>
/// <param name="other">The version to compare with this instance.</param>
/// <param name="other">The version to compare with this instance. A null value is always older.</param>
/// <exception cref="FormatException">The specified version is not a valid semantic version.</exception>
bool IsNewerThan(string other);
/// <remarks>Although the <paramref name="other"/> parameter is nullable, it isn't optional. A <c>null</c> version is considered earlier than every possible valid version, so passing <c>null</c> to <paramref name="other"/> will always return true.</remarks>
bool IsNewerThan(string? other);
/// <summary>Get whether this version is between two specified versions (inclusively).</summary>
/// <param name="min">The minimum version.</param>
/// <param name="max">The maximum version.</param>
bool IsBetween(ISemanticVersion min, ISemanticVersion max);
/// <param name="min">The minimum version. A null value is always older.</param>
/// <param name="max">The maximum version. A null value is never newer.</param>
/// <remarks>Although the <paramref name="min"/> and <paramref name="max"/> parameters are nullable, they are not optional. A <c>null</c> version is considered earlier than every possible valid version. For example, passing <c>null</c> to <paramref name="max"/> will always return false, since no valid version can be earlier than <c>null</c>.</remarks>
bool IsBetween(ISemanticVersion? min, ISemanticVersion? max);
/// <summary>Get whether this version is between two specified versions (inclusively).</summary>
/// <param name="min">The minimum version.</param>
/// <param name="max">The maximum version.</param>
/// <param name="min">The minimum version. A null value is always older.</param>
/// <param name="max">The maximum version. A null value is never newer.</param>
/// <exception cref="FormatException">One of the specified versions is not a valid semantic version.</exception>
bool IsBetween(string min, string max);
/// <remarks>Although the <paramref name="min"/> and <paramref name="max"/> parameters are nullable, they are not optional. A <c>null</c> version is considered earlier than every possible valid version. For example, passing <c>null</c> to <paramref name="max"/> will always return false, since no valid version can be earlier than <c>null</c>.</remarks>
bool IsBetween(string? min, string? max);
/// <summary>Get a string representation of the version.</summary>
string ToString();

View File

@ -1,6 +1,5 @@
#nullable disable
using System;
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
using StardewModdingAPI.Toolkit.Framework;
@ -40,10 +39,10 @@ namespace StardewModdingAPI.Toolkit
public int PlatformRelease { get; }
/// <inheritdoc />
public string PrereleaseTag { get; }
public string? PrereleaseTag { get; }
/// <inheritdoc />
public string BuildMetadata { get; }
public string? BuildMetadata { get; }
/*********
@ -56,7 +55,7 @@ namespace StardewModdingAPI.Toolkit
/// <param name="platformRelease">The platform-specific version (if applicable).</param>
/// <param name="prereleaseTag">An optional prerelease tag.</param>
/// <param name="buildMetadata">Optional build metadata. This is ignored when determining version precedence.</param>
public SemanticVersion(int major, int minor, int patch, int platformRelease = 0, string prereleaseTag = null, string buildMetadata = null)
public SemanticVersion(int major, int minor, int patch, int platformRelease = 0, string? prereleaseTag = null, string? buildMetadata = null)
{
this.MajorVersion = major;
this.MinorVersion = minor;
@ -106,7 +105,7 @@ namespace StardewModdingAPI.Toolkit
}
/// <inheritdoc />
public int CompareTo(ISemanticVersion other)
public int CompareTo(ISemanticVersion? other)
{
return other == null
? 1
@ -114,7 +113,7 @@ namespace StardewModdingAPI.Toolkit
}
/// <inheritdoc />
public bool Equals(ISemanticVersion other)
public bool Equals(ISemanticVersion? other)
{
return other != null && this.CompareTo(other) == 0;
}
@ -126,15 +125,15 @@ namespace StardewModdingAPI.Toolkit
}
/// <inheritdoc />
public bool IsOlderThan(ISemanticVersion other)
public bool IsOlderThan(ISemanticVersion? other)
{
return this.CompareTo(other) < 0;
}
/// <inheritdoc />
public bool IsOlderThan(string other)
public bool IsOlderThan(string? other)
{
ISemanticVersion otherVersion = other != null
ISemanticVersion? otherVersion = other != null
? new SemanticVersion(other, allowNonStandard: true)
: null;
@ -142,15 +141,15 @@ namespace StardewModdingAPI.Toolkit
}
/// <inheritdoc />
public bool IsNewerThan(ISemanticVersion other)
public bool IsNewerThan(ISemanticVersion? other)
{
return this.CompareTo(other) > 0;
}
/// <inheritdoc />
public bool IsNewerThan(string other)
public bool IsNewerThan(string? other)
{
ISemanticVersion otherVersion = other != null
ISemanticVersion? otherVersion = other != null
? new SemanticVersion(other, allowNonStandard: true)
: null;
@ -158,18 +157,18 @@ namespace StardewModdingAPI.Toolkit
}
/// <inheritdoc />
public bool IsBetween(ISemanticVersion min, ISemanticVersion max)
public bool IsBetween(ISemanticVersion? min, ISemanticVersion? max)
{
return this.CompareTo(min) >= 0 && this.CompareTo(max) <= 0;
}
/// <inheritdoc />
public bool IsBetween(string min, string max)
public bool IsBetween(string? min, string? max)
{
ISemanticVersion minVersion = min != null
ISemanticVersion? minVersion = min != null
? new SemanticVersion(min, allowNonStandard: true)
: null;
ISemanticVersion maxVersion = max != null
ISemanticVersion? maxVersion = max != null
? new SemanticVersion(max, allowNonStandard: true)
: null;
@ -199,7 +198,12 @@ namespace StardewModdingAPI.Toolkit
/// <param name="version">The version string.</param>
/// <param name="parsed">The parsed representation.</param>
/// <returns>Returns whether parsing the version succeeded.</returns>
public static bool TryParse(string version, out ISemanticVersion parsed)
public static bool TryParse(string? version,
#if NET5_0_OR_GREATER
[NotNullWhen(true)]
#endif
out ISemanticVersion? parsed
)
{
return SemanticVersion.TryParse(version, allowNonStandard: false, out parsed);
}
@ -209,8 +213,19 @@ namespace StardewModdingAPI.Toolkit
/// <param name="allowNonStandard">Whether to allow non-standard extensions to semantic versioning.</param>
/// <param name="parsed">The parsed representation.</param>
/// <returns>Returns whether parsing the version succeeded.</returns>
public static bool TryParse(string version, bool allowNonStandard, out ISemanticVersion parsed)
public static bool TryParse(string? version, bool allowNonStandard,
#if NET5_0_OR_GREATER
[NotNullWhen(true)]
#endif
out ISemanticVersion? parsed
)
{
if (version == null)
{
parsed = null;
return false;
}
try
{
parsed = new SemanticVersion(version, allowNonStandard);
@ -229,7 +244,7 @@ namespace StardewModdingAPI.Toolkit
*********/
/// <summary>Get a normalized prerelease or build tag.</summary>
/// <param name="tag">The tag to normalize.</param>
private string GetNormalizedTag(string tag)
private string? GetNormalizedTag(string? tag)
{
tag = tag?.Trim();
return !string.IsNullOrWhiteSpace(tag) ? tag : null;
@ -241,7 +256,7 @@ namespace StardewModdingAPI.Toolkit
/// <param name="otherPatch">The patch version to compare with this instance.</param>
/// <param name="otherPlatformRelease">The non-standard platform release to compare with this instance.</param>
/// <param name="otherTag">The prerelease tag to compare with this instance.</param>
private int CompareTo(int otherMajor, int otherMinor, int otherPatch, int otherPlatformRelease, string otherTag)
private int CompareTo(int otherMajor, int otherMinor, int otherPatch, int otherPlatformRelease, string? otherTag)
{
const int same = 0;
const int curNewer = 1;
@ -270,8 +285,8 @@ namespace StardewModdingAPI.Toolkit
return curOlder;
// compare two prerelease tag values
string[] curParts = this.PrereleaseTag.Split('.', '-');
string[] otherParts = otherTag.Split('.', '-');
string[] curParts = this.PrereleaseTag?.Split('.', '-') ?? Array.Empty<string>();
string[] otherParts = otherTag?.Split('.', '-') ?? Array.Empty<string>();
int length = Math.Max(curParts.Length, otherParts.Length);
for (int i = 0; i < length; i++)
{

View File

@ -73,5 +73,6 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=tilesheets/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=tilesheet_0027s/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=unloadable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=versioning/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=virally/@EntryIndexedValue">True</s:Boolean>
</wpf:ResourceDictionary>

View File

@ -1,6 +1,5 @@
#nullable disable
using System;
using System.Diagnostics.CodeAnalysis;
using Newtonsoft.Json;
namespace StardewModdingAPI
@ -28,10 +27,10 @@ namespace StardewModdingAPI
public int PatchVersion => this.Version.PatchVersion;
/// <inheritdoc />
public string PrereleaseTag => this.Version.PrereleaseTag;
public string? PrereleaseTag => this.Version.PrereleaseTag;
/// <inheritdoc />
public string BuildMetadata => this.Version.BuildMetadata;
public string? BuildMetadata => this.Version.BuildMetadata;
/*********
@ -43,7 +42,7 @@ namespace StardewModdingAPI
/// <param name="patchVersion">The patch version for backwards-compatible bug fixes.</param>
/// <param name="prereleaseTag">An optional prerelease tag.</param>
/// <param name="buildMetadata">Optional build metadata. This is ignored when determining version precedence.</param>
public SemanticVersion(int majorVersion, int minorVersion, int patchVersion, string prereleaseTag = null, string buildMetadata = null)
public SemanticVersion(int majorVersion, int minorVersion, int patchVersion, string? prereleaseTag = null, string? buildMetadata = null)
: this(majorVersion, minorVersion, patchVersion, 0, prereleaseTag, buildMetadata) { }
/// <summary>Construct an instance.</summary>
@ -54,7 +53,7 @@ namespace StardewModdingAPI
/// <param name="platformRelease">The platform-specific version (if applicable).</param>
/// <param name="buildMetadata">Optional build metadata. This is ignored when determining version precedence.</param>
[JsonConstructor]
internal SemanticVersion(int majorVersion, int minorVersion, int patchVersion, int platformRelease, string prereleaseTag = null, string buildMetadata = null)
internal SemanticVersion(int majorVersion, int minorVersion, int patchVersion, int platformRelease, string? prereleaseTag = null, string? buildMetadata = null)
: this(new Toolkit.SemanticVersion(majorVersion, minorVersion, patchVersion, platformRelease, prereleaseTag, buildMetadata)) { }
/// <summary>Construct an instance.</summary>
@ -93,49 +92,49 @@ namespace StardewModdingAPI
/// <inheritdoc />
/// <remarks>The implementation is defined by Semantic Version 2.0 (https://semver.org/).</remarks>
public int CompareTo(ISemanticVersion other)
public int CompareTo(ISemanticVersion? other)
{
return this.Version.CompareTo(other);
}
/// <inheritdoc />
public bool IsOlderThan(ISemanticVersion other)
public bool IsOlderThan(ISemanticVersion? other)
{
return this.Version.IsOlderThan(other);
}
/// <inheritdoc />
public bool IsOlderThan(string other)
public bool IsOlderThan(string? other)
{
return this.Version.IsOlderThan(other);
}
/// <inheritdoc />
public bool IsNewerThan(ISemanticVersion other)
public bool IsNewerThan(ISemanticVersion? other)
{
return this.Version.IsNewerThan(other);
}
/// <inheritdoc />
public bool IsNewerThan(string other)
public bool IsNewerThan(string? other)
{
return this.Version.IsNewerThan(other);
}
/// <inheritdoc />
public bool IsBetween(ISemanticVersion min, ISemanticVersion max)
public bool IsBetween(ISemanticVersion? min, ISemanticVersion? max)
{
return this.Version.IsBetween(min, max);
}
/// <inheritdoc />
public bool IsBetween(string min, string max)
public bool IsBetween(string? min, string? max)
{
return this.Version.IsBetween(min, max);
}
/// <inheritdoc />
public bool Equals(ISemanticVersion other)
public bool Equals(ISemanticVersion? other)
{
return other != null && this.CompareTo(other) == 0;
}
@ -156,9 +155,9 @@ namespace StardewModdingAPI
/// <param name="version">The version string.</param>
/// <param name="parsed">The parsed representation.</param>
/// <returns>Returns whether parsing the version succeeded.</returns>
public static bool TryParse(string version, out ISemanticVersion parsed)
public static bool TryParse(string version, [NotNullWhen(true)] out ISemanticVersion? parsed)
{
if (Toolkit.SemanticVersion.TryParse(version, out ISemanticVersion versionImpl))
if (Toolkit.SemanticVersion.TryParse(version, out ISemanticVersion? versionImpl))
{
parsed = new SemanticVersion(versionImpl);
return true;