enable nullable annotations for manifests (#837)

This commit is contained in:
Jesse Plamondon-Willard 2022-04-07 01:38:02 -04:00
parent ab6cf45b03
commit e58e8a2283
No known key found for this signature in database
GPG Key ID: CF8B1456B3E29F49
9 changed files with 164 additions and 91 deletions

View File

@ -1,5 +1,3 @@
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@ -52,11 +50,11 @@ namespace SMAPI.Tests.Core
// act // act
IModMetadata[] mods = new ModResolver().ReadManifests(new ModToolkit(), rootFolder, new ModDatabase()).ToArray(); IModMetadata[] mods = new ModResolver().ReadManifests(new ModToolkit(), rootFolder, new ModDatabase()).ToArray();
IModMetadata mod = mods.FirstOrDefault(); IModMetadata? mod = mods.FirstOrDefault();
// assert // assert
Assert.AreEqual(1, mods.Length, 0, $"Expected to find one manifest, found {mods.Length} instead."); Assert.AreEqual(1, mods.Length, 0, $"Expected to find one manifest, found {mods.Length} instead.");
Assert.AreEqual(ModMetadataStatus.Failed, mod.Status, "The mod metadata was not marked failed."); Assert.AreEqual(ModMetadataStatus.Failed, mod!.Status, "The mod metadata was not marked failed.");
Assert.IsNotNull(mod.Error, "The mod metadata did not have an error message set."); Assert.IsNotNull(mod.Error, "The mod metadata did not have an error message set.");
} }
@ -91,12 +89,12 @@ namespace SMAPI.Tests.Core
// act // act
IModMetadata[] mods = new ModResolver().ReadManifests(new ModToolkit(), rootFolder, new ModDatabase()).ToArray(); IModMetadata[] mods = new ModResolver().ReadManifests(new ModToolkit(), rootFolder, new ModDatabase()).ToArray();
IModMetadata mod = mods.FirstOrDefault(); IModMetadata? mod = mods.FirstOrDefault();
// assert // assert
Assert.AreEqual(1, mods.Length, 0, "Expected to find one manifest."); Assert.AreEqual(1, mods.Length, 0, "Expected to find one manifest.");
Assert.IsNotNull(mod, "The loaded manifest shouldn't be null."); Assert.IsNotNull(mod, "The loaded manifest shouldn't be null.");
Assert.AreEqual(null, mod.DataRecord, "The data record should be null since we didn't provide one."); Assert.AreEqual(null, mod!.DataRecord, "The data record should be null since we didn't provide one.");
Assert.AreEqual(modFolder, mod.DirectoryPath, "The directory path doesn't match."); Assert.AreEqual(modFolder, mod.DirectoryPath, "The directory path doesn't match.");
Assert.AreEqual(null, mod.Error, "The error should be null since parsing should have succeeded."); Assert.AreEqual(null, mod.Error, "The error should be null since parsing should have succeeded.");
Assert.AreEqual(ModMetadataStatus.Found, mod.Status, "The status doesn't match."); Assert.AreEqual(ModMetadataStatus.Found, mod.Status, "The status doesn't match.");
@ -215,7 +213,7 @@ namespace SMAPI.Tests.Core
// create DLL // create DLL
string modFolder = Path.Combine(this.GetTempFolderPath(), Guid.NewGuid().ToString("N")); string modFolder = Path.Combine(this.GetTempFolderPath(), Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(modFolder); Directory.CreateDirectory(modFolder);
File.WriteAllText(Path.Combine(modFolder, manifest.EntryDll), ""); File.WriteAllText(Path.Combine(modFolder, manifest.EntryDll!), "");
// arrange // arrange
Mock<IModMetadata> mock = new Mock<IModMetadata>(MockBehavior.Strict); Mock<IModMetadata> mock = new Mock<IModMetadata>(MockBehavior.Strict);
@ -480,21 +478,20 @@ namespace SMAPI.Tests.Core
/// <param name="contentPackForID">The <see cref="IManifest.ContentPackFor"/> value.</param> /// <param name="contentPackForID">The <see cref="IManifest.ContentPackFor"/> value.</param>
/// <param name="minimumApiVersion">The <see cref="IManifest.MinimumApiVersion"/> value.</param> /// <param name="minimumApiVersion">The <see cref="IManifest.MinimumApiVersion"/> value.</param>
/// <param name="dependencies">The <see cref="IManifest.Dependencies"/> value.</param> /// <param name="dependencies">The <see cref="IManifest.Dependencies"/> value.</param>
private Manifest GetManifest(string id = null, string name = null, string version = null, string entryDll = null, string contentPackForID = null, string minimumApiVersion = null, IManifestDependency[] dependencies = null) private Manifest GetManifest(string? id = null, string? name = null, string? version = null, string? entryDll = null, string? contentPackForID = null, string? minimumApiVersion = null, IManifestDependency[]? dependencies = null)
{ {
return new Manifest return new Manifest(
{ uniqueId: id ?? $"{Sample.String()}.{Sample.String()}",
UniqueID = id ?? $"{Sample.String()}.{Sample.String()}", name: name ?? id ?? Sample.String(),
Name = name ?? id ?? Sample.String(), author: Sample.String(),
Author = Sample.String(), description: Sample.String(),
Description = Sample.String(), version: version != null ? new SemanticVersion(version) : new SemanticVersion(Sample.Int(), Sample.Int(), Sample.Int(), Sample.String()),
Version = version != null ? new SemanticVersion(version) : new SemanticVersion(Sample.Int(), Sample.Int(), Sample.Int(), Sample.String()), entryDll: entryDll ?? $"{Sample.String()}.dll",
EntryDll = entryDll ?? $"{Sample.String()}.dll", contentPackFor: contentPackForID != null ? new ManifestContentPackFor(contentPackForID, null) : null,
ContentPackFor = contentPackForID != null ? new ManifestContentPackFor { UniqueID = contentPackForID } : null, minimumApiVersion: minimumApiVersion != null ? new SemanticVersion(minimumApiVersion) : null,
MinimumApiVersion = minimumApiVersion != null ? new SemanticVersion(minimumApiVersion) : null, dependencies: dependencies ?? Array.Empty<IManifestDependency>(),
Dependencies = dependencies ?? Array.Empty<IManifestDependency>(), updateKeys: Array.Empty<string>()
UpdateKeys = Array.Empty<string>() );
};
} }
/// <summary>Get a randomized basic manifest.</summary> /// <summary>Get a randomized basic manifest.</summary>
@ -510,7 +507,7 @@ namespace SMAPI.Tests.Core
/// <param name="allowStatusChange">Whether the code being tested is allowed to change the mod status.</param> /// <param name="allowStatusChange">Whether the code being tested is allowed to change the mod status.</param>
private Mock<IModMetadata> GetMetadata(string uniqueID, string[] dependencies, bool allowStatusChange = false) private Mock<IModMetadata> GetMetadata(string uniqueID, string[] dependencies, bool allowStatusChange = false)
{ {
IManifest manifest = this.GetManifest(id: uniqueID, version: "1.0", dependencies: dependencies?.Select(dependencyID => (IManifestDependency)new ManifestDependency(dependencyID, null)).ToArray()); IManifest manifest = this.GetManifest(id: uniqueID, version: "1.0", dependencies: dependencies?.Select(dependencyID => (IManifestDependency)new ManifestDependency(dependencyID, null as ISemanticVersion)).ToArray());
return this.GetMetadata(manifest, allowStatusChange); return this.GetMetadata(manifest, allowStatusChange);
} }
@ -538,7 +535,7 @@ namespace SMAPI.Tests.Core
/// <summary>Set up a mock mod metadata for <see cref="ModResolver.ValidateManifests"/>.</summary> /// <summary>Set up a mock mod metadata for <see cref="ModResolver.ValidateManifests"/>.</summary>
/// <param name="mod">The mock mod metadata.</param> /// <param name="mod">The mock mod metadata.</param>
/// <param name="modRecord">The extra metadata about the mod from SMAPI's internal data (if any).</param> /// <param name="modRecord">The extra metadata about the mod from SMAPI's internal data (if any).</param>
private void SetupMetadataForValidation(Mock<IModMetadata> mod, ModDataRecordVersionedFields modRecord = null) private void SetupMetadataForValidation(Mock<IModMetadata> mod, ModDataRecordVersionedFields? modRecord = null)
{ {
mod.Setup(p => p.Status).Returns(ModMetadataStatus.Found); mod.Setup(p => p.Status).Returns(ModMetadataStatus.Found);
mod.Setup(p => p.DataRecord).Returns(() => null); mod.Setup(p => p.DataRecord).Returns(() => null);

View File

@ -1,5 +1,3 @@
#nullable disable
using System.Collections.Generic; using System.Collections.Generic;
namespace StardewModdingAPI namespace StardewModdingAPI
@ -23,16 +21,16 @@ namespace StardewModdingAPI
ISemanticVersion Version { get; } ISemanticVersion Version { get; }
/// <summary>The minimum SMAPI version required by this mod, if any.</summary> /// <summary>The minimum SMAPI version required by this mod, if any.</summary>
ISemanticVersion MinimumApiVersion { get; } ISemanticVersion? MinimumApiVersion { get; }
/// <summary>The unique mod ID.</summary> /// <summary>The unique mod ID.</summary>
string UniqueID { get; } string UniqueID { get; }
/// <summary>The name of the DLL in the directory that has the <c>Entry</c> method. Mutually exclusive with <see cref="ContentPackFor"/>.</summary> /// <summary>The name of the DLL in the directory that has the <c>Entry</c> method. Mutually exclusive with <see cref="ContentPackFor"/>.</summary>
string EntryDll { get; } string? EntryDll { get; }
/// <summary>The mod which will read this as a content pack. Mutually exclusive with <see cref="EntryDll"/>.</summary> /// <summary>The mod which will read this as a content pack. Mutually exclusive with <see cref="EntryDll"/>.</summary>
IManifestContentPackFor ContentPackFor { get; } IManifestContentPackFor? ContentPackFor { get; }
/// <summary>The other mods that must be loaded before this mod.</summary> /// <summary>The other mods that must be loaded before this mod.</summary>
IManifestDependency[] Dependencies { get; } IManifestDependency[] Dependencies { get; }

View File

@ -1,5 +1,3 @@
#nullable disable
namespace StardewModdingAPI namespace StardewModdingAPI
{ {
/// <summary>Indicates which mod can read the content pack represented by the containing manifest.</summary> /// <summary>Indicates which mod can read the content pack represented by the containing manifest.</summary>
@ -9,6 +7,6 @@ namespace StardewModdingAPI
string UniqueID { get; } string UniqueID { get; }
/// <summary>The minimum required version (if any).</summary> /// <summary>The minimum required version (if any).</summary>
ISemanticVersion MinimumVersion { get; } ISemanticVersion? MinimumVersion { get; }
} }
} }

View File

@ -1,5 +1,3 @@
#nullable disable
namespace StardewModdingAPI namespace StardewModdingAPI
{ {
/// <summary>A mod dependency listed in a mod manifest.</summary> /// <summary>A mod dependency listed in a mod manifest.</summary>
@ -12,7 +10,7 @@ namespace StardewModdingAPI
string UniqueID { get; } string UniqueID { get; }
/// <summary>The minimum required version (if any).</summary> /// <summary>The minimum required version (if any).</summary>
ISemanticVersion MinimumVersion { get; } ISemanticVersion? MinimumVersion { get; }
/// <summary>Whether the dependency must be installed to use the mod.</summary> /// <summary>Whether the dependency must be installed to use the mod.</summary>
bool IsRequired { get; } bool IsRequired { get; }

View File

@ -171,14 +171,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 // get mod type
ModType type; ModType type;
{ {
@ -365,12 +357,5 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
return hasVortexMarker; 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", "");
}
} }
} }

View File

@ -1,8 +1,6 @@
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.Serialization; using System.Diagnostics.CodeAnalysis;
using Newtonsoft.Json; using Newtonsoft.Json;
using StardewModdingAPI.Toolkit.Serialization.Converters; using StardewModdingAPI.Toolkit.Serialization.Converters;
@ -15,48 +13,45 @@ namespace StardewModdingAPI.Toolkit.Serialization.Models
** Accessors ** Accessors
*********/ *********/
/// <summary>The mod name.</summary> /// <summary>The mod name.</summary>
public string Name { get; set; } public string Name { get; }
/// <summary>A brief description of the mod.</summary> /// <summary>A brief description of the mod.</summary>
public string Description { get; set; } public string Description { get; }
/// <summary>The mod author's name.</summary> /// <summary>The mod author's name.</summary>
public string Author { get; set; } public string Author { get; }
/// <summary>The mod version.</summary> /// <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> /// <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> /// <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> /// <summary>The mod which will read this as a content pack. Mutually exclusive with <see cref="Manifest.EntryDll"/>.</summary>
[JsonConverter(typeof(ManifestContentPackForConverter))] [JsonConverter(typeof(ManifestContentPackForConverter))]
public IManifestContentPackFor ContentPackFor { get; set; } public IManifestContentPackFor? ContentPackFor { get; }
/// <summary>The other mods that must be loaded before this mod.</summary> /// <summary>The other mods that must be loaded before this mod.</summary>
[JsonConverter(typeof(ManifestDependencyArrayConverter))] [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> /// <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> /// <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> /// <summary>Any manifest fields which didn't match a valid field.</summary>
[JsonExtensionData] [JsonExtensionData]
public IDictionary<string, object> ExtraFields { get; set; } public IDictionary<string, object> ExtraFields { get; set; } = new Dictionary<string, object>();
/********* /*********
** Public methods ** Public methods
*********/ *********/
/// <summary>Construct an instance.</summary>
public Manifest() { }
/// <summary>Construct an instance for a transitional content pack.</summary> /// <summary>Construct an instance for a transitional content pack.</summary>
/// <param name="uniqueID">The unique mod ID.</param> /// <param name="uniqueID">The unique mod ID.</param>
/// <param name="name">The mod name.</param> /// <param name="name">The mod name.</param>
@ -64,24 +59,71 @@ namespace StardewModdingAPI.Toolkit.Serialization.Models
/// <param name="description">A brief description of the mod.</param> /// <param name="description">A brief description of the mod.</param>
/// <param name="version">The mod version.</param> /// <param name="version">The mod version.</param>
/// <param name="contentPackFor">The modID which will read this as a content pack.</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.UniqueID = this.NormalizeWhitespace(uniqueId);
this.Author = author; this.Name = this.NormalizeWhitespace(name);
this.Description = description; this.Author = this.NormalizeWhitespace(author);
this.Description = this.NormalizeWhitespace(description);
this.Version = version; this.Version = version;
this.UniqueID = uniqueID; this.MinimumApiVersion = minimumApiVersion;
this.UpdateKeys = Array.Empty<string>(); this.EntryDll = this.NormalizeWhitespace(entryDll);
this.ContentPackFor = new ManifestContentPackFor { UniqueID = contentPackFor }; this.ContentPackFor = contentPackFor;
this.Dependencies = dependencies ?? Array.Empty<IManifestDependency>();
this.UpdateKeys = updateKeys ?? Array.Empty<string>();
} }
/// <summary>Normalize the model after it's deserialized.</summary> /// <summary>Override the update keys loaded from the mod info.</summary>
/// <param name="context">The deserialization context.</param> /// <param name="updateKeys">The new update keys to set.</param>
[OnDeserialized] internal void OverrideUpdateKeys(params string[] updateKeys)
public void OnDeserialized(StreamingContext context)
{ {
this.Dependencies ??= Array.Empty<IManifestDependency>(); this.UpdateKeys = updateKeys;
this.UpdateKeys ??= Array.Empty<string>(); }
/*********
** 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()
.Replace("\r", "")
.Replace("\n", "");
} }
} }
} }

View File

@ -1,4 +1,4 @@
#nullable disable using System.Diagnostics.CodeAnalysis;
namespace StardewModdingAPI.Toolkit.Serialization.Models namespace StardewModdingAPI.Toolkit.Serialization.Models
{ {
@ -9,9 +9,36 @@ namespace StardewModdingAPI.Toolkit.Serialization.Models
** Accessors ** Accessors
*********/ *********/
/// <summary>The unique ID of the mod which can read this content pack.</summary> /// <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> /// <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();
}
} }
} }

View File

@ -1,4 +1,5 @@
#nullable disable using System.Diagnostics.CodeAnalysis;
using Newtonsoft.Json;
namespace StardewModdingAPI.Toolkit.Serialization.Models namespace StardewModdingAPI.Toolkit.Serialization.Models
{ {
@ -9,13 +10,13 @@ namespace StardewModdingAPI.Toolkit.Serialization.Models
** Accessors ** Accessors
*********/ *********/
/// <summary>The unique mod ID to require.</summary> /// <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> /// <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> /// <summary>Whether the dependency must be installed to use the mod.</summary>
public bool IsRequired { get; set; } public bool IsRequired { get; }
/********* /*********
@ -26,12 +27,39 @@ namespace StardewModdingAPI.Toolkit.Serialization.Models
/// <param name="minimumVersion">The minimum required version (if any).</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> /// <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.UniqueID = this.NormalizeWhitespace(uniqueID);
this.MinimumVersion = !string.IsNullOrWhiteSpace(minimumVersion) this.MinimumVersion = minimumVersion;
? new SemanticVersion(minimumVersion)
: null;
this.IsRequired = required; 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();
}
} }
} }

View File

@ -35,7 +35,7 @@ namespace StardewModdingAPI.Framework.ModLoading
// apply defaults // apply defaults
if (manifest != null && dataRecord?.UpdateKey is not null) if (manifest != null && dataRecord?.UpdateKey is not null)
manifest.UpdateKeys = new[] { dataRecord.UpdateKey }; manifest.OverrideUpdateKeys(dataRecord.UpdateKey);
// build metadata // build metadata
bool shouldIgnore = folder.Type == ModType.Ignored; bool shouldIgnore = folder.Type == ModType.Ignored;