add deprecation warnings (#165)

This commit is contained in:
Jesse Plamondon-Willard 2016-11-05 16:20:31 -04:00
parent 0749fdcbe5
commit 8d8b640779
8 changed files with 213 additions and 10 deletions

View File

@ -6,7 +6,7 @@
<s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForBuiltInTypes/@EntryValue">UseVarWhenEvident</s:String>
<s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForSimpleTypes/@EntryValue">UseExplicitType</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=ID/@EntryIndexedValue">ID</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateInstanceFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateInstanceFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"&gt;&lt;ExtraRule Prefix="_" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAddAccessorOwnerDeclarationBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>

View File

@ -99,6 +99,17 @@ namespace StardewModdingAPI
return this as T;
}
}
/*********
** Protected methods
*********/
/// <summary>Construct an instance.</summary>
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
}
}
/// <summary>Provides extension methods for <see cref="Config"/> classes.</summary>

View File

@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
namespace StardewModdingAPI.Framework
{
/// <summary>Manages deprecation warnings.</summary>
internal class DeprecationManager
{
/*********
** Properties
*********/
/// <summary>The friendly mod names treated as deprecation warning sources (assembly full name => mod name).</summary>
private readonly IDictionary<string, string> ModNamesByAssembly = new Dictionary<string, string>();
/// <summary>The deprecations which have already been logged (as 'mod name::noun phrase::version').</summary>
private readonly HashSet<string> LoggedDeprecations = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
/*********
** Public methods
*********/
/// <summary>Register a mod as a possible source of deprecation warnings.</summary>
/// <param name="assembly">The mod assembly.</param>
/// <param name="name">The mod's friendly name.</param>
public void AddMod(Assembly assembly, string name)
{
this.ModNamesByAssembly[assembly.FullName] = name;
}
/// <summary>Log a deprecation warning.</summary>
/// <param name="nounPhrase">A noun phrase describing what is deprecated.</param>
/// <param name="version">The SMAPI version which deprecated it.</param>
public void Warn(string nounPhrase, string version)
{
this.Warn(this.GetSourceNameFromStack(), nounPhrase, version);
}
/// <summary>Log a deprecation warning.</summary>
/// <param name="source">The friendly mod name which used the deprecated code.</param>
/// <param name="nounPhrase">A noun phrase describing what is deprecated.</param>
/// <param name="version">The SMAPI version which deprecated it.</param>
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}"
);
}
/// <summary>Mark a deprecation warning as already logged.</summary>
/// <param name="nounPhrase">A noun phrase describing what is deprecated (e.g. "the Extensions.AsInt32 method").</param>
/// <param name="version">The SMAPI version which deprecated it.</param>
/// <returns>Returns whether the deprecation was successfully marked as warned. Returns <c>false</c> if it was already marked.</returns>
public bool MarkWarned(string nounPhrase, string version)
{
return this.MarkWarned(this.GetSourceNameFromStack(), nounPhrase, version);
}
/// <summary>Mark a deprecation warning as already logged.</summary>
/// <param name="source">The friendly name of the assembly which used the deprecated code.</param>
/// <param name="nounPhrase">A noun phrase describing what is deprecated (e.g. "the Extensions.AsInt32 method").</param>
/// <param name="version">The SMAPI version which deprecated it.</param>
/// <returns>Returns whether the deprecation was successfully marked as warned. Returns <c>false</c> if it was already marked.</returns>
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;
}
/// <summary>Get whether a type implements the given virtual method.</summary>
/// <param name="subtype">The type to check.</param>
/// <param name="baseType">The base type which declares the virtual method.</param>
/// <param name="name">The method name.</param>
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
*********/
/// <summary>Get the friendly name 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>
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;
}
}
}

View File

@ -9,6 +9,10 @@ namespace StardewModdingAPI
/*********
** Accessors
*********/
/// <summary>Whether the manifest defined the deprecated <see cref="Authour"/> field.</summary>
[JsonIgnore]
internal bool UsedAuthourField { get; private set; }
/// <summary>The mod name.</summary>
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;
}
}
/// <summary>The mod version.</summary>

View File

@ -6,6 +6,12 @@ namespace StardewModdingAPI
/// <summary>The base class for a mod.</summary>
public class Mod
{
/*********
** Properties
*********/
/// <summary>The backing field for <see cref="Mod.PathOnDisk"/>.</summary>
private string _pathOnDisk;
/*********
** Accessors
*********/
@ -16,16 +22,44 @@ namespace StardewModdingAPI
public Manifest Manifest { get; internal set; }
/// <summary>The full path to the mod's directory on the disk.</summary>
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; }
}
/// <summary>The full path to the mod's <c>config.json</c> file on the disk.</summary>
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");
}
}
/// <summary>The full path to the per-save configs folder (if <see cref="StardewModdingAPI.Manifest.PerSaveConfigs"/> is <c>true</c>).</summary>
[Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(ModHelper.ReadJsonFile) + " instead")]
public string PerSaveConfigFolder => this.GetPerSaveConfigFolder();
/// <summary>The full path to the per-save configuration file for the current save (if <see cref="StardewModdingAPI.Manifest.PerSaveConfigs"/> is <c>true</c>).</summary>
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
/// <summary>Get the full path to the per-save configuration file for the current save (if <see cref="StardewModdingAPI.Manifest.PerSaveConfigs"/> is <c>true</c>).</summary>
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");

View File

@ -54,6 +54,8 @@ namespace StardewModdingAPI
/// <summary>The game's build type (i.e. GOG vs Steam).</summary>
public static int BuildType => (int)Program.StardewProgramType.GetField("buildType", BindingFlags.Public | BindingFlags.Static).GetValue(null);
/// <summary>Manages deprecation warnings.</summary>
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

View File

@ -195,6 +195,7 @@
<Compile Include="Events\MineEvents.cs" />
<Compile Include="Events\PlayerEvents.cs" />
<Compile Include="Events\TimeEvents.cs" />
<Compile Include="Framework\DeprecationManager.cs" />
<Compile Include="Framework\UpdateHelper.cs" />
<Compile Include="Framework\GitRelease.cs" />
<Compile Include="Inheritance\ChangeType.cs" />

View File

@ -32,8 +32,15 @@ namespace StardewModdingAPI
/// <summary>Obsolete.</summary>
[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;