diff options
Diffstat (limited to 'src/SMAPI.Toolkit')
40 files changed, 384 insertions, 190 deletions
diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs index 0115fbf3..d5ca2034 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs index 188db31d..9aac7fd3 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs @@ -1,3 +1,5 @@ +#nullable disable + using Newtonsoft.Json; using StardewModdingAPI.Toolkit.Serialization.Converters; diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs index 0fa4a74d..eb54ec78 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Collections.Generic; using System.Linq; diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs index 404d4618..8fe8fa2a 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs index 73698173..393391f7 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs @@ -1,3 +1,5 @@ +#nullable disable + using System.Linq; using StardewModdingAPI.Toolkit.Utilities; diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs index c2d906a0..56acb768 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Collections.Generic; using System.Linq; @@ -62,9 +64,9 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi private TResult Post<TBody, TResult>(string url, TBody content) { // note: avoid HttpClient for macOS compatibility - using WebClient client = new WebClient(); + using WebClient client = new(); - Uri fullUrl = new Uri(this.BaseUrl, url); + Uri fullUrl = new(this.BaseUrl, url); string data = JsonConvert.SerializeObject(content); client.Headers["Content-Type"] = "application/json"; diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/ChangeDescriptor.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/ChangeDescriptor.cs index 2ed255c8..910bf793 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/ChangeDescriptor.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/ChangeDescriptor.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Collections.Generic; using System.Collections.ObjectModel; diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs index 0f5a0ec3..86c3bd75 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -65,7 +67,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki foreach (var entry in this.ParseOverrideEntries(modNodes)) { - if (entry.Ids?.Any() != true || !entry.HasChanges) + if (entry.Ids.Any() != true || !entry.HasChanges) continue; foreach (string id in entry.Ids) @@ -127,7 +129,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki string pullRequestUrl = this.GetAttribute(node, "data-pr"); // parse stable compatibility - WikiCompatibilityInfo compatibility = new WikiCompatibilityInfo + WikiCompatibilityInfo compatibility = new() { Status = this.GetAttributeAsEnum<WikiCompatibilityStatus>(node, "data-status") ?? WikiCompatibilityStatus.Ok, BrokeIn = this.GetAttribute(node, "data-broke-in"), diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs index 204acd2b..30e76d04 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs @@ -1,3 +1,5 @@ +#nullable disable + namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki { /// <summary>Compatibility info for a mod.</summary> diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs index 5cdf489f..2c222b71 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs @@ -1,3 +1,5 @@ +#nullable disable + namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki { /// <summary>The compatibility status for a mod.</summary> diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiDataOverrideEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiDataOverrideEntry.cs index 03c0d214..a6f5a88f 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiDataOverrideEntry.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiDataOverrideEntry.cs @@ -1,7 +1,5 @@ using System; -#nullable enable - namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki { /// <summary>The data overrides to apply to matching mods.</summary> diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs index 4e0104da..91943ff9 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs @@ -1,3 +1,5 @@ +#nullable disable + namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki { /// <summary>A mod entry in the wiki list.</summary> diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs index 0d614f28..1787197a 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs @@ -1,3 +1,5 @@ +#nullable disable + namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki { /// <summary>Metadata from the wiki's mod compatibility list.</summary> diff --git a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs index 8d4198de..4f872f1c 100644 --- a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs +++ b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs @@ -39,13 +39,13 @@ 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 foreach (string path in paths) { - DirectoryInfo folder = new DirectoryInfo(path); + DirectoryInfo folder = new(path); if (this.LooksLikeGameFolder(folder)) yield return folder; } @@ -78,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 { @@ -121,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"; @@ -146,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 @@ -186,12 +188,12 @@ 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; // get targets file - FileInfo file = new FileInfo(Path.Combine(homePath, "stardewvalley.targets")); + FileInfo file = new(Path.Combine(homePath, "stardewvalley.targets")); if (!file.Exists) yield break; @@ -208,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(); } @@ -217,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 8b6eb5fb..6978567e 100644 --- a/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs +++ b/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs @@ -57,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 @@ -98,7 +100,7 @@ namespace StardewModdingAPI.Toolkit.Framework /// </remarks> private static bool IsRunningAndroid() { - using Process process = new Process + using Process process = new() { StartInfo = { @@ -135,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/ModData/MetadataModel.cs b/src/SMAPI.Toolkit/Framework/ModData/MetadataModel.cs index ef6d4dd9..3fa70615 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/MetadataModel.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/MetadataModel.cs @@ -1,3 +1,5 @@ +#nullable disable + using System.Collections.Generic; namespace StardewModdingAPI.Toolkit.Framework.ModData diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataField.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataField.cs index b02be3e4..46cb81e1 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/ModDataField.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataField.cs @@ -1,3 +1,5 @@ +#nullable disable + using System.Linq; namespace StardewModdingAPI.Toolkit.Framework.ModData diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs index 2167d3e5..4d96a555 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Collections.Generic; using System.Linq; diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs index 5dd32acf..4c09e1ba 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Collections.Generic; using System.Linq; @@ -82,7 +84,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData /// <param name="manifest">The manifest to match.</param> public ModDataRecordVersionedFields GetVersionedFields(IManifest manifest) { - ModDataRecordVersionedFields parsed = new ModDataRecordVersionedFields { DisplayName = this.DisplayName, DataRecord = this }; + ModDataRecordVersionedFields parsed = new() { DisplayName = this.DisplayName, DataRecord = this }; foreach (ModDataField field in this.Fields.Where(field => field.IsMatch(manifest))) { switch (field.Key) diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs index 5aaabd51..b599b343 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs @@ -1,3 +1,5 @@ +#nullable disable + namespace StardewModdingAPI.Toolkit.Framework.ModData { /// <summary>The versioned fields from a <see cref="ModDataRecord"/> for a specific manifest.</summary> diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs index 5b7e2a02..a5237334 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Collections.Generic; using System.Linq; @@ -22,7 +24,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData *********/ /// <summary>Construct an empty instance.</summary> public ModDatabase() - : this(Array.Empty<ModDataRecord>(), key => null) { } + : this(Array.Empty<ModDataRecord>(), _ => null) { } /// <summary>Construct an instance.</summary> /// <param name="records">The underlying mod data records indexed by default display name.</param> diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs index 825b98e5..da2a3c85 100644 --- a/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs +++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs @@ -22,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; } /********* @@ -49,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; @@ -59,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 e6105f9c..2af30092 100644 --- a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs +++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs @@ -18,7 +18,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning private readonly JsonHelper JsonHelper; /// <summary>A list of filesystem entry names to ignore when checking whether a folder should be treated as a mod.</summary> - private readonly HashSet<Regex> IgnoreFilesystemNames = new HashSet<Regex> + private readonly HashSet<Regex> IgnoreFilesystemNames = new() { new Regex(@"^__folder_managed_by_vortex$", RegexOptions.Compiled | RegexOptions.IgnoreCase), // Vortex mod manager new Regex(@"(?:^\._|^\.DS_Store$|^__MACOSX$|^mcs$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), // macOS @@ -26,7 +26,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning }; /// <summary>A list of file extensions to ignore when searching for mod files.</summary> - private readonly HashSet<string> IgnoreFileExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase) + private readonly HashSet<string> IgnoreFileExtensions = new(StringComparer.OrdinalIgnoreCase) { // text ".doc", @@ -60,7 +60,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning }; /// <summary>The extensions for packed content files.</summary> - private readonly HashSet<string> StrictXnbModExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase) + private readonly HashSet<string> StrictXnbModExtensions = new(StringComparer.OrdinalIgnoreCase) { ".xgs", ".xnb", @@ -69,7 +69,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning }; /// <summary>The extensions for files which an XNB mod may contain, in addition to <see cref="StrictXnbModExtensions"/>.</summary> - private readonly HashSet<string> PotentialXnbModExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase) + private readonly HashSet<string> PotentialXnbModExtensions = new(StringComparer.OrdinalIgnoreCase) { ".json", ".yaml" @@ -96,7 +96,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning /// <param name="rootPath">The root folder containing mods.</param> public IEnumerable<ModFolder> GetModFolders(string rootPath) { - DirectoryInfo root = new DirectoryInfo(rootPath); + DirectoryInfo root = new(rootPath); return this.GetModFolders(root, root); } @@ -115,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) @@ -137,7 +137,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning return new ModFolder(root, searchFolder, ModType.Xnb, null, ModParseError.XnbMod, "it's not a SMAPI mod (see https://smapi.io/xnb for info)."); // SMAPI installer - if (relevantFiles.Any(p => p.Name == "install on Linux.sh" || p.Name == "install on macOS.command" || p.Name == "install on Windows.bat")) + if (relevantFiles.Any(p => p.Name is "install on Linux.sh" or "install on macOS.command" or "install on Windows.bat")) return new ModFolder(root, searchFolder, ModType.Invalid, null, ModParseError.ManifestMissing, "the SMAPI installer isn't a mod (you can delete this folder after running the installer file)."); // not a mod? @@ -145,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."; @@ -169,14 +169,6 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning } } - // normalize display fields - if (manifest != null) - { - manifest.Name = this.StripNewlines(manifest.Name); - manifest.Description = this.StripNewlines(manifest.Description); - manifest.Author = this.StripNewlines(manifest.Author); - } - // get mod type ModType type; { @@ -192,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); } @@ -255,12 +247,12 @@ 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) { // check for manifest in current folder - FileInfo file = new FileInfo(Path.Combine(folder.FullName, "manifest.json")); + FileInfo file = new(Path.Combine(folder.FullName, "manifest.json")); if (file.Exists) return file; @@ -363,12 +355,5 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning return hasVortexMarker; } - - /// <summary>Strip newlines from a string.</summary> - /// <param name="input">The input to strip.</param> - private string StripNewlines(string input) - { - return input?.Replace("\r", "").Replace("\n", ""); - } } } diff --git a/src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs b/src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs index 489e1c4d..939be771 100644 --- a/src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs +++ b/src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs @@ -1,3 +1,5 @@ +using System.Diagnostics.CodeAnalysis; + namespace StardewModdingAPI.Toolkit.Framework { /// <summary>Reads strings into a semantic version.</summary> @@ -16,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; @@ -103,7 +105,12 @@ 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, +#if NET5_0_OR_GREATER + [NotNullWhen(true)] +#endif + 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 077c0361..4c9ca2ff 100644 --- a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs +++ b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; namespace StardewModdingAPI.Toolkit.Framework.UpdateData { @@ -15,10 +16,13 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData public ModSiteKey Site { get; } /// <summary>The mod ID within the repository.</summary> - public string ID { get; } +#if NET5_0_OR_GREATER + [MemberNotNullWhen(true, nameof(LooksValid))] +#endif + 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; } @@ -32,9 +36,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(); @@ -47,19 +51,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(); @@ -69,7 +73,7 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData id = null; // extract subkey - string subkey = null; + string? subkey = null; if (id != null) { string[] parts = id.Split('@'); @@ -109,7 +113,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) { @@ -127,7 +131,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); } @@ -143,7 +147,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 38a67ae5..51f6fa24 100644 --- a/src/SMAPI.Toolkit/ModToolkit.cs +++ b/src/SMAPI.Toolkit/ModToolkit.cs @@ -22,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", @@ -34,7 +34,7 @@ namespace StardewModdingAPI.Toolkit ** Accessors *********/ /// <summary>Encapsulates SMAPI's JSON parsing.</summary> - public JsonHelper JsonHelper { get; } = new JsonHelper(); + public JsonHelper JsonHelper { get; } = new(); /********* @@ -43,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}"; } @@ -57,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(); } @@ -87,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 2f3e282b..2cb27e11 100644 --- a/src/SMAPI.Toolkit/SemanticVersion.cs +++ b/src/SMAPI.Toolkit/SemanticVersion.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Text.RegularExpressions; using StardewModdingAPI.Toolkit.Framework; @@ -38,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; } /********* @@ -54,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; @@ -90,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; @@ -104,15 +105,15 @@ namespace StardewModdingAPI.Toolkit } /// <inheritdoc /> - public int CompareTo(ISemanticVersion other) + public int CompareTo(ISemanticVersion? other) { - if (other == null) - throw new ArgumentNullException(nameof(other)); - return this.CompareTo(other.MajorVersion, other.MinorVersion, other.PatchVersion, (other as SemanticVersion)?.PlatformRelease ?? 0, other.PrereleaseTag); + return other == null + ? 1 + : this.CompareTo(other.MajorVersion, other.MinorVersion, other.PatchVersion, (other as SemanticVersion)?.PlatformRelease ?? 0, other.PrereleaseTag); } /// <inheritdoc /> - public bool Equals(ISemanticVersion other) + public bool Equals(ISemanticVersion? other) { return other != null && this.CompareTo(other) == 0; } @@ -124,39 +125,54 @@ 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) { - return this.IsOlderThan(new SemanticVersion(other, allowNonStandard: true)); + ISemanticVersion? otherVersion = other != null + ? new SemanticVersion(other, allowNonStandard: true) + : null; + + return this.IsOlderThan(otherVersion); } /// <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) { - return this.IsNewerThan(new SemanticVersion(other, allowNonStandard: true)); + ISemanticVersion? otherVersion = other != null + ? new SemanticVersion(other, allowNonStandard: true) + : null; + + return this.IsNewerThan(otherVersion); } /// <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) { - return this.IsBetween(new SemanticVersion(min, allowNonStandard: true), new SemanticVersion(max, allowNonStandard: true)); + ISemanticVersion? minVersion = min != null + ? new SemanticVersion(min, allowNonStandard: true) + : null; + ISemanticVersion? maxVersion = max != null + ? new SemanticVersion(max, allowNonStandard: true) + : null; + + return this.IsBetween(minVersion, maxVersion); } /// <inheritdoc cref="ISemanticVersion.ToString" /> @@ -182,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); } @@ -192,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); @@ -212,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; @@ -224,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; @@ -253,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++) { diff --git a/src/SMAPI.Toolkit/SemanticVersionComparer.cs b/src/SMAPI.Toolkit/SemanticVersionComparer.cs index 9f6b57a2..85c974bd 100644 --- a/src/SMAPI.Toolkit/SemanticVersionComparer.cs +++ b/src/SMAPI.Toolkit/SemanticVersionComparer.cs @@ -9,14 +9,14 @@ namespace StardewModdingAPI.Toolkit ** Accessors *********/ /// <summary>A singleton instance of the comparer.</summary> - public static SemanticVersionComparer Instance { get; } = new SemanticVersionComparer(); + public static SemanticVersionComparer Instance { get; } = new(); /********* ** 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 5cabe9d8..faaeedea 100644 --- a/src/SMAPI.Toolkit/Serialization/Converters/ManifestContentPackForConverter.cs +++ b/src/SMAPI.Toolkit/Serialization/Converters/ManifestContentPackForConverter.cs @@ -33,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); } @@ -42,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 7b88d6b7..c499a2c6 100644 --- a/src/SMAPI.Toolkit/Serialization/Converters/ManifestDependencyArrayConverter.cs +++ b/src/SMAPI.Toolkit/Serialization/Converters/ManifestDependencyArrayConverter.cs @@ -35,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)); } @@ -52,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 cf69104d..c32c3185 100644 --- a/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs +++ b/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs @@ -39,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})."); } @@ -57,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()); } @@ -73,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); } @@ -81,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 ccc5158b..1c59f5e7 100644 --- a/src/SMAPI.Toolkit/Serialization/Converters/SimpleReadOnlyConverter.cs +++ b/src/SMAPI.Toolkit/Serialization/Converters/SimpleReadOnlyConverter.cs @@ -25,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) @@ -58,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 10f88dde..78297035 100644 --- a/src/SMAPI.Toolkit/Serialization/InternalExtensions.cs +++ b/src/SMAPI.Toolkit/Serialization/InternalExtensions.cs @@ -10,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 00db9903..3c9308f2 100644 --- a/src/SMAPI.Toolkit/Serialization/JsonHelper.cs +++ b/src/SMAPI.Toolkit/Serialization/JsonHelper.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -14,7 +15,7 @@ namespace StardewModdingAPI.Toolkit.Serialization ** Accessors *********/ /// <summary>The JSON settings to use when serializing and deserializing files.</summary> - public JsonSerializerSettings JsonSettings { get; } = new JsonSerializerSettings + public JsonSerializerSettings JsonSettings { get; } = new() { Formatting = Formatting.Indented, ObjectCreationHandling = ObjectCreationHandling.Replace, // avoid issue where default ICollection<T> values are duplicated each time the config is loaded @@ -36,7 +37,12 @@ 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, +#if NET5_0_OR_GREATER + [NotNullWhen(true)] +#endif + out TModel? result + ) { // validate if (string.IsNullOrWhiteSpace(fullPath)) @@ -48,9 +54,9 @@ namespace StardewModdingAPI.Toolkit.Serialization { json = File.ReadAllText(fullPath); } - catch (Exception ex) when (ex is DirectoryNotFoundException || ex is FileNotFoundException) + catch (Exception ex) when (ex is DirectoryNotFoundException or FileNotFoundException) { - result = default(TModel); + result = default; return false; } @@ -58,7 +64,7 @@ namespace StardewModdingAPI.Toolkit.Serialization try { result = this.Deserialize<TModel>(json); - return true; + return result != null; } catch (Exception ex) { @@ -88,7 +94,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)) @@ -106,7 +112,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) { @@ -115,7 +122,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/Manifest.cs b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs index 4ad97b6d..01010602 100644 --- a/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs +++ b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.Runtime.Serialization; +using System.Diagnostics.CodeAnalysis; using Newtonsoft.Json; using StardewModdingAPI.Toolkit.Serialization.Converters; @@ -13,48 +13,45 @@ namespace StardewModdingAPI.Toolkit.Serialization.Models ** Accessors *********/ /// <summary>The mod name.</summary> - public string Name { get; set; } + public string Name { get; } /// <summary>A brief description of the mod.</summary> - public string Description { get; set; } + public string Description { get; } /// <summary>The mod author's name.</summary> - public string Author { get; set; } + public string Author { get; } /// <summary>The mod version.</summary> - public ISemanticVersion Version { get; set; } + public ISemanticVersion Version { get; } /// <summary>The minimum SMAPI version required by this mod, if any.</summary> - public ISemanticVersion MinimumApiVersion { get; set; } + public ISemanticVersion? MinimumApiVersion { get; } /// <summary>The name of the DLL in the directory that has the <c>Entry</c> method. Mutually exclusive with <see cref="ContentPackFor"/>.</summary> - public string EntryDll { get; set; } + public string? EntryDll { get; } /// <summary>The mod which will read this as a content pack. Mutually exclusive with <see cref="Manifest.EntryDll"/>.</summary> [JsonConverter(typeof(ManifestContentPackForConverter))] - public IManifestContentPackFor ContentPackFor { get; set; } + public IManifestContentPackFor? ContentPackFor { get; } /// <summary>The other mods that must be loaded before this mod.</summary> [JsonConverter(typeof(ManifestDependencyArrayConverter))] - public IManifestDependency[] Dependencies { get; set; } + public IManifestDependency[] Dependencies { get; } /// <summary>The namespaced mod IDs to query for updates (like <c>Nexus:541</c>).</summary> - public string[] UpdateKeys { get; set; } + public string[] UpdateKeys { get; private set; } /// <summary>The unique mod ID.</summary> - public string UniqueID { get; set; } + public string UniqueID { get; } /// <summary>Any manifest fields which didn't match a valid field.</summary> [JsonExtensionData] - public IDictionary<string, object> ExtraFields { get; set; } + public IDictionary<string, object> ExtraFields { get; set; } = new Dictionary<string, object>(); /********* ** Public methods *********/ - /// <summary>Construct an instance.</summary> - public Manifest() { } - /// <summary>Construct an instance for a transitional content pack.</summary> /// <param name="uniqueID">The unique mod ID.</param> /// <param name="name">The mod name.</param> @@ -62,24 +59,71 @@ namespace StardewModdingAPI.Toolkit.Serialization.Models /// <param name="description">A brief description of the mod.</param> /// <param name="version">The mod version.</param> /// <param name="contentPackFor">The modID which will read this as a content pack.</param> - public Manifest(string uniqueID, string name, string author, string description, ISemanticVersion version, string contentPackFor = null) + public Manifest(string uniqueID, string name, string author, string description, ISemanticVersion version, string? contentPackFor = null) + : this( + uniqueId: uniqueID, + name: name, + author: author, + description: description, + version: version, + minimumApiVersion: null, + entryDll: null, + contentPackFor: contentPackFor != null + ? new ManifestContentPackFor(contentPackFor, null) + : null, + dependencies: null, + updateKeys: null + ) + { } + + /// <summary>Construct an instance for a transitional content pack.</summary> + /// <param name="uniqueId">The unique mod ID.</param> + /// <param name="name">The mod name.</param> + /// <param name="author">The mod author's name.</param> + /// <param name="description">A brief description of the mod.</param> + /// <param name="version">The mod version.</param> + /// <param name="minimumApiVersion">The minimum SMAPI version required by this mod, if any.</param> + /// <param name="entryDll">The name of the DLL in the directory that has the <c>Entry</c> method. Mutually exclusive with <see cref="ContentPackFor"/>.</param> + /// <param name="contentPackFor">The modID which will read this as a content pack.</param> + /// <param name="dependencies">The other mods that must be loaded before this mod.</param> + /// <param name="updateKeys">The namespaced mod IDs to query for updates (like <c>Nexus:541</c>).</param> + [JsonConstructor] + public Manifest(string uniqueId, string name, string author, string description, ISemanticVersion version, ISemanticVersion? minimumApiVersion, string? entryDll, IManifestContentPackFor? contentPackFor, IManifestDependency[]? dependencies, string[]? updateKeys) { - this.Name = name; - this.Author = author; - this.Description = description; + this.UniqueID = this.NormalizeWhitespace(uniqueId); + this.Name = this.NormalizeWhitespace(name); + this.Author = this.NormalizeWhitespace(author); + this.Description = this.NormalizeWhitespace(description); this.Version = version; - this.UniqueID = uniqueID; - this.UpdateKeys = Array.Empty<string>(); - this.ContentPackFor = new ManifestContentPackFor { UniqueID = contentPackFor }; + this.MinimumApiVersion = minimumApiVersion; + this.EntryDll = this.NormalizeWhitespace(entryDll); + this.ContentPackFor = contentPackFor; + this.Dependencies = dependencies ?? Array.Empty<IManifestDependency>(); + this.UpdateKeys = updateKeys ?? Array.Empty<string>(); + } + + /// <summary>Override the update keys loaded from the mod info.</summary> + /// <param name="updateKeys">The new update keys to set.</param> + internal void OverrideUpdateKeys(params string[] updateKeys) + { + this.UpdateKeys = updateKeys; } - /// <summary>Normalize the model after it's deserialized.</summary> - /// <param name="context">The deserialization context.</param> - [OnDeserialized] - public void OnDeserialized(StreamingContext context) + + /********* + ** Private methods + *********/ + /// <summary>Normalize whitespace in a raw string.</summary> + /// <param name="input">The input to strip.</param> +#if NET5_0_OR_GREATER + [return: NotNullIfNotNull("input")] +#endif + private string? NormalizeWhitespace(string? input) { - this.Dependencies ??= Array.Empty<IManifestDependency>(); - this.UpdateKeys ??= Array.Empty<string>(); + return input + ?.Trim() + .Replace("\r", "") + .Replace("\n", ""); } } } diff --git a/src/SMAPI.Toolkit/Serialization/Models/ManifestContentPackFor.cs b/src/SMAPI.Toolkit/Serialization/Models/ManifestContentPackFor.cs index 1eb80889..f7dc8aa8 100644 --- a/src/SMAPI.Toolkit/Serialization/Models/ManifestContentPackFor.cs +++ b/src/SMAPI.Toolkit/Serialization/Models/ManifestContentPackFor.cs @@ -1,3 +1,5 @@ +using System.Diagnostics.CodeAnalysis; + namespace StardewModdingAPI.Toolkit.Serialization.Models { /// <summary>Indicates which mod can read the content pack represented by the containing manifest.</summary> @@ -7,9 +9,36 @@ namespace StardewModdingAPI.Toolkit.Serialization.Models ** Accessors *********/ /// <summary>The unique ID of the mod which can read this content pack.</summary> - public string UniqueID { get; set; } + public string UniqueID { get; } /// <summary>The minimum required version (if any).</summary> - public ISemanticVersion MinimumVersion { get; set; } + public ISemanticVersion? MinimumVersion { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="uniqueId">The unique ID of the mod which can read this content pack.</param> + /// <param name="minimumVersion">The minimum required version (if any).</param> + public ManifestContentPackFor(string uniqueId, ISemanticVersion? minimumVersion) + { + this.UniqueID = this.NormalizeWhitespace(uniqueId); + this.MinimumVersion = minimumVersion; + } + + + /********* + ** Private methods + *********/ + /// <summary>Normalize whitespace in a raw string.</summary> + /// <param name="input">The input to strip.</param> +#if NET5_0_OR_GREATER + [return: NotNullIfNotNull("input")] +#endif + private string? NormalizeWhitespace(string? input) + { + return input?.Trim(); + } } } diff --git a/src/SMAPI.Toolkit/Serialization/Models/ManifestDependency.cs b/src/SMAPI.Toolkit/Serialization/Models/ManifestDependency.cs index 00f168f4..fa254ea7 100644 --- a/src/SMAPI.Toolkit/Serialization/Models/ManifestDependency.cs +++ b/src/SMAPI.Toolkit/Serialization/Models/ManifestDependency.cs @@ -1,3 +1,6 @@ +using System.Diagnostics.CodeAnalysis; +using Newtonsoft.Json; + namespace StardewModdingAPI.Toolkit.Serialization.Models { /// <summary>A mod dependency listed in a mod manifest.</summary> @@ -7,13 +10,13 @@ namespace StardewModdingAPI.Toolkit.Serialization.Models ** Accessors *********/ /// <summary>The unique mod ID to require.</summary> - public string UniqueID { get; set; } + public string UniqueID { get; } /// <summary>The minimum required version (if any).</summary> - public ISemanticVersion MinimumVersion { get; set; } + public ISemanticVersion? MinimumVersion { get; } /// <summary>Whether the dependency must be installed to use the mod.</summary> - public bool IsRequired { get; set; } + public bool IsRequired { get; } /********* @@ -23,13 +26,40 @@ 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) + ? new SemanticVersion(minimumVersion) + : null, + required: required + ) + { } + + /// <summary>Construct an instance.</summary> + /// <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> + [JsonConstructor] + public ManifestDependency(string uniqueID, ISemanticVersion? minimumVersion, bool required = true) { - this.UniqueID = uniqueID; - this.MinimumVersion = !string.IsNullOrWhiteSpace(minimumVersion) - ? new SemanticVersion(minimumVersion) - : null; + this.UniqueID = this.NormalizeWhitespace(uniqueID); + this.MinimumVersion = minimumVersion; this.IsRequired = required; } + + + /********* + ** Private methods + *********/ + /// <summary>Normalize whitespace in a raw string.</summary> + /// <param name="input">The input to strip.</param> +#if NET5_0_OR_GREATER + [return: NotNullIfNotNull("input")] +#endif + private string? NormalizeWhitespace(string? input) + { + return input?.Trim(); + } } } diff --git a/src/SMAPI.Toolkit/Serialization/SParseException.cs b/src/SMAPI.Toolkit/Serialization/SParseException.cs index 5f58b5b8..c2b3f68e 100644 --- a/src/SMAPI.Toolkit/Serialization/SParseException.cs +++ b/src/SMAPI.Toolkit/Serialization/SParseException.cs @@ -11,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 7536337a..1791c5b3 100644 --- a/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs +++ b/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics.CodeAnalysis; using StardewModdingAPI.Toolkit.Framework; namespace StardewModdingAPI.Toolkit.Utilities @@ -34,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/PathUtilities.cs b/src/SMAPI.Toolkit/Utilities/PathUtilities.cs index 2e9e5eac..136279f2 100644 --- a/src/SMAPI.Toolkit/Utilities/PathUtilities.cs +++ b/src/SMAPI.Toolkit/Utilities/PathUtilities.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.IO; using System.Linq; @@ -36,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); @@ -45,8 +49,16 @@ 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] +#if NET5_0_OR_GREATER + [return: NotNullIfNotNull("assetName")] +#endif + 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 } @@ -54,7 +66,10 @@ 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) +#if NET5_0_OR_GREATER + [return: NotNullIfNotNull("path")] +#endif + public static string? NormalizePath(string? path) { path = path?.Trim(); if (string.IsNullOrEmpty(path)) @@ -100,8 +115,8 @@ namespace StardewModdingAPI.Toolkit.Utilities // though, this is only for compatibility with the mod build package. // convert to URIs - Uri from = new Uri(sourceDir.TrimEnd(PathUtilities.PossiblePathSeparators) + "/"); - Uri to = new Uri(targetPath.TrimEnd(PathUtilities.PossiblePathSeparators) + "/"); + Uri from = new(sourceDir.TrimEnd(PathUtilities.PossiblePathSeparators) + "/"); + Uri to = new(targetPath.TrimEnd(PathUtilities.PossiblePathSeparators) + "/"); if (from.Scheme != to.Scheme) throw new InvalidOperationException($"Can't get path for '{targetPath}' relative to '{sourceDir}'."); @@ -132,7 +147,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; @@ -145,9 +160,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); } } } |