diff options
author | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2022-04-07 02:33:23 -0400 |
---|---|---|
committer | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2022-04-07 02:33:23 -0400 |
commit | d706a25053cdc5d9f1ccc2c09dc3913f835c3f78 (patch) | |
tree | bb628c31b328e46d8aa7ae4c521f5f76bdfcbd38 | |
parent | 6b05296e71c32abd158f354eeeaf1e135e72e6e2 (diff) | |
download | SMAPI-d706a25053cdc5d9f1ccc2c09dc3913f835c3f78.tar.gz SMAPI-d706a25053cdc5d9f1ccc2c09dc3913f835c3f78.tar.bz2 SMAPI-d706a25053cdc5d9f1ccc2c09dc3913f835c3f78.zip |
enable nullable annotations for most of the SMAPI toolkit (#837)
27 files changed, 126 insertions, 148 deletions
diff --git a/src/SMAPI.Toolkit/Framework/Constants.cs b/src/SMAPI.Toolkit/Framework/Constants.cs index c3a787c7..55f26582 100644 --- a/src/SMAPI.Toolkit/Framework/Constants.cs +++ b/src/SMAPI.Toolkit/Framework/Constants.cs @@ -1,5 +1,3 @@ -#nullable disable - namespace StardewModdingAPI.Toolkit.Framework { /// <summary>Contains the SMAPI installer's constants and assumptions.</summary> diff --git a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs index ac6fe411..4f872f1c 100644 --- a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs +++ b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using System.IO; @@ -41,7 +39,7 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning IEnumerable<string> paths = this .GetCustomInstallPaths() .Concat(this.GetDefaultInstallPaths()) - .Select(PathUtilities.NormalizePath) + .Select(path => PathUtilities.NormalizePath(path)) .Distinct(StringComparer.OrdinalIgnoreCase); // yield valid folders @@ -80,10 +78,12 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning return GameFolderType.NoGameFound; // get assembly version - Version version; + Version? version; try { version = AssemblyName.GetAssemblyName(executable.FullName).Version; + if (version == null) + return GameFolderType.InvalidUnknown; } catch { @@ -123,7 +123,7 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning case Platform.Linux: case Platform.Mac: { - string home = Environment.GetEnvironmentVariable("HOME"); + string home = Environment.GetEnvironmentVariable("HOME")!; // Linux yield return $"{home}/GOG Games/Stardew Valley/game"; @@ -148,13 +148,13 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning }; foreach (var pair in registryKeys) { - string path = this.GetLocalMachineRegistryValue(pair.Key, pair.Value); + string? path = this.GetLocalMachineRegistryValue(pair.Key, pair.Value); if (!string.IsNullOrWhiteSpace(path)) yield return path; } // via Steam library path - string steamPath = this.GetCurrentUserRegistryValue(@"Software\Valve\Steam", "SteamPath"); + string? steamPath = this.GetCurrentUserRegistryValue(@"Software\Valve\Steam", "SteamPath"); if (steamPath != null) yield return Path.Combine(steamPath.Replace('/', '\\'), @"steamapps\common\Stardew Valley"); #endif @@ -188,7 +188,7 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning private IEnumerable<string> GetCustomInstallPaths() { // get home path - string homePath = Environment.GetEnvironmentVariable(this.Platform == Platform.Windows ? "USERPROFILE" : "HOME"); + string homePath = Environment.GetEnvironmentVariable(this.Platform == Platform.Windows ? "USERPROFILE" : "HOME")!; if (string.IsNullOrWhiteSpace(homePath)) yield break; @@ -210,7 +210,7 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning } // get install path - XElement element = root.XPathSelectElement("//*[local-name() = 'GamePath']"); // can't use '//GamePath' due to the default namespace + XElement? element = root.XPathSelectElement("//*[local-name() = 'GamePath']"); // can't use '//GamePath' due to the default namespace if (!string.IsNullOrWhiteSpace(element?.Value)) yield return element.Value.Trim(); } @@ -219,27 +219,27 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning /// <summary>Get the value of a key in the Windows HKLM registry.</summary> /// <param name="key">The full path of the registry key relative to HKLM.</param> /// <param name="name">The name of the value.</param> - private string GetLocalMachineRegistryValue(string key, string name) + private string? GetLocalMachineRegistryValue(string key, string name) { RegistryKey localMachine = Environment.Is64BitOperatingSystem ? RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64) : Registry.LocalMachine; - RegistryKey openKey = localMachine.OpenSubKey(key); + RegistryKey? openKey = localMachine.OpenSubKey(key); if (openKey == null) return null; using (openKey) - return (string)openKey.GetValue(name); + return (string?)openKey.GetValue(name); } /// <summary>Get the value of a key in the Windows HKCU registry.</summary> /// <param name="key">The full path of the registry key relative to HKCU.</param> /// <param name="name">The name of the value.</param> - private string GetCurrentUserRegistryValue(string key, string name) + private string? GetCurrentUserRegistryValue(string key, string name) { - RegistryKey currentuser = Environment.Is64BitOperatingSystem ? RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64) : Registry.CurrentUser; - RegistryKey openKey = currentuser.OpenSubKey(key); + RegistryKey currentUser = Environment.Is64BitOperatingSystem ? RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64) : Registry.CurrentUser; + RegistryKey? openKey = currentUser.OpenSubKey(key); if (openKey == null) return null; using (openKey) - return (string)openKey.GetValue(name); + return (string?)openKey.GetValue(name); } #endif } diff --git a/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs b/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs index 9998edec..6978567e 100644 --- a/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs +++ b/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -59,11 +57,13 @@ namespace StardewModdingAPI.Toolkit.Framework #if SMAPI_FOR_WINDOWS try { - return new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem") + string? result = new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem") .Get() .Cast<ManagementObject>() .Select(entry => entry.GetPropertyValue("Caption").ToString()) .FirstOrDefault(); + + return result ?? "Windows"; } catch { } #endif @@ -137,7 +137,7 @@ namespace StardewModdingAPI.Toolkit.Framework buffer = Marshal.AllocHGlobal(8192); if (LowLevelEnvironmentUtility.uname(buffer) == 0) { - string os = Marshal.PtrToStringAnsi(buffer); + string? os = Marshal.PtrToStringAnsi(buffer); return os == "Darwin"; } return false; diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs index 81d72c0b..da2a3c85 100644 --- a/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs +++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs @@ -1,5 +1,3 @@ -#nullable disable - using System.Collections.Generic; using System.IO; using System.Linq; @@ -24,13 +22,13 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning public ModType Type { get; } /// <summary>The mod manifest.</summary> - public Manifest Manifest { get; } + public Manifest? Manifest { get; } /// <summary>The error which occurred parsing the manifest, if any.</summary> public ModParseError ManifestParseError { get; set; } /// <summary>A human-readable message for the <see cref="ManifestParseError"/>, if any.</summary> - public string ManifestParseErrorText { get; set; } + public string? ManifestParseErrorText { get; set; } /********* @@ -51,7 +49,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning /// <param name="manifest">The mod manifest.</param> /// <param name="manifestParseError">The error which occurred parsing the manifest, if any.</param> /// <param name="manifestParseErrorText">A human-readable message for the <paramref name="manifestParseError"/>, if any.</param> - public ModFolder(DirectoryInfo root, DirectoryInfo directory, ModType type, Manifest manifest, ModParseError manifestParseError, string manifestParseErrorText) + public ModFolder(DirectoryInfo root, DirectoryInfo directory, ModType type, Manifest? manifest, ModParseError manifestParseError, string? manifestParseErrorText) { // save info this.Directory = directory; @@ -61,9 +59,9 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning this.ManifestParseErrorText = manifestParseErrorText; // set display name - this.DisplayName = manifest?.Name; - if (string.IsNullOrWhiteSpace(this.DisplayName)) - this.DisplayName = PathUtilities.GetRelativePath(root.FullName, directory.FullName); + this.DisplayName = !string.IsNullOrWhiteSpace(manifest?.Name) + ? manifest.Name + : PathUtilities.GetRelativePath(root.FullName, directory.FullName); } /// <summary>Get the update keys for a mod.</summary> diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs index 621f1e28..2af30092 100644 --- a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs +++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using System.IO; @@ -117,7 +115,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning public ModFolder ReadFolder(DirectoryInfo root, DirectoryInfo searchFolder) { // find manifest.json - FileInfo manifestFile = this.FindManifest(searchFolder); + FileInfo? manifestFile = this.FindManifest(searchFolder); // set appropriate invalid-mod error if (manifestFile == null) @@ -147,13 +145,13 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning } // read mod info - Manifest manifest = null; + Manifest? manifest = null; ModParseError error = ModParseError.None; - string errorText = null; + string? errorText = null; { try { - if (!this.JsonHelper.ReadJsonFileIfExists<Manifest>(manifestFile.FullName, out manifest) || manifest == null) + if (!this.JsonHelper.ReadJsonFileIfExists<Manifest>(manifestFile.FullName, out manifest)) { error = ModParseError.ManifestInvalid; errorText = "its manifest is invalid."; @@ -186,7 +184,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning } // build result - return new ModFolder(root, manifestFile.Directory, type, manifest, error, errorText); + return new ModFolder(root, manifestFile.Directory!, type, manifest, error, errorText); } @@ -249,7 +247,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning /// <summary>Find the manifest for a mod folder.</summary> /// <param name="folder">The folder to search.</param> - private FileInfo FindManifest(DirectoryInfo folder) + private FileInfo? FindManifest(DirectoryInfo folder) { while (true) { diff --git a/src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs b/src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs index 57eea2f7..836b1134 100644 --- a/src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs +++ b/src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs @@ -1,4 +1,4 @@ -#nullable disable +using System.Diagnostics.CodeAnalysis; namespace StardewModdingAPI.Toolkit.Framework { @@ -18,7 +18,7 @@ namespace StardewModdingAPI.Toolkit.Framework /// <param name="prereleaseTag">An optional prerelease tag.</param> /// <param name="buildMetadata">Optional build metadata. This is ignored when determining version precedence.</param> /// <returns>Returns whether the version was successfully parsed.</returns> - public static bool TryParse(string versionStr, bool allowNonStandard, out int major, out int minor, out int patch, out int platformRelease, out string prereleaseTag, out string buildMetadata) + public static bool TryParse(string? versionStr, bool allowNonStandard, out int major, out int minor, out int patch, out int platformRelease, out string? prereleaseTag, out string? buildMetadata) { // init major = 0; @@ -105,7 +105,7 @@ namespace StardewModdingAPI.Toolkit.Framework /// <param name="raw">The raw characters to parse.</param> /// <param name="index">The index of the next character to read.</param> /// <param name="tag">The parsed tag.</param> - private static bool TryParseTag(char[] raw, ref int index, out string tag) + private static bool TryParseTag(char[] raw, ref int index, [NotNullWhen(true)] out string? tag) { // read tag length int length = 0; diff --git a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs index ec94ed51..d40d8f2b 100644 --- a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs +++ b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs @@ -1,6 +1,5 @@ -#nullable disable - using System; +using System.Diagnostics.CodeAnalysis; namespace StardewModdingAPI.Toolkit.Framework.UpdateData { @@ -17,10 +16,11 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData public ModSiteKey Site { get; } /// <summary>The mod ID within the repository.</summary> - public string ID { get; } + [MemberNotNullWhen(true, nameof(LooksValid))] + public string? ID { get; } /// <summary>If specified, a substring in download names/descriptions to match.</summary> - public string Subkey { get; } + public string? Subkey { get; } /// <summary>Whether the update key seems to be valid.</summary> public bool LooksValid { get; } @@ -34,9 +34,9 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData /// <param name="site">The mod site containing the mod.</param> /// <param name="id">The mod ID within the site.</param> /// <param name="subkey">If specified, a substring in download names/descriptions to match.</param> - public UpdateKey(string rawText, ModSiteKey site, string id, string subkey) + public UpdateKey(string? rawText, ModSiteKey site, string? id, string? subkey) { - this.RawText = rawText?.Trim(); + this.RawText = rawText?.Trim() ?? string.Empty; this.Site = site; this.ID = id?.Trim(); this.Subkey = subkey?.Trim(); @@ -49,19 +49,19 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData /// <param name="site">The mod site containing the mod.</param> /// <param name="id">The mod ID within the site.</param> /// <param name="subkey">If specified, a substring in download names/descriptions to match.</param> - public UpdateKey(ModSiteKey site, string id, string subkey) + public UpdateKey(ModSiteKey site, string? id, string? subkey) : this(UpdateKey.GetString(site, id, subkey), site, id, subkey) { } /// <summary>Parse a raw update key.</summary> /// <param name="raw">The raw update key to parse.</param> - public static UpdateKey Parse(string raw) + public static UpdateKey Parse(string? raw) { // extract site + ID - string rawSite; - string id; + string? rawSite; + string? id; { - string[] parts = raw?.Trim().Split(':'); - if (parts == null || parts.Length != 2) + string[]? parts = raw?.Trim().Split(':'); + if (parts?.Length != 2) return new UpdateKey(raw, ModSiteKey.Unknown, null, null); rawSite = parts[0].Trim(); @@ -71,7 +71,7 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData id = null; // extract subkey - string subkey = null; + string? subkey = null; if (id != null) { string[] parts = id.Split('@'); @@ -111,7 +111,7 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData /// <summary>Indicates whether the current object is equal to another object of the same type.</summary> /// <param name="other">An object to compare with this object.</param> - public bool Equals(UpdateKey other) + public bool Equals(UpdateKey? other) { if (!this.LooksValid) { @@ -129,7 +129,7 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData /// <summary>Determines whether the specified object is equal to the current object.</summary> /// <param name="obj">The object to compare with the current object.</param> - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is UpdateKey other && this.Equals(other); } @@ -145,7 +145,7 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData /// <param name="site">The mod site containing the mod.</param> /// <param name="id">The mod ID within the repository.</param> /// <param name="subkey">If specified, a substring in download names/descriptions to match.</param> - public static string GetString(ModSiteKey site, string id, string subkey = null) + public static string GetString(ModSiteKey site, string? id, string? subkey = null) { return $"{site}:{id}{subkey}".Trim(); } diff --git a/src/SMAPI.Toolkit/ModToolkit.cs b/src/SMAPI.Toolkit/ModToolkit.cs index 9ae8cd1c..51f6fa24 100644 --- a/src/SMAPI.Toolkit/ModToolkit.cs +++ b/src/SMAPI.Toolkit/ModToolkit.cs @@ -1,5 +1,3 @@ -#nullable disable - using System.Collections.Generic; using System.IO; using System.Linq; @@ -24,7 +22,7 @@ namespace StardewModdingAPI.Toolkit private readonly string UserAgent; /// <summary>Maps vendor keys (like <c>Nexus</c>) to their mod URL template (where <c>{0}</c> is the mod ID). This doesn't affect update checks, which defer to the remote web API.</summary> - private readonly IDictionary<ModSiteKey, string> VendorModUrls = new Dictionary<ModSiteKey, string>() + private readonly Dictionary<ModSiteKey, string> VendorModUrls = new() { [ModSiteKey.Chucklefish] = "https://community.playstarbound.com/resources/{0}", [ModSiteKey.GitHub] = "https://github.com/{0}/releases", @@ -45,7 +43,7 @@ namespace StardewModdingAPI.Toolkit /// <summary>Construct an instance.</summary> public ModToolkit() { - ISemanticVersion version = new SemanticVersion(this.GetType().Assembly.GetName().Version); + ISemanticVersion version = new SemanticVersion(this.GetType().Assembly.GetName().Version!); this.UserAgent = $"SMAPI Mod Handler Toolkit/{version}"; } @@ -59,7 +57,7 @@ namespace StardewModdingAPI.Toolkit /// <summary>Extract mod metadata from the wiki compatibility list.</summary> public async Task<WikiModList> GetWikiCompatibilityListAsync() { - var client = new WikiClient(this.UserAgent); + WikiClient client = new(this.UserAgent); return await client.FetchModsAsync(); } @@ -89,13 +87,13 @@ namespace StardewModdingAPI.Toolkit /// <summary>Get an update URL for an update key (if valid).</summary> /// <param name="updateKey">The update key.</param> - public string GetUpdateUrl(string updateKey) + public string? GetUpdateUrl(string updateKey) { UpdateKey parsed = UpdateKey.Parse(updateKey); if (!parsed.LooksValid) return null; - if (this.VendorModUrls.TryGetValue(parsed.Site, out string urlTemplate)) + if (this.VendorModUrls.TryGetValue(parsed.Site, out string? urlTemplate)) return string.Format(urlTemplate, parsed.ID); return null; diff --git a/src/SMAPI.Toolkit/SemanticVersion.cs b/src/SMAPI.Toolkit/SemanticVersion.cs index ca9d15f5..cbdd7a85 100644 --- a/src/SMAPI.Toolkit/SemanticVersion.cs +++ b/src/SMAPI.Toolkit/SemanticVersion.cs @@ -91,7 +91,7 @@ namespace StardewModdingAPI.Toolkit { if (version == null) throw new ArgumentNullException(nameof(version), "The input version string can't be null."); - if (!SemanticVersionReader.TryParse(version, allowNonStandard, out int major, out int minor, out int patch, out int platformRelease, out string prereleaseTag, out string buildMetadata) || (!allowNonStandard && platformRelease != 0)) + if (!SemanticVersionReader.TryParse(version, allowNonStandard, out int major, out int minor, out int patch, out int platformRelease, out string? prereleaseTag, out string? buildMetadata) || (!allowNonStandard && platformRelease != 0)) throw new FormatException($"The input '{version}' isn't a valid semantic version."); this.MajorVersion = major; diff --git a/src/SMAPI.Toolkit/SemanticVersionComparer.cs b/src/SMAPI.Toolkit/SemanticVersionComparer.cs index a0472f62..85c974bd 100644 --- a/src/SMAPI.Toolkit/SemanticVersionComparer.cs +++ b/src/SMAPI.Toolkit/SemanticVersionComparer.cs @@ -1,5 +1,3 @@ -#nullable disable - using System.Collections.Generic; namespace StardewModdingAPI.Toolkit @@ -18,7 +16,7 @@ namespace StardewModdingAPI.Toolkit ** Public methods *********/ /// <inheritdoc /> - public int Compare(ISemanticVersion x, ISemanticVersion y) + public int Compare(ISemanticVersion? x, ISemanticVersion? y) { if (object.ReferenceEquals(x, y)) return 0; diff --git a/src/SMAPI.Toolkit/Serialization/Converters/ManifestContentPackForConverter.cs b/src/SMAPI.Toolkit/Serialization/Converters/ManifestContentPackForConverter.cs index d2dc0d22..faaeedea 100644 --- a/src/SMAPI.Toolkit/Serialization/Converters/ManifestContentPackForConverter.cs +++ b/src/SMAPI.Toolkit/Serialization/Converters/ManifestContentPackForConverter.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using Newtonsoft.Json; using StardewModdingAPI.Toolkit.Serialization.Models; @@ -35,7 +33,7 @@ namespace StardewModdingAPI.Toolkit.Serialization.Converters /// <param name="objectType">The object type.</param> /// <param name="existingValue">The object being read.</param> /// <param name="serializer">The calling serializer.</param> - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { return serializer.Deserialize<ManifestContentPackFor>(reader); } @@ -44,7 +42,7 @@ namespace StardewModdingAPI.Toolkit.Serialization.Converters /// <param name="writer">The JSON writer.</param> /// <param name="value">The value.</param> /// <param name="serializer">The calling serializer.</param> - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { throw new InvalidOperationException("This converter does not write JSON."); } diff --git a/src/SMAPI.Toolkit/Serialization/Converters/ManifestDependencyArrayConverter.cs b/src/SMAPI.Toolkit/Serialization/Converters/ManifestDependencyArrayConverter.cs index 2c3c2ee7..c499a2c6 100644 --- a/src/SMAPI.Toolkit/Serialization/Converters/ManifestDependencyArrayConverter.cs +++ b/src/SMAPI.Toolkit/Serialization/Converters/ManifestDependencyArrayConverter.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using Newtonsoft.Json; @@ -37,13 +35,13 @@ namespace StardewModdingAPI.Toolkit.Serialization.Converters /// <param name="objectType">The object type.</param> /// <param name="existingValue">The object being read.</param> /// <param name="serializer">The calling serializer.</param> - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { List<ManifestDependency> result = new List<ManifestDependency>(); foreach (JObject obj in JArray.Load(reader).Children<JObject>()) { - string uniqueID = obj.ValueIgnoreCase<string>(nameof(ManifestDependency.UniqueID)); - string minVersion = obj.ValueIgnoreCase<string>(nameof(ManifestDependency.MinimumVersion)); + string uniqueID = obj.ValueIgnoreCase<string>(nameof(ManifestDependency.UniqueID))!; // will be validated separately if null + string? minVersion = obj.ValueIgnoreCase<string>(nameof(ManifestDependency.MinimumVersion)); bool required = obj.ValueIgnoreCase<bool?>(nameof(ManifestDependency.IsRequired)) ?? true; result.Add(new ManifestDependency(uniqueID, minVersion, required)); } @@ -54,7 +52,7 @@ namespace StardewModdingAPI.Toolkit.Serialization.Converters /// <param name="writer">The JSON writer.</param> /// <param name="value">The value.</param> /// <param name="serializer">The calling serializer.</param> - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { throw new InvalidOperationException("This converter does not write JSON."); } diff --git a/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs b/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs index 9205cebe..c32c3185 100644 --- a/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs +++ b/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -41,15 +39,17 @@ namespace StardewModdingAPI.Toolkit.Serialization.Converters /// <param name="objectType">The object type.</param> /// <param name="existingValue">The object being read.</param> /// <param name="serializer">The calling serializer.</param> - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { string path = reader.Path; switch (reader.TokenType) { case JsonToken.StartObject: return this.ReadObject(JObject.Load(reader)); + case JsonToken.String: return this.ReadString(JToken.Load(reader).Value<string>(), path); + default: throw new SParseException($"Can't parse {nameof(ISemanticVersion)} from {reader.TokenType} node (path: {reader.Path})."); } @@ -59,7 +59,7 @@ namespace StardewModdingAPI.Toolkit.Serialization.Converters /// <param name="writer">The JSON writer.</param> /// <param name="value">The value.</param> /// <param name="serializer">The calling serializer.</param> - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { writer.WriteValue(value?.ToString()); } @@ -75,7 +75,7 @@ namespace StardewModdingAPI.Toolkit.Serialization.Converters int major = obj.ValueIgnoreCase<int>(nameof(ISemanticVersion.MajorVersion)); int minor = obj.ValueIgnoreCase<int>(nameof(ISemanticVersion.MinorVersion)); int patch = obj.ValueIgnoreCase<int>(nameof(ISemanticVersion.PatchVersion)); - string prereleaseTag = obj.ValueIgnoreCase<string>(nameof(ISemanticVersion.PrereleaseTag)); + string? prereleaseTag = obj.ValueIgnoreCase<string>(nameof(ISemanticVersion.PrereleaseTag)); return new SemanticVersion(major, minor, patch, prereleaseTag: prereleaseTag); } @@ -83,11 +83,11 @@ namespace StardewModdingAPI.Toolkit.Serialization.Converters /// <summary>Read a JSON string.</summary> /// <param name="str">The JSON string value.</param> /// <param name="path">The path to the current JSON node.</param> - private ISemanticVersion ReadString(string str, string path) + private ISemanticVersion? ReadString(string str, string path) { if (string.IsNullOrWhiteSpace(str)) return null; - if (!SemanticVersion.TryParse(str, allowNonStandard: this.AllowNonStandard, out ISemanticVersion version)) + if (!SemanticVersion.TryParse(str, allowNonStandard: this.AllowNonStandard, out ISemanticVersion? version)) throw new SParseException($"Can't parse semantic version from invalid value '{str}', should be formatted like 1.2, 1.2.30, or 1.2.30-beta (path: {path})."); return version; } diff --git a/src/SMAPI.Toolkit/Serialization/Converters/SimpleReadOnlyConverter.cs b/src/SMAPI.Toolkit/Serialization/Converters/SimpleReadOnlyConverter.cs index c923350c..1c59f5e7 100644 --- a/src/SMAPI.Toolkit/Serialization/Converters/SimpleReadOnlyConverter.cs +++ b/src/SMAPI.Toolkit/Serialization/Converters/SimpleReadOnlyConverter.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -27,21 +25,12 @@ namespace StardewModdingAPI.Toolkit.Serialization.Converters return objectType == typeof(T) || Nullable.GetUnderlyingType(objectType) == typeof(T); } - /// <summary>Writes the JSON representation of the object.</summary> - /// <param name="writer">The JSON writer.</param> - /// <param name="value">The value.</param> - /// <param name="serializer">The calling serializer.</param> - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - throw new InvalidOperationException("This converter does not write JSON."); - } - /// <summary>Reads the JSON representation of the object.</summary> /// <param name="reader">The JSON reader.</param> /// <param name="objectType">The object type.</param> /// <param name="existingValue">The object being read.</param> /// <param name="serializer">The calling serializer.</param> - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { string path = reader.Path; switch (reader.TokenType) @@ -60,6 +49,15 @@ namespace StardewModdingAPI.Toolkit.Serialization.Converters } } + /// <summary>Writes the JSON representation of the object.</summary> + /// <param name="writer">The JSON writer.</param> + /// <param name="value">The value.</param> + /// <param name="serializer">The calling serializer.</param> + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + throw new InvalidOperationException("This converter does not write JSON."); + } + /********* ** Protected methods diff --git a/src/SMAPI.Toolkit/Serialization/InternalExtensions.cs b/src/SMAPI.Toolkit/Serialization/InternalExtensions.cs index 1fce5f9d..78297035 100644 --- a/src/SMAPI.Toolkit/Serialization/InternalExtensions.cs +++ b/src/SMAPI.Toolkit/Serialization/InternalExtensions.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using Newtonsoft.Json.Linq; @@ -12,12 +10,12 @@ namespace StardewModdingAPI.Toolkit.Serialization /// <typeparam name="T">The value type.</typeparam> /// <param name="obj">The JSON object to search.</param> /// <param name="fieldName">The field name.</param> - public static T ValueIgnoreCase<T>(this JObject obj, string fieldName) + public static T? ValueIgnoreCase<T>(this JObject obj, string fieldName) { - JToken token = obj.GetValue(fieldName, StringComparison.OrdinalIgnoreCase); + JToken? token = obj.GetValue(fieldName, StringComparison.OrdinalIgnoreCase); return token != null ? token.Value<T>() - : default(T); + : default; } } } diff --git a/src/SMAPI.Toolkit/Serialization/JsonHelper.cs b/src/SMAPI.Toolkit/Serialization/JsonHelper.cs index 9700e712..7d1804e5 100644 --- a/src/SMAPI.Toolkit/Serialization/JsonHelper.cs +++ b/src/SMAPI.Toolkit/Serialization/JsonHelper.cs @@ -1,7 +1,6 @@ -#nullable disable - using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -38,7 +37,7 @@ namespace StardewModdingAPI.Toolkit.Serialization /// <returns>Returns false if the file doesn't exist, else true.</returns> /// <exception cref="ArgumentException">The given <paramref name="fullPath"/> is empty or invalid.</exception> /// <exception cref="JsonReaderException">The file contains invalid JSON.</exception> - public bool ReadJsonFileIfExists<TModel>(string fullPath, out TModel result) + public bool ReadJsonFileIfExists<TModel>(string fullPath, [NotNullWhen(true)] out TModel? result) { // validate if (string.IsNullOrWhiteSpace(fullPath)) @@ -52,7 +51,7 @@ namespace StardewModdingAPI.Toolkit.Serialization } catch (Exception ex) when (ex is DirectoryNotFoundException or FileNotFoundException) { - result = default(TModel); + result = default; return false; } @@ -60,7 +59,7 @@ namespace StardewModdingAPI.Toolkit.Serialization try { result = this.Deserialize<TModel>(json); - return true; + return result != null; } catch (Exception ex) { @@ -90,7 +89,7 @@ namespace StardewModdingAPI.Toolkit.Serialization throw new ArgumentException("The file path is empty or invalid.", nameof(fullPath)); // create directory if needed - string dir = Path.GetDirectoryName(fullPath); + string dir = Path.GetDirectoryName(fullPath)!; if (dir == null) throw new ArgumentException("The file path is invalid.", nameof(fullPath)); if (!Directory.Exists(dir)) @@ -108,7 +107,8 @@ namespace StardewModdingAPI.Toolkit.Serialization { try { - return JsonConvert.DeserializeObject<TModel>(json, this.JsonSettings); + return JsonConvert.DeserializeObject<TModel>(json, this.JsonSettings) + ?? throw new InvalidOperationException($"Couldn't deserialize model type '{typeof(TModel)}' from empty or null JSON."); } catch (JsonReaderException) { @@ -117,7 +117,8 @@ namespace StardewModdingAPI.Toolkit.Serialization { try { - return JsonConvert.DeserializeObject<TModel>(json.Replace('“', '"').Replace('”', '"'), this.JsonSettings); + return JsonConvert.DeserializeObject<TModel>(json.Replace('“', '"').Replace('”', '"'), this.JsonSettings) + ?? throw new InvalidOperationException($"Couldn't deserialize model type '{typeof(TModel)}' from empty or null JSON."); } catch { /* rethrow original error */ } } diff --git a/src/SMAPI.Toolkit/Serialization/Models/ManifestDependency.cs b/src/SMAPI.Toolkit/Serialization/Models/ManifestDependency.cs index 64725818..5d1b73b1 100644 --- a/src/SMAPI.Toolkit/Serialization/Models/ManifestDependency.cs +++ b/src/SMAPI.Toolkit/Serialization/Models/ManifestDependency.cs @@ -26,7 +26,7 @@ namespace StardewModdingAPI.Toolkit.Serialization.Models /// <param name="uniqueID">The unique mod ID to require.</param> /// <param name="minimumVersion">The minimum required version (if any).</param> /// <param name="required">Whether the dependency must be installed to use the mod.</param> - public ManifestDependency(string uniqueID, string minimumVersion, bool required = true) + public ManifestDependency(string uniqueID, string? minimumVersion, bool required = true) : this( uniqueID: uniqueID, minimumVersion: !string.IsNullOrWhiteSpace(minimumVersion) diff --git a/src/SMAPI.Toolkit/Serialization/SParseException.cs b/src/SMAPI.Toolkit/Serialization/SParseException.cs index 1581e027..c2b3f68e 100644 --- a/src/SMAPI.Toolkit/Serialization/SParseException.cs +++ b/src/SMAPI.Toolkit/Serialization/SParseException.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; namespace StardewModdingAPI.Toolkit.Serialization @@ -13,7 +11,7 @@ namespace StardewModdingAPI.Toolkit.Serialization /// <summary>Construct an instance.</summary> /// <param name="message">The error message.</param> /// <param name="ex">The underlying exception, if any.</param> - public SParseException(string message, Exception ex = null) + public SParseException(string message, Exception? ex = null) : base(message, ex) { } } } diff --git a/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs b/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs index f14678be..1791c5b3 100644 --- a/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs +++ b/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs @@ -1,7 +1,4 @@ -#nullable disable - using System; -using System.Diagnostics.CodeAnalysis; using StardewModdingAPI.Toolkit.Framework; namespace StardewModdingAPI.Toolkit.Utilities @@ -36,7 +33,6 @@ namespace StardewModdingAPI.Toolkit.Utilities /// <summary>Get the human-readable OS name and version.</summary> /// <param name="platform">The current platform.</param> - [SuppressMessage("ReSharper", "EmptyGeneralCatchClause", Justification = "Error suppressed deliberately to fallback to default behaviour.")] public static string GetFriendlyPlatformName(Platform platform) { return LowLevelEnvironmentUtility.GetFriendlyPlatformName(platform.ToString()); diff --git a/src/SMAPI.Toolkit/Utilities/FileUtilities.cs b/src/SMAPI.Toolkit/Utilities/FileUtilities.cs index ba2d0f47..a6bf5929 100644 --- a/src/SMAPI.Toolkit/Utilities/FileUtilities.cs +++ b/src/SMAPI.Toolkit/Utilities/FileUtilities.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.IO; using System.Security.Cryptography; diff --git a/src/SMAPI.Toolkit/Utilities/PathUtilities.cs b/src/SMAPI.Toolkit/Utilities/PathUtilities.cs index d035d4cd..9b7a78a0 100644 --- a/src/SMAPI.Toolkit/Utilities/PathUtilities.cs +++ b/src/SMAPI.Toolkit/Utilities/PathUtilities.cs @@ -1,6 +1,5 @@ -#nullable disable - using System; +using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.IO; using System.Linq; @@ -38,8 +37,11 @@ namespace StardewModdingAPI.Toolkit.Utilities /// <param name="path">The path to split.</param> /// <param name="limit">The number of segments to match. Any additional segments will be merged into the last returned part.</param> [Pure] - public static string[] GetSegments(string path, int? limit = null) + public static string[] GetSegments(string? path, int? limit = null) { + if (path == null) + return Array.Empty<string>(); + return limit.HasValue ? path.Split(PathUtilities.PossiblePathSeparators, limit.Value, StringSplitOptions.RemoveEmptyEntries) : path.Split(PathUtilities.PossiblePathSeparators, StringSplitOptions.RemoveEmptyEntries); @@ -47,8 +49,14 @@ namespace StardewModdingAPI.Toolkit.Utilities /// <summary>Normalize an asset name to match how MonoGame's content APIs would normalize and cache it.</summary> /// <param name="assetName">The asset name to normalize.</param> - public static string NormalizeAssetName(string assetName) + [Pure] + [return: NotNullIfNotNull("assetName")] + public static string? NormalizeAssetName(string? assetName) { + assetName = assetName?.Trim(); + if (string.IsNullOrEmpty(assetName)) + return assetName; + return string.Join(PathUtilities.PreferredAssetSeparator.ToString(), PathUtilities.GetSegments(assetName)); // based on MonoGame's ContentManager.Load<T> logic } @@ -56,7 +64,8 @@ namespace StardewModdingAPI.Toolkit.Utilities /// <param name="path">The file path to normalize.</param> /// <remarks>This should only be used for file paths. For asset names, use <see cref="NormalizeAssetName"/> instead.</remarks> [Pure] - public static string NormalizePath(string path) + [return: NotNullIfNotNull("path")] + public static string? NormalizePath(string? path) { path = path?.Trim(); if (string.IsNullOrEmpty(path)) @@ -80,7 +89,7 @@ namespace StardewModdingAPI.Toolkit.Utilities } // keep trailing separator - if ((!hasRoot || segments.Any()) && PathUtilities.PossiblePathSeparators.Contains(path[path.Length - 1])) + if ((!hasRoot || segments.Any()) && PathUtilities.PossiblePathSeparators.Contains(path[^1])) newPath += PathUtilities.PreferredPathSeparator; return newPath; @@ -98,7 +107,7 @@ namespace StardewModdingAPI.Toolkit.Utilities /// <summary>Get whether a path is relative and doesn't try to climb out of its containing folder (e.g. doesn't contain <c>../</c>).</summary> /// <param name="path">The path to check.</param> [Pure] - public static bool IsSafeRelativePath(string path) + public static bool IsSafeRelativePath(string? path) { if (string.IsNullOrWhiteSpace(path)) return true; @@ -111,9 +120,11 @@ namespace StardewModdingAPI.Toolkit.Utilities /// <summary>Get whether a string is a valid 'slug', containing only basic characters that are safe in all contexts (e.g. filenames, URLs, etc).</summary> /// <param name="str">The string to check.</param> [Pure] - public static bool IsSlug(string str) + public static bool IsSlug(string? str) { - return !Regex.IsMatch(str, "[^a-z0-9_.-]", RegexOptions.IgnoreCase); + return + string.IsNullOrWhiteSpace(str) + || !Regex.IsMatch(str, "[^a-z0-9_.-]", RegexOptions.IgnoreCase); } } } diff --git a/src/SMAPI/Framework/Serialization/ColorConverter.cs b/src/SMAPI/Framework/Serialization/ColorConverter.cs index 8fb6cd9c..3b3720b5 100644 --- a/src/SMAPI/Framework/Serialization/ColorConverter.cs +++ b/src/SMAPI/Framework/Serialization/ColorConverter.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using Microsoft.Xna.Framework; using Newtonsoft.Json.Linq; diff --git a/src/SMAPI/Framework/Serialization/KeybindConverter.cs b/src/SMAPI/Framework/Serialization/KeybindConverter.cs index 9cf52228..f3bab20d 100644 --- a/src/SMAPI/Framework/Serialization/KeybindConverter.cs +++ b/src/SMAPI/Framework/Serialization/KeybindConverter.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -38,7 +36,7 @@ namespace StardewModdingAPI.Framework.Serialization /// <param name="objectType">The object type.</param> /// <param name="existingValue">The object being read.</param> /// <param name="serializer">The calling serializer.</param> - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { string path = reader.Path; @@ -76,7 +74,7 @@ namespace StardewModdingAPI.Framework.Serialization /// <param name="writer">The JSON writer.</param> /// <param name="value">The value.</param> /// <param name="serializer">The calling serializer.</param> - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { writer.WriteValue(value?.ToString()); } diff --git a/src/SMAPI/Framework/Serialization/PointConverter.cs b/src/SMAPI/Framework/Serialization/PointConverter.cs index b48d757a..21d1f845 100644 --- a/src/SMAPI/Framework/Serialization/PointConverter.cs +++ b/src/SMAPI/Framework/Serialization/PointConverter.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using Microsoft.Xna.Framework; using Newtonsoft.Json.Linq; diff --git a/src/SMAPI/Framework/Serialization/RectangleConverter.cs b/src/SMAPI/Framework/Serialization/RectangleConverter.cs index 7f060e3a..31f3ad77 100644 --- a/src/SMAPI/Framework/Serialization/RectangleConverter.cs +++ b/src/SMAPI/Framework/Serialization/RectangleConverter.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Text.RegularExpressions; using Microsoft.Xna.Framework; diff --git a/src/SMAPI/Framework/Serialization/Vector2Converter.cs b/src/SMAPI/Framework/Serialization/Vector2Converter.cs index bcd483d3..589febf8 100644 --- a/src/SMAPI/Framework/Serialization/Vector2Converter.cs +++ b/src/SMAPI/Framework/Serialization/Vector2Converter.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using Microsoft.Xna.Framework; using Newtonsoft.Json.Linq; diff --git a/src/SMAPI/Utilities/PathUtilities.cs b/src/SMAPI/Utilities/PathUtilities.cs index e8ab9645..4350f441 100644 --- a/src/SMAPI/Utilities/PathUtilities.cs +++ b/src/SMAPI/Utilities/PathUtilities.cs @@ -1,5 +1,4 @@ -#nullable disable - +using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using ToolkitPathUtilities = StardewModdingAPI.Toolkit.Utilities.PathUtilities; @@ -22,14 +21,16 @@ namespace StardewModdingAPI.Utilities /// <param name="path">The path to split.</param> /// <param name="limit">The number of segments to match. Any additional segments will be merged into the last returned part.</param> [Pure] - public static string[] GetSegments(string path, int? limit = null) + public static string[] GetSegments(string? path, int? limit = null) { return ToolkitPathUtilities.GetSegments(path, limit); } /// <summary>Normalize an asset name to match how MonoGame's content APIs would normalize and cache it.</summary> /// <param name="assetName">The asset name to normalize.</param> - public static string NormalizeAssetName(string assetName) + [Pure] + [return: NotNullIfNotNull("assetName")] + public static string? NormalizeAssetName(string? assetName) { return ToolkitPathUtilities.NormalizeAssetName(assetName); } @@ -38,7 +39,8 @@ namespace StardewModdingAPI.Utilities /// <param name="path">The file path to normalize.</param> /// <remarks>This should only be used for file paths. For asset names, use <see cref="NormalizeAssetName"/> instead.</remarks> [Pure] - public static string NormalizePath(string path) + [return: NotNullIfNotNull("path")] + public static string? NormalizePath(string? path) { return ToolkitPathUtilities.NormalizePath(path); } @@ -46,7 +48,7 @@ namespace StardewModdingAPI.Utilities /// <summary>Get whether a path is relative and doesn't try to climb out of its containing folder (e.g. doesn't contain <c>../</c>).</summary> /// <param name="path">The path to check.</param> [Pure] - public static bool IsSafeRelativePath(string path) + public static bool IsSafeRelativePath(string? path) { return ToolkitPathUtilities.IsSafeRelativePath(path); } @@ -54,7 +56,7 @@ namespace StardewModdingAPI.Utilities /// <summary>Get whether a string is a valid 'slug', containing only basic characters that are safe in all contexts (e.g. filenames, URLs, etc).</summary> /// <param name="str">The string to check.</param> [Pure] - public static bool IsSlug(string str) + public static bool IsSlug(string? str) { return ToolkitPathUtilities.IsSlug(str); } |