diff options
| author | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2022-05-01 18:16:09 -0400 |
|---|---|---|
| committer | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2022-05-01 18:16:09 -0400 |
| commit | c8ad50dad1d706a1901798f9396f6becfea36c0e (patch) | |
| tree | 28bd818a5db39ec5ece1bd141a28de955950463b /src/SMAPI/Framework/Deprecations | |
| parent | 451b70953ff4c0b1b27ae0de203ad99379b45b2a (diff) | |
| parent | f78093bdb58d477b400cde3f19b70ffd6ddf833d (diff) | |
| download | SMAPI-c8ad50dad1d706a1901798f9396f6becfea36c0e.tar.gz SMAPI-c8ad50dad1d706a1901798f9396f6becfea36c0e.tar.bz2 SMAPI-c8ad50dad1d706a1901798f9396f6becfea36c0e.zip | |
Merge branch 'develop' into stable
Diffstat (limited to 'src/SMAPI/Framework/Deprecations')
4 files changed, 306 insertions, 0 deletions
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 +{ + /// <summary>Indicates how deprecated something is.</summary> + internal enum DeprecationLevel + { + /// <summary>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.</summary> + Notice, + + /// <summary>Mods should no longer be using it. Deprecation messages should be debug entries in the console.</summary> + Info, + + /// <summary>The code will be removed soon. Deprecation messages should be warnings in the console.</summary> + PendingRemoval + } +} diff --git a/src/SMAPI/Framework/Deprecations/DeprecationManager.cs b/src/SMAPI/Framework/Deprecations/DeprecationManager.cs new file mode 100644 index 00000000..288abde2 --- /dev/null +++ b/src/SMAPI/Framework/Deprecations/DeprecationManager.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; + +namespace StardewModdingAPI.Framework.Deprecations +{ + /// <summary>Manages deprecation warnings.</summary> + internal class DeprecationManager + { + /********* + ** Fields + *********/ + /// <summary>The deprecations which have already been logged (as 'mod name::noun phrase::version').</summary> + private readonly HashSet<string> LoggedDeprecations = new(StringComparer.OrdinalIgnoreCase); + + /// <summary>Encapsulates monitoring and logging for a given module.</summary> + private readonly IMonitor Monitor; + + /// <summary>Tracks the installed mods.</summary> + private readonly ModRegistry ModRegistry; + + /// <summary>The queued deprecation warnings to display.</summary> + private readonly IList<DeprecationWarning> QueuedWarnings = new List<DeprecationWarning>(); + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="monitor">Encapsulates monitoring and logging for a given module.</param> + /// <param name="modRegistry">Tracks the installed mods.</param> + public DeprecationManager(IMonitor monitor, ModRegistry modRegistry) + { + this.Monitor = monitor; + this.ModRegistry = modRegistry; + } + + /// <summary>Get a mod for the closest assembly registered as a source of deprecation warnings.</summary> + /// <returns>Returns the source name, or <c>null</c> if no registered assemblies were found.</returns> + public IModMetadata? GetModFromStack() + { + return this.ModRegistry.GetFromStack(); + } + + /// <summary>Get a mod from its unique ID.</summary> + /// <param name="modId">The mod's unique ID.</param> + public IModMetadata? GetMod(string modId) + { + return this.ModRegistry.Get(modId); + } + + /// <summary>Log a deprecation warning.</summary> + /// <param name="source">The mod which used the deprecated code, if known.</param> + /// <param name="nounPhrase">A noun phrase describing what is deprecated.</param> + /// <param name="version">The SMAPI version which deprecated it.</param> + /// <param name="severity">How deprecated the code is.</param> + /// <param name="unlessStackIncludes">A list of stack trace substrings which should suppress deprecation warnings if they appear in the stack trace.</param> + /// <param name="logStackTrace">Whether to log a stack trace showing where the deprecated code is in the mod.</param> + 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 ?? "<unknown>"}::{nounPhrase}::{version}"; + if (this.LoggedDeprecations.Contains(cacheKey)) + return; + + // warn if valid + ImmutableStackTrace stack = ImmutableStackTrace.Get(skipFrames: 1); + if (!this.ShouldSuppress(stack, unlessStackIncludes)) + { + this.LoggedDeprecations.Add(cacheKey); + this.QueuedWarnings.Add(new DeprecationWarning(source, nounPhrase, version, severity, stack, logStackTrace)); + } + } + + /// <summary>A placeholder method used to track deprecated code for which a separate warning will be shown.</summary> + /// <param name="version">The SMAPI version which deprecated it.</param> + /// <param name="severity">How deprecated the code is.</param> + public void PlaceholderWarn(string version, DeprecationLevel severity) { } + + /// <summary>Print any queued messages.</summary> + 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) + { + if (warning.LogStackTrace) + message += $"\n{this.GetSimplifiedStackTrace(warning.StackTrace, warning.Mod)}"; + this.Monitor.Log(message, level); + } + else + { + this.Monitor.Log(message, level); + if (warning.LogStackTrace) + this.Monitor.Log(this.GetSimplifiedStackTrace(warning.StackTrace, warning.Mod), LogLevel.Debug); + } + } + + this.QueuedWarnings.Clear(); + } + + + /********* + ** Private methods + *********/ + /// <summary>Get whether a deprecation warning should be suppressed.</summary> + /// <param name="stack">The stack trace for which it was raised.</param> + /// <param name="unlessStackIncludes">A list of stack trace substrings which should suppress deprecation warnings if they appear in the stack trace.</param> + private bool ShouldSuppress(ImmutableStackTrace stack, string[]? unlessStackIncludes) + { + if (unlessStackIncludes?.Any() == true) + { + string stackTrace = stack.ToString(); + foreach (string method in unlessStackIncludes) + { + if (stackTrace.Contains(method)) + return true; + } + } + + return false; + } + + /// <summary>Get the simplest stack trace which shows where in the mod the deprecated code was called from.</summary> + /// <param name="stack">The stack trace.</param> + /// <param name="mod">The mod for which to show a stack trace.</param> + private string GetSimplifiedStackTrace(ImmutableStackTrace 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<string>( + 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..5936517b --- /dev/null +++ b/src/SMAPI/Framework/Deprecations/DeprecationWarning.cs @@ -0,0 +1,51 @@ +namespace StardewModdingAPI.Framework.Deprecations +{ + /// <summary>A deprecation warning for a mod.</summary> + internal class DeprecationWarning + { + /********* + ** Accessors + *********/ + /// <summary>The affected mod.</summary> + public IModMetadata? Mod { get; } + + /// <summary>Get the display name for the affected mod.</summary> + public string ModName => this.Mod?.DisplayName ?? "<unknown mod>"; + + /// <summary>A noun phrase describing what is deprecated.</summary> + public string NounPhrase { get; } + + /// <summary>The SMAPI version which deprecated it.</summary> + public string Version { get; } + + /// <summary>The deprecation level for the affected code.</summary> + public DeprecationLevel Level { get; } + + /// <summary>The stack trace when the deprecation warning was raised.</summary> + public ImmutableStackTrace StackTrace { get; } + + /// <summary>Whether to log a stack trace showing where the deprecated code is in the mod.</summary> + public bool LogStackTrace { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="mod">The affected mod.</param> + /// <param name="nounPhrase">A noun phrase describing what is deprecated.</param> + /// <param name="version">The SMAPI version which deprecated it.</param> + /// <param name="level">The deprecation level for the affected code.</param> + /// <param name="stackTrace">The stack trace when the deprecation warning was raised.</param> + /// <param name="logStackTrace">Whether to log a stack trace showing where the deprecated code is in the mod.</param> + 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; + } + } +} 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 +{ + /// <summary>An immutable stack trace that caches its values.</summary> + internal class ImmutableStackTrace + { + /********* + ** Fields + *********/ + /// <summary>The underlying stack trace.</summary> + private readonly StackTrace StackTrace; + + /// <summary>The individual method calls in the stack trace.</summary> + private StackFrame[]? Frames; + + /// <summary>The string representation of the stack trace.</summary> + private string? StringForm; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="stackTrace">The underlying stack trace.</param> + public ImmutableStackTrace(StackTrace stackTrace) + { + this.StackTrace = stackTrace; + } + + /// <summary>Get the underlying frames.</summary> + /// <remarks>This is a reference to the underlying stack frames, so this array should not be edited.</remarks> + public StackFrame[] GetFrames() + { + return this.Frames ??= this.StackTrace.GetFrames(); + } + + /// <inheritdoc /> + public override string ToString() + { + return this.StringForm ??= this.StackTrace.ToString(); + } + + /// <summary>Get the current stack trace.</summary> + /// <param name="skipFrames">The number of frames up the stack from which to start the trace.</param> + public static ImmutableStackTrace Get(int skipFrames = 0) + { + return new ImmutableStackTrace( + new StackTrace(skipFrames: skipFrames + 1) // also skip this method + ); + } + } +} |
