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 From e6c696fa6b0bfe5ef013e1179765ce1dcb071c38 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 19 Apr 2022 19:11:58 -0400 Subject: add immutable stack trace to cache stack info --- .../Framework/Deprecations/DeprecationManager.cs | 4 +- .../Framework/Deprecations/DeprecationWarning.cs | 4 +- .../Framework/Deprecations/ImmutableStackTrace.cs | 53 ++++++++++++++++++++++ 3 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 src/SMAPI/Framework/Deprecations/ImmutableStackTrace.cs (limited to 'src/SMAPI/Framework/Deprecations') diff --git a/src/SMAPI/Framework/Deprecations/DeprecationManager.cs b/src/SMAPI/Framework/Deprecations/DeprecationManager.cs index da17ce7e..84ce2132 100644 --- a/src/SMAPI/Framework/Deprecations/DeprecationManager.cs +++ b/src/SMAPI/Framework/Deprecations/DeprecationManager.cs @@ -63,7 +63,7 @@ namespace StardewModdingAPI.Framework.Deprecations return; // queue warning - var stack = new StackTrace(skipFrames: 1); // skip this method + ImmutableStackTrace stack = ImmutableStackTrace.Get(skipFrames: 1); this.QueuedWarnings.Add(new DeprecationWarning(source, nounPhrase, version, severity, stack)); } @@ -134,7 +134,7 @@ namespace StardewModdingAPI.Framework.Deprecations /// 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) + private string GetSimplifiedStackTrace(ImmutableStackTrace stack, IModMetadata? mod) { // unknown mod, show entire stack trace if (mod == null) diff --git a/src/SMAPI/Framework/Deprecations/DeprecationWarning.cs b/src/SMAPI/Framework/Deprecations/DeprecationWarning.cs index 38062daf..e00881b1 100644 --- a/src/SMAPI/Framework/Deprecations/DeprecationWarning.cs +++ b/src/SMAPI/Framework/Deprecations/DeprecationWarning.cs @@ -24,7 +24,7 @@ namespace StardewModdingAPI.Framework.Deprecations public DeprecationLevel Level { get; } /// The stack trace when the deprecation warning was raised. - public StackTrace StackTrace { get; } + public ImmutableStackTrace StackTrace { get; } /********* @@ -36,7 +36,7 @@ namespace StardewModdingAPI.Framework.Deprecations /// 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) + public DeprecationWarning(IModMetadata? mod, string nounPhrase, string version, DeprecationLevel level, ImmutableStackTrace stackTrace) { this.Mod = mod; this.NounPhrase = nounPhrase; diff --git a/src/SMAPI/Framework/Deprecations/ImmutableStackTrace.cs b/src/SMAPI/Framework/Deprecations/ImmutableStackTrace.cs new file mode 100644 index 00000000..059d871c --- /dev/null +++ b/src/SMAPI/Framework/Deprecations/ImmutableStackTrace.cs @@ -0,0 +1,53 @@ +using System.Diagnostics; + +namespace StardewModdingAPI.Framework.Deprecations +{ + /// An immutable stack trace that caches its values. + internal class ImmutableStackTrace + { + /********* + ** Fields + *********/ + /// The underlying stack trace. + private readonly StackTrace StackTrace; + + /// The individual method calls in the stack trace. + private StackFrame[]? Frames; + + /// The string representation of the stack trace. + private string? StringForm; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The underlying stack trace. + public ImmutableStackTrace(StackTrace stackTrace) + { + this.StackTrace = stackTrace; + } + + /// Get the underlying frames. + /// This is a reference to the underlying stack frames, so this array should not be edited. + public StackFrame[] GetFrames() + { + return this.Frames ??= this.StackTrace.GetFrames(); + } + + /// + public override string ToString() + { + return this.StringForm ??= this.StackTrace.ToString(); + } + + /// Get the current stack trace. + /// The number of frames up the stack from which to start the trace. + public static ImmutableStackTrace Get(int skipFrames = 0) + { + return new ImmutableStackTrace( + new StackTrace(skipFrames: skipFrames + 1) // also skip this method + ); + } + } +} -- cgit From 5c819662f88cd7004a85ac0d0e30a79b5dc4be37 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 19 Apr 2022 19:14:53 -0400 Subject: suppress some duplicate deprecation notices --- .../Framework/Deprecations/DeprecationManager.cs | 42 +++++++++++++--------- 1 file changed, 26 insertions(+), 16 deletions(-) (limited to 'src/SMAPI/Framework/Deprecations') diff --git a/src/SMAPI/Framework/Deprecations/DeprecationManager.cs b/src/SMAPI/Framework/Deprecations/DeprecationManager.cs index 84ce2132..5ca07702 100644 --- a/src/SMAPI/Framework/Deprecations/DeprecationManager.cs +++ b/src/SMAPI/Framework/Deprecations/DeprecationManager.cs @@ -56,15 +56,21 @@ namespace StardewModdingAPI.Framework.Deprecations /// 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) + /// A list of stack trace substrings which should suppress deprecation warnings if they appear in the stack trace. + public void Warn(IModMetadata? source, string nounPhrase, string version, DeprecationLevel severity, string[]? unlessStackIncludes = null) { - // ignore if already warned - if (!this.MarkWarned(source, nounPhrase, version)) + // skip if already warned + string cacheKey = $"{source?.DisplayName ?? ""}::{nounPhrase}::{version}"; + if (this.LoggedDeprecations.Contains(cacheKey)) return; - // queue warning + // warn if valid ImmutableStackTrace stack = ImmutableStackTrace.Get(skipFrames: 1); - this.QueuedWarnings.Add(new DeprecationWarning(source, nounPhrase, version, severity, stack)); + if (!this.ShouldSuppress(stack, unlessStackIncludes)) + { + this.LoggedDeprecations.Add(cacheKey); + 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. @@ -117,18 +123,22 @@ namespace StardewModdingAPI.Framework.Deprecations /********* ** 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) + /// Get whether a deprecation warning should be suppressed. + /// The stack trace for which it was raised. + /// A list of stack trace substrings which should suppress deprecation warnings if they appear in the stack trace. + private bool ShouldSuppress(ImmutableStackTrace stack, string[]? unlessStackIncludes) { - string key = $"{source?.DisplayName ?? ""}::{nounPhrase}::{version}"; - if (this.LoggedDeprecations.Contains(key)) - return false; - this.LoggedDeprecations.Add(key); - return true; + if (unlessStackIncludes?.Any() == true) + { + string stackTrace = stack.ToString(); + foreach (string method in unlessStackIncludes) + { + if (stackTrace.Contains(method)) + return true; + } + } + + return false; } /// Get the simplest stack trace which shows where in the mod the deprecated code was called from. -- cgit From 7bb7a7522fcdfef27f8f22ba77f24c7609f49912 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 30 Apr 2022 12:57:28 -0400 Subject: omit stack trace for deprecated code not called directly by the mod --- src/SMAPI/Framework/Deprecations/DeprecationManager.cs | 14 ++++++++++---- src/SMAPI/Framework/Deprecations/DeprecationWarning.cs | 9 ++++++--- 2 files changed, 16 insertions(+), 7 deletions(-) (limited to 'src/SMAPI/Framework/Deprecations') diff --git a/src/SMAPI/Framework/Deprecations/DeprecationManager.cs b/src/SMAPI/Framework/Deprecations/DeprecationManager.cs index 5ca07702..288abde2 100644 --- a/src/SMAPI/Framework/Deprecations/DeprecationManager.cs +++ b/src/SMAPI/Framework/Deprecations/DeprecationManager.cs @@ -57,7 +57,8 @@ namespace StardewModdingAPI.Framework.Deprecations /// The SMAPI version which deprecated it. /// How deprecated the code is. /// A list of stack trace substrings which should suppress deprecation warnings if they appear in the stack trace. - public void Warn(IModMetadata? source, string nounPhrase, string version, DeprecationLevel severity, string[]? unlessStackIncludes = null) + /// Whether to log a stack trace showing where the deprecated code is in the mod. + public void Warn(IModMetadata? source, string nounPhrase, string version, DeprecationLevel severity, string[]? unlessStackIncludes = null, bool logStackTrace = true) { // skip if already warned string cacheKey = $"{source?.DisplayName ?? ""}::{nounPhrase}::{version}"; @@ -69,7 +70,7 @@ namespace StardewModdingAPI.Framework.Deprecations if (!this.ShouldSuppress(stack, unlessStackIncludes)) { this.LoggedDeprecations.Add(cacheKey); - this.QueuedWarnings.Add(new DeprecationWarning(source, nounPhrase, version, severity, stack)); + this.QueuedWarnings.Add(new DeprecationWarning(source, nounPhrase, version, severity, stack, logStackTrace)); } } @@ -108,11 +109,16 @@ namespace StardewModdingAPI.Framework.Deprecations // log message if (level == LogLevel.Trace) - this.Monitor.Log($"{message}\n{this.GetSimplifiedStackTrace(warning.StackTrace, warning.Mod)}", level); + { + if (warning.LogStackTrace) + message += $"\n{this.GetSimplifiedStackTrace(warning.StackTrace, warning.Mod)}"; + this.Monitor.Log(message, level); + } else { this.Monitor.Log(message, level); - this.Monitor.Log(this.GetSimplifiedStackTrace(warning.StackTrace, warning.Mod), LogLevel.Debug); + if (warning.LogStackTrace) + this.Monitor.Log(this.GetSimplifiedStackTrace(warning.StackTrace, warning.Mod), LogLevel.Debug); } } diff --git a/src/SMAPI/Framework/Deprecations/DeprecationWarning.cs b/src/SMAPI/Framework/Deprecations/DeprecationWarning.cs index e00881b1..5936517b 100644 --- a/src/SMAPI/Framework/Deprecations/DeprecationWarning.cs +++ b/src/SMAPI/Framework/Deprecations/DeprecationWarning.cs @@ -1,5 +1,3 @@ -using System.Diagnostics; - namespace StardewModdingAPI.Framework.Deprecations { /// A deprecation warning for a mod. @@ -26,6 +24,9 @@ namespace StardewModdingAPI.Framework.Deprecations /// The stack trace when the deprecation warning was raised. public ImmutableStackTrace StackTrace { get; } + /// Whether to log a stack trace showing where the deprecated code is in the mod. + public bool LogStackTrace { get; } + /********* ** Public methods @@ -36,13 +37,15 @@ namespace StardewModdingAPI.Framework.Deprecations /// 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, ImmutableStackTrace stackTrace) + /// Whether to log a stack trace showing where the deprecated code is in the mod. + public DeprecationWarning(IModMetadata? mod, string nounPhrase, string version, DeprecationLevel level, ImmutableStackTrace stackTrace, bool logStackTrace) { this.Mod = mod; this.NounPhrase = nounPhrase; this.Version = version; this.Level = level; this.StackTrace = stackTrace; + this.LogStackTrace = logStackTrace; } } } -- cgit