From 889004f1eba31aa3a5069e1dcbe79896d05720b0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 19 Apr 2022 19:03:47 -0400 Subject: move deprecation code into namespace --- .../Framework/Deprecations/DeprecationLevel.cs | 15 ++ .../Framework/Deprecations/DeprecationManager.cs | 171 +++++++++++++++++++++ .../Framework/Deprecations/DeprecationWarning.cs | 48 ++++++ 3 files changed, 234 insertions(+) create mode 100644 src/SMAPI/Framework/Deprecations/DeprecationLevel.cs create mode 100644 src/SMAPI/Framework/Deprecations/DeprecationManager.cs create mode 100644 src/SMAPI/Framework/Deprecations/DeprecationWarning.cs (limited to 'src/SMAPI/Framework/Deprecations') diff --git a/src/SMAPI/Framework/Deprecations/DeprecationLevel.cs b/src/SMAPI/Framework/Deprecations/DeprecationLevel.cs new file mode 100644 index 00000000..8b15b59a --- /dev/null +++ b/src/SMAPI/Framework/Deprecations/DeprecationLevel.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Framework.Deprecations +{ + /// Indicates how deprecated something is. + internal enum DeprecationLevel + { + /// It's deprecated but won't be removed soon. Mod authors have some time to update their mods. Deprecation warnings should be logged, but not written to the console. + Notice, + + /// Mods should no longer be using it. Deprecation messages should be debug entries in the console. + Info, + + /// The code will be removed soon. Deprecation messages should be warnings in the console. + PendingRemoval + } +} diff --git a/src/SMAPI/Framework/Deprecations/DeprecationManager.cs b/src/SMAPI/Framework/Deprecations/DeprecationManager.cs new file mode 100644 index 00000000..da17ce7e --- /dev/null +++ b/src/SMAPI/Framework/Deprecations/DeprecationManager.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; + +namespace StardewModdingAPI.Framework.Deprecations +{ + /// Manages deprecation warnings. + internal class DeprecationManager + { + /********* + ** Fields + *********/ + /// The deprecations which have already been logged (as 'mod name::noun phrase::version'). + private readonly HashSet LoggedDeprecations = new(StringComparer.OrdinalIgnoreCase); + + /// Encapsulates monitoring and logging for a given module. + private readonly IMonitor Monitor; + + /// Tracks the installed mods. + private readonly ModRegistry ModRegistry; + + /// The queued deprecation warnings to display. + private readonly IList QueuedWarnings = new List(); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Encapsulates monitoring and logging for a given module. + /// Tracks the installed mods. + public DeprecationManager(IMonitor monitor, ModRegistry modRegistry) + { + this.Monitor = monitor; + this.ModRegistry = modRegistry; + } + + /// Get a mod for the closest assembly registered as a source of deprecation warnings. + /// Returns the source name, or null if no registered assemblies were found. + public IModMetadata? GetModFromStack() + { + return this.ModRegistry.GetFromStack(); + } + + /// Get a mod from its unique ID. + /// The mod's unique ID. + public IModMetadata? GetMod(string modId) + { + return this.ModRegistry.Get(modId); + } + + /// Log a deprecation warning. + /// The mod which used the deprecated code, if known. + /// A noun phrase describing what is deprecated. + /// The SMAPI version which deprecated it. + /// How deprecated the code is. + public void Warn(IModMetadata? source, string nounPhrase, string version, DeprecationLevel severity) + { + // ignore if already warned + if (!this.MarkWarned(source, nounPhrase, version)) + return; + + // queue warning + var stack = new StackTrace(skipFrames: 1); // skip this method + this.QueuedWarnings.Add(new DeprecationWarning(source, nounPhrase, version, severity, stack)); + } + + /// A placeholder method used to track deprecated code for which a separate warning will be shown. + /// The SMAPI version which deprecated it. + /// How deprecated the code is. + public void PlaceholderWarn(string version, DeprecationLevel severity) { } + + /// Print any queued messages. + public void PrintQueued() + { + foreach (DeprecationWarning warning in this.QueuedWarnings.OrderBy(p => p.ModName).ThenBy(p => p.NounPhrase)) + { + // build message + string message = $"{warning.ModName} uses deprecated code ({warning.NounPhrase} is deprecated since SMAPI {warning.Version})."; + + // get log level + LogLevel level; + switch (warning.Level) + { + case DeprecationLevel.Notice: + level = LogLevel.Trace; + break; + + case DeprecationLevel.Info: + level = LogLevel.Debug; + break; + + case DeprecationLevel.PendingRemoval: + level = LogLevel.Warn; + break; + + default: + throw new NotSupportedException($"Unknown deprecation level '{warning.Level}'."); + } + + // log message + if (level == LogLevel.Trace) + this.Monitor.Log($"{message}\n{this.GetSimplifiedStackTrace(warning.StackTrace, warning.Mod)}", level); + else + { + this.Monitor.Log(message, level); + this.Monitor.Log(this.GetSimplifiedStackTrace(warning.StackTrace, warning.Mod), LogLevel.Debug); + } + } + + this.QueuedWarnings.Clear(); + } + + + /********* + ** Private methods + *********/ + /// Mark a deprecation warning as already logged. + /// The mod 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. + private bool MarkWarned(IModMetadata? source, string nounPhrase, string version) + { + string key = $"{source?.DisplayName ?? ""}::{nounPhrase}::{version}"; + if (this.LoggedDeprecations.Contains(key)) + return false; + this.LoggedDeprecations.Add(key); + return true; + } + + /// Get the simplest stack trace which shows where in the mod the deprecated code was called from. + /// The stack trace. + /// The mod for which to show a stack trace. + private string GetSimplifiedStackTrace(StackTrace stack, IModMetadata? mod) + { + // unknown mod, show entire stack trace + if (mod == null) + return stack.ToString(); + + // get frame info + var frames = stack + .GetFrames() + .Select(frame => (Frame: frame, Mod: this.ModRegistry.GetFrom(frame))) + .ToArray(); + var modIds = new HashSet( + from frame in frames + let id = frame.Mod?.Manifest.UniqueID + where id != null + select id + ); + + // can't filter to the target mod + if (modIds.Count != 1 || !modIds.Contains(mod.Manifest.UniqueID)) + return stack.ToString(); + + // get stack frames for the target mod, plus one for context + var framesStartingAtMod = frames.SkipWhile(p => p.Mod == null).ToArray(); + var displayFrames = framesStartingAtMod.TakeWhile(p => p.Mod != null).ToArray(); + displayFrames = displayFrames.Concat(framesStartingAtMod.Skip(displayFrames.Length).Take(1)).ToArray(); + + // build stack trace + StringBuilder str = new(); + foreach (var frame in displayFrames) + str.Append(new StackTrace(frame.Frame)); + return str.ToString().TrimEnd(); + } + } +} diff --git a/src/SMAPI/Framework/Deprecations/DeprecationWarning.cs b/src/SMAPI/Framework/Deprecations/DeprecationWarning.cs new file mode 100644 index 00000000..38062daf --- /dev/null +++ b/src/SMAPI/Framework/Deprecations/DeprecationWarning.cs @@ -0,0 +1,48 @@ +using System.Diagnostics; + +namespace StardewModdingAPI.Framework.Deprecations +{ + /// A deprecation warning for a mod. + internal class DeprecationWarning + { + /********* + ** Accessors + *********/ + /// The affected mod. + public IModMetadata? Mod { get; } + + /// Get the display name for the affected mod. + public string ModName => this.Mod?.DisplayName ?? ""; + + /// A noun phrase describing what is deprecated. + public string NounPhrase { get; } + + /// The SMAPI version which deprecated it. + public string Version { get; } + + /// The deprecation level for the affected code. + public DeprecationLevel Level { get; } + + /// The stack trace when the deprecation warning was raised. + public StackTrace StackTrace { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The affected mod. + /// A noun phrase describing what is deprecated. + /// The SMAPI version which deprecated it. + /// The deprecation level for the affected code. + /// The stack trace when the deprecation warning was raised. + public DeprecationWarning(IModMetadata? mod, string nounPhrase, string version, DeprecationLevel level, StackTrace stackTrace) + { + this.Mod = mod; + this.NounPhrase = nounPhrase; + this.Version = version; + this.Level = level; + this.StackTrace = stackTrace; + } + } +} -- cgit