From 8d8b640779061a5cd001890c15f6f00a602df463 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 5 Nov 2016 16:20:31 -0400 Subject: add deprecation warnings (#165) --- src/StardewModdingAPI/Config.cs | 11 ++ .../Framework/DeprecationManager.cs | 124 +++++++++++++++++++++ src/StardewModdingAPI/Manifest.cs | 10 +- src/StardewModdingAPI/Mod.cs | 45 +++++++- src/StardewModdingAPI/Program.cs | 17 ++- src/StardewModdingAPI/StardewModdingAPI.csproj | 1 + src/StardewModdingAPI/Version.cs | 13 ++- 7 files changed, 212 insertions(+), 9 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/DeprecationManager.cs (limited to 'src/StardewModdingAPI') diff --git a/src/StardewModdingAPI/Config.cs b/src/StardewModdingAPI/Config.cs index d811d69d..91503a83 100644 --- a/src/StardewModdingAPI/Config.cs +++ b/src/StardewModdingAPI/Config.cs @@ -99,6 +99,17 @@ namespace StardewModdingAPI return this as T; } } + + + /********* + ** Protected methods + *********/ + /// Construct an instance. + protected Config() + { + Program.DeprecationManager.Warn("the Config class", "1.0"); + Program.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(Mod.BaseConfigPath)}", "1.0"); // typically used to construct config, avoid redundant warnings + } } /// Provides extension methods for classes. diff --git a/src/StardewModdingAPI/Framework/DeprecationManager.cs b/src/StardewModdingAPI/Framework/DeprecationManager.cs new file mode 100644 index 00000000..2d4ff614 --- /dev/null +++ b/src/StardewModdingAPI/Framework/DeprecationManager.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; + +namespace StardewModdingAPI.Framework +{ + /// Manages deprecation warnings. + internal class DeprecationManager + { + /********* + ** Properties + *********/ + /// The friendly mod names treated as deprecation warning sources (assembly full name => mod name). + private readonly IDictionary ModNamesByAssembly = new Dictionary(); + + /// The deprecations which have already been logged (as 'mod name::noun phrase::version'). + private readonly HashSet LoggedDeprecations = new HashSet(StringComparer.InvariantCultureIgnoreCase); + + + /********* + ** Public methods + *********/ + /// Register a mod as a possible source of deprecation warnings. + /// The mod assembly. + /// The mod's friendly name. + public void AddMod(Assembly assembly, string name) + { + this.ModNamesByAssembly[assembly.FullName] = name; + } + + /// Log a deprecation warning. + /// A noun phrase describing what is deprecated. + /// The SMAPI version which deprecated it. + public void Warn(string nounPhrase, string version) + { + this.Warn(this.GetSourceNameFromStack(), nounPhrase, version); + } + + /// Log a deprecation warning. + /// The friendly mod name which used the deprecated code. + /// A noun phrase describing what is deprecated. + /// The SMAPI version which deprecated it. + public void Warn(string source, string nounPhrase, string version) + { + if (source != null && !this.MarkWarned(source, nounPhrase, version)) + return; + + Log.Debug(source != null + ? $"NOTE: {source} used {nounPhrase}, which is deprecated since SMAPI {version}. It will work fine for now, but may be removed in a future version of SMAPI." + : $"NOTE: an unknown mod used {nounPhrase}, which is deprecated since SMAPI {version}. It will work fine for now, but may be removed in a future version of SMAPI.\n{Environment.StackTrace}" + ); + } + + /// Mark a deprecation warning as already logged. + /// A noun phrase describing what is deprecated (e.g. "the Extensions.AsInt32 method"). + /// The SMAPI version which deprecated it. + /// Returns whether the deprecation was successfully marked as warned. Returns false if it was already marked. + public bool MarkWarned(string nounPhrase, string version) + { + return this.MarkWarned(this.GetSourceNameFromStack(), nounPhrase, version); + } + + /// Mark a deprecation warning as already logged. + /// The friendly name of the assembly which used the deprecated code. + /// A noun phrase describing what is deprecated (e.g. "the Extensions.AsInt32 method"). + /// The SMAPI version which deprecated it. + /// Returns whether the deprecation was successfully marked as warned. Returns false if it was already marked. + public bool MarkWarned(string source, string nounPhrase, string version) + { + if (string.IsNullOrWhiteSpace(source)) + throw new InvalidOperationException("The deprecation source cannot be empty."); + + string key = $"{source}::{nounPhrase}::{version}"; + if (this.LoggedDeprecations.Contains(key)) + return false; + this.LoggedDeprecations.Add(key); + return true; + } + + /// Get whether a type implements the given virtual method. + /// The type to check. + /// The base type which declares the virtual method. + /// The method name. + public bool IsVirtualMethodImplemented(Type subtype, Type baseType, string name) + { + MethodInfo method = subtype.GetMethod(nameof(Mod.Entry), new[] { typeof(object[]) }); + return method.DeclaringType != baseType; + } + + + /********* + ** Private methods + *********/ + /// Get the friendly name for the closest assembly registered as a source of deprecation warnings. + /// Returns the source name, or null if no registered assemblies were found. + private string GetSourceNameFromStack() + { + // get stack frames + StackTrace stack = new StackTrace(); + StackFrame[] frames = stack.GetFrames(); + if (frames == null) + return null; + + // search stack for a source assembly + foreach (StackFrame frame in frames) + { + // get assembly name + MethodBase method = frame.GetMethod(); + Type type = method.ReflectedType; + if (type == null) + continue; + string assemblyName = type.Assembly.FullName; + + // get name if it's a registered source + if (this.ModNamesByAssembly.ContainsKey(assemblyName)) + return this.ModNamesByAssembly[assemblyName]; + } + + // no known assembly found + return null; + } + } +} diff --git a/src/StardewModdingAPI/Manifest.cs b/src/StardewModdingAPI/Manifest.cs index 89ce7904..6819cbd1 100644 --- a/src/StardewModdingAPI/Manifest.cs +++ b/src/StardewModdingAPI/Manifest.cs @@ -9,6 +9,10 @@ namespace StardewModdingAPI /********* ** Accessors *********/ + /// Whether the manifest defined the deprecated field. + [JsonIgnore] + internal bool UsedAuthourField { get; private set; } + /// The mod name. public virtual string Name { get; set; } = ""; @@ -20,7 +24,11 @@ namespace StardewModdingAPI public virtual string Authour { get { return this.Author; } - set { this.Author = value; } + set + { + this.UsedAuthourField = true; + this.Author = value; + } } /// The mod version. diff --git a/src/StardewModdingAPI/Mod.cs b/src/StardewModdingAPI/Mod.cs index c1cc99d4..fa70d291 100644 --- a/src/StardewModdingAPI/Mod.cs +++ b/src/StardewModdingAPI/Mod.cs @@ -6,6 +6,12 @@ namespace StardewModdingAPI /// The base class for a mod. public class Mod { + /********* + ** Properties + *********/ + /// The backing field for . + private string _pathOnDisk; + /********* ** Accessors *********/ @@ -16,16 +22,44 @@ namespace StardewModdingAPI public Manifest Manifest { get; internal set; } /// The full path to the mod's directory on the disk. - public string PathOnDisk { get; internal set; } + [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(ModHelper.DirectoryPath) + " instead")] + public string PathOnDisk + { + get + { + Program.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(PathOnDisk)}", "1.0"); + return this._pathOnDisk; + } + internal set { this._pathOnDisk = value; } + } /// The full path to the mod's config.json file on the disk. - public string BaseConfigPath => Path.Combine(this.PathOnDisk, "config.json"); + [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(ModHelper.ReadConfig) + " instead")] + public string BaseConfigPath + { + get + { + Program.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(BaseConfigPath)}", "1.0"); + Program.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(PathOnDisk)}", "1.0"); // avoid redundant warnings + return Path.Combine(this.PathOnDisk, "config.json"); + } + } /// The full path to the per-save configs folder (if is true). + [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(ModHelper.ReadJsonFile) + " instead")] public string PerSaveConfigFolder => this.GetPerSaveConfigFolder(); /// The full path to the per-save configuration file for the current save (if is true). - public string PerSaveConfigPath => Constants.CurrentSavePathExists ? Path.Combine(this.PerSaveConfigFolder, Constants.SaveFolderName + ".json") : ""; + [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(ModHelper.ReadJsonFile) + " instead")] + public string PerSaveConfigPath + { + get + { + Program.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(PerSaveConfigPath)}", "1.0"); + Program.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(PerSaveConfigFolder)}", "1.0"); // avoid redundant warnings + return Constants.CurrentSavePathExists ? Path.Combine(this.PerSaveConfigFolder, Constants.SaveFolderName + ".json") : ""; + } + } /********* @@ -46,9 +80,12 @@ namespace StardewModdingAPI /// Get the full path to the per-save configuration file for the current save (if is true). private string GetPerSaveConfigFolder() { + Program.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(PerSaveConfigFolder)}", "1.0"); + Program.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(PathOnDisk)}", "1.0"); // avoid redundant warnings + if (!this.Manifest.PerSaveConfigs) { - Log.AsyncR($"The mod [{this.Manifest.Name}] is not configured to use per-save configs."); + Log.Error($"The mod [{this.Manifest.Name}] is not configured to use per-save configs."); return ""; } return Path.Combine(this.PathOnDisk, "psconfigs"); diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index c0129036..3831e3d4 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -54,6 +54,8 @@ namespace StardewModdingAPI /// The game's build type (i.e. GOG vs Steam). public static int BuildType => (int)Program.StardewProgramType.GetField("buildType", BindingFlags.Public | BindingFlags.Static).GetValue(null); + /// Manages deprecation warnings. + internal static readonly DeprecationManager DeprecationManager = new DeprecationManager(); /********* ** Public methods @@ -267,6 +269,10 @@ namespace StardewModdingAPI Log.Error($"{errorPrefix}: manifest doesn't specify an entry DLL."); continue; } + + // log deprecated fields + if(manifest.UsedAuthourField) + Program.DeprecationManager.Warn(manifest.Name, $"{nameof(Manifest)}.{nameof(Manifest.Authour)}", "1.0"); } catch (Exception ex) { @@ -277,6 +283,7 @@ namespace StardewModdingAPI // create per-save directory if (manifest.PerSaveConfigs) { + Program.DeprecationManager.Warn($"{nameof(Manifest)}.{nameof(Manifest.PerSaveConfigs)}", "1.0"); try { string psDir = Path.Combine(directory, "psconfigs"); @@ -312,13 +319,21 @@ namespace StardewModdingAPI Mod modEntry = (Mod)modAssembly.CreateInstance(modEntryType.ToString()); if (modEntry != null) { + // add as possible source of deprecation warnings + Program.DeprecationManager.AddMod(modAssembly, manifest.Name); + + // hook up mod modEntry.Helper = helper; modEntry.PathOnDisk = directory; modEntry.Manifest = manifest; Log.Info($"Loaded mod: {modEntry.Manifest.Name} by {modEntry.Manifest.Author}, v{modEntry.Manifest.Version} | {modEntry.Manifest.Description}\n@ {targDll}"); Program.ModsLoaded += 1; - modEntry.Entry(); // obsolete + modEntry.Entry(); // deprecated modEntry.Entry(modEntry.Helper); + + // raise deprecation warning for old Entry() method + if (Program.DeprecationManager.IsVirtualMethodImplemented(modEntryType, typeof(Mod), nameof(Mod.Entry))) + Program.DeprecationManager.Warn(manifest.Name, $"an old version of {nameof(Mod)}.{nameof(Mod.Entry)}", "1.0"); } } else diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 451e5961..0b55a925 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -195,6 +195,7 @@ + diff --git a/src/StardewModdingAPI/Version.cs b/src/StardewModdingAPI/Version.cs index b1903d7b..75195820 100644 --- a/src/StardewModdingAPI/Version.cs +++ b/src/StardewModdingAPI/Version.cs @@ -32,8 +32,15 @@ namespace StardewModdingAPI /// Obsolete. [JsonIgnore] - [Obsolete("Use `Version.ToString()` instead.")] - public string VersionString => this.ToString(); + [Obsolete("Use " + nameof(Version) + "." + nameof(Version.ToString) + " instead.")] + public string VersionString + { + get + { + Program.DeprecationManager.Warn($"{nameof(Version)}.{nameof(Version.VersionString)}", "1.0"); + return this.ToString(); + } + } /********* @@ -59,7 +66,7 @@ namespace StardewModdingAPI var match = Version.Regex.Match(version); if (!match.Success) throw new FormatException($"The input '{version}' is not a semantic version."); - + this.MajorVersion = int.Parse(match.Groups["major"].Value); this.MinorVersion = match.Groups["minor"].Success ? int.Parse(match.Groups["minor"].Value) : 0; this.PatchVersion = match.Groups["patch"].Success ? int.Parse(match.Groups["patch"].Value) : 0; -- cgit