summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs29
-rw-r--r--src/SMAPI/Framework/ModLoading/RewriteHelper.cs16
-rw-r--r--src/SMAPI/Framework/ModLoading/TypeReferenceComparer.cs201
-rw-r--r--src/SMAPI/Metadata/InstructionMetadata.cs2
-rw-r--r--src/SMAPI/StardewModdingAPI.csproj1
5 files changed, 226 insertions, 23 deletions
diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs
index ecad649a..cf5a3175 100644
--- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs
+++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs
@@ -1,6 +1,5 @@
using System.Collections.Generic;
using System.Linq;
-using System.Text.RegularExpressions;
using Mono.Cecil;
using Mono.Cecil.Cil;
@@ -16,9 +15,6 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
/// <summary>The assembly names to which to heuristically detect broken references.</summary>
private readonly HashSet<string> ValidateReferencesToAssemblies;
- /// <summary>A pattern matching type name substrings to strip for display.</summary>
- private readonly Regex StripTypeNamePattern = new Regex(@"`\d+(?=<)", RegexOptions.Compiled);
-
/*********
** Accessors
@@ -65,11 +61,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
return InstructionHandleResult.None;
// validate return type
- string actualReturnTypeID = this.GetComparableTypeID(targetField.FieldType);
- string expectedReturnTypeID = this.GetComparableTypeID(fieldRef.FieldType);
- if (actualReturnTypeID != expectedReturnTypeID)
+ if (!RewriteHelper.LooksLikeSameType(fieldRef.FieldType, targetField.FieldType))
{
- this.NounPhrase = $"reference to {fieldRef.DeclaringType.FullName}.{fieldRef.Name} (field returns {this.GetFriendlyTypeName(targetField.FieldType, actualReturnTypeID)}, not {this.GetFriendlyTypeName(fieldRef.FieldType, expectedReturnTypeID)})";
+ this.NounPhrase = $"reference to {fieldRef.DeclaringType.FullName}.{fieldRef.Name} (field returns {this.GetFriendlyTypeName(targetField.FieldType)}, not {this.GetFriendlyTypeName(fieldRef.FieldType)})";
return InstructionHandleResult.NotCompatible;
}
}
@@ -91,10 +85,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
return InstructionHandleResult.NotCompatible;
}
- string expectedReturnType = this.GetComparableTypeID(methodDef.ReturnType);
- if (candidateMethods.All(method => this.GetComparableTypeID(method.ReturnType) != expectedReturnType))
+ if (candidateMethods.All(method => !RewriteHelper.LooksLikeSameType(method.ReturnType, methodDef.ReturnType)))
{
- this.NounPhrase = $"reference to {methodDef.DeclaringType.FullName}.{methodDef.Name} (no such method returns {this.GetFriendlyTypeName(methodDef.ReturnType, expectedReturnType)})";
+ this.NounPhrase = $"reference to {methodDef.DeclaringType.FullName}.{methodDef.Name} (no such method returns {this.GetFriendlyTypeName(methodDef.ReturnType)})";
return InstructionHandleResult.NotCompatible;
}
}
@@ -113,17 +106,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
return type != null && this.ValidateReferencesToAssemblies.Contains(type.Scope.Name);
}
- /// <summary>Get a unique string representation of a type.</summary>
- /// <param name="type">The type reference.</param>
- private string GetComparableTypeID(TypeReference type)
- {
- return this.StripTypeNamePattern.Replace(type.FullName, "");
- }
-
/// <summary>Get a shorter type name for display.</summary>
/// <param name="type">The type reference.</param>
- /// <param name="typeID">The comparable type ID from <see cref="GetComparableTypeID"/>.</param>
- private string GetFriendlyTypeName(TypeReference type, string typeID)
+ private string GetFriendlyTypeName(TypeReference type)
{
// most common built-in types
switch (type.FullName)
@@ -140,10 +125,10 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
foreach (string @namespace in new[] { "Microsoft.Xna.Framework", "Netcode", "System", "System.Collections.Generic" })
{
if (type.Namespace == @namespace)
- return typeID.Substring(@namespace.Length + 1);
+ return type.Name;
}
- return typeID;
+ return type.FullName;
}
}
}
diff --git a/src/SMAPI/Framework/ModLoading/RewriteHelper.cs b/src/SMAPI/Framework/ModLoading/RewriteHelper.cs
index 56a60a72..2f79809c 100644
--- a/src/SMAPI/Framework/ModLoading/RewriteHelper.cs
+++ b/src/SMAPI/Framework/ModLoading/RewriteHelper.cs
@@ -10,6 +10,13 @@ namespace StardewModdingAPI.Framework.ModLoading
internal static class RewriteHelper
{
/*********
+ ** Properties
+ *********/
+ /// <summary>The comparer which heuristically compares type definitions.</summary>
+ private static readonly TypeReferenceComparer TypeDefinitionComparer = new TypeReferenceComparer();
+
+
+ /*********
** Public methods
*********/
/// <summary>Get the field reference from an instruction if it matches.</summary>
@@ -59,6 +66,15 @@ namespace StardewModdingAPI.Framework.ModLoading
return true;
}
+ /// <summary>Determine whether two type IDs look like the same type, accounting for placeholder values such as !0.</summary>
+ /// <param name="typeA">The type ID to compare.</param>
+ /// <param name="typeB">The other type ID to compare.</param>
+ /// <returns>true if the type IDs look like the same type, false if not.</returns>
+ public static bool LooksLikeSameType(TypeReference typeA, TypeReference typeB)
+ {
+ return RewriteHelper.TypeDefinitionComparer.Equals(typeA, typeB);
+ }
+
/// <summary>Get whether a method definition matches the signature expected by a method reference.</summary>
/// <param name="definition">The method definition.</param>
/// <param name="reference">The method reference.</param>
diff --git a/src/SMAPI/Framework/ModLoading/TypeReferenceComparer.cs b/src/SMAPI/Framework/ModLoading/TypeReferenceComparer.cs
new file mode 100644
index 00000000..f7497789
--- /dev/null
+++ b/src/SMAPI/Framework/ModLoading/TypeReferenceComparer.cs
@@ -0,0 +1,201 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Mono.Cecil;
+
+namespace StardewModdingAPI.Framework.ModLoading
+{
+ /// <summary>Performs heuristic equality checks for <see cref="TypeReference"/> instances.</summary>
+ /// <remarks>
+ /// This implementation compares <see cref="TypeReference"/> instances to see if they likely
+ /// refer to the same type. While the implementation is obvious for types like <c>System.Bool</c>,
+ /// this class mainly exists to handle cases like <c>System.Collections.Generic.Dictionary`2&lt;!0,Netcode.NetRoot`1&lt;!1&gt;&gt;</c>
+ /// and <c>System.Collections.Generic.Dictionary`2&lt;TKey,Netcode.NetRoot`1&lt;TValue&gt;&gt;</c>
+ /// which are compatible, but not directly comparable. It does this by splitting each type name
+ /// into its component token types, and performing placeholder substitution (e.g. <c>!0</c> to
+ /// <c>TKey</c> in the above example). If all components are equal after substitution, and the
+ /// tokens can all be mapped to the same generic type, the types are considered equal.
+ /// </remarks>
+ internal class TypeReferenceComparer : IEqualityComparer<TypeReference>
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Get whether the specified objects are equal.</summary>
+ /// <param name="a">The first object to compare.</param>
+ /// <param name="b">The second object to compare.</param>
+ public bool Equals(TypeReference a, TypeReference b)
+ {
+ if (a == null || b == null)
+ return a == b;
+
+ return
+ a == b
+ || a.FullName == b.FullName
+ || this.HeuristicallyEquals(a, b);
+ }
+
+ /// <summary>Get a hash code for the specified object.</summary>
+ /// <param name="obj">The object for which a hash code is to be returned.</param>
+ /// <exception cref="T:System.ArgumentNullException">The object type is a reference type and <paramref name="obj" /> is null.</exception>
+ public int GetHashCode(TypeReference obj)
+ {
+ return obj.GetHashCode();
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Get whether two types are heuristically equal based on generic type token substitution.</summary>
+ /// <param name="typeA">The first type to compare.</param>
+ /// <param name="typeB">The second type to compare.</param>
+ private bool HeuristicallyEquals(TypeReference typeA, TypeReference typeB)
+ {
+ bool HeuristicallyEquals(string typeNameA, string typeNameB, IDictionary<string, string> tokenMap)
+ {
+ // analyse type names
+ bool hasTokensA = typeNameA.Contains("!");
+ bool hasTokensB = typeNameB.Contains("!");
+ bool isTokenA = hasTokensA && typeNameA[0] == '!';
+ bool isTokenB = hasTokensB && typeNameB[0] == '!';
+
+ // validate
+ if (!hasTokensA && !hasTokensB)
+ return typeNameA == typeNameB; // no substitution needed
+ if (hasTokensA && hasTokensB)
+ throw new InvalidOperationException("Can't compare two type names when both contain generic type tokens.");
+
+ // perform substitution if applicable
+ if (isTokenA)
+ typeNameA = this.MapPlaceholder(placeholder: typeNameA, type: typeNameB, map: tokenMap);
+ if (isTokenB)
+ typeNameB = this.MapPlaceholder(placeholder: typeNameB, type: typeNameA, map: tokenMap);
+
+ // compare inner tokens
+ string[] symbolsA = this.GetTypeSymbols(typeNameA).ToArray();
+ string[] symbolsB = this.GetTypeSymbols(typeNameB).ToArray();
+ if (symbolsA.Length != symbolsB.Length)
+ return false;
+
+ for (int i = 0; i < symbolsA.Length; i++)
+ {
+ if (!HeuristicallyEquals(symbolsA[i], symbolsB[i], tokenMap))
+ return false;
+ }
+
+ return true;
+ }
+
+ return HeuristicallyEquals(typeA.FullName, typeB.FullName, new Dictionary<string, string>());
+ }
+
+ /// <summary>Map a generic type placeholder (like <c>!0</c>) to its actual type.</summary>
+ /// <param name="placeholder">The token placeholder.</param>
+ /// <param name="type">The actual type.</param>
+ /// <param name="map">The map of token to map substitutions.</param>
+ /// <returns>Returns the previously-mapped type if applicable, else the <paramref name="type"/>.</returns>
+ private string MapPlaceholder(string placeholder, string type, IDictionary<string, string> map)
+ {
+ if (map.TryGetValue(placeholder, out string result))
+ return result;
+
+ map[placeholder] = type;
+ return type;
+ }
+
+ /// <summary>Get the top-level type symbols in a type name (e.g. <code>List</code> and <code>NetRef&lt;T&gt;</code> in <code>List&lt;NetRef&lt;T&gt;&gt;</code>)</summary>
+ /// <param name="typeName">The full type name.</param>
+ private IEnumerable<string> GetTypeSymbols(string typeName)
+ {
+ int openGenerics = 0;
+
+ Queue<char> queue = new Queue<char>(typeName);
+ string symbol = "";
+ while (queue.Any())
+ {
+ char ch = queue.Dequeue();
+ switch (ch)
+ {
+ // skip `1 generic type identifiers
+ case '`':
+ while (int.TryParse(queue.Peek().ToString(), out int _))
+ queue.Dequeue();
+ break;
+
+ // start generic args
+ case '<':
+ switch (openGenerics)
+ {
+ // start new generic symbol
+ case 0:
+ yield return symbol;
+ symbol = "";
+ openGenerics++;
+ break;
+
+ // continue accumulating nested type symbol
+ default:
+ symbol += ch;
+ openGenerics++;
+ break;
+ }
+ break;
+
+ // generic args delimiter
+ case ',':
+ switch (openGenerics)
+ {
+ // invalid
+ case 0:
+ throw new InvalidOperationException($"Encountered unexpected comma in type name: {typeName}.");
+
+ // start next generic symbol
+ case 1:
+ yield return symbol;
+ symbol = "";
+ break;
+
+ // continue accumulating nested type symbol
+ default:
+ symbol += ch;
+ break;
+ }
+ break;
+
+
+ // end generic args
+ case '>':
+ switch (openGenerics)
+ {
+ // invalid
+ case 0:
+ throw new InvalidOperationException($"Encountered unexpected closing generic in type name: {typeName}.");
+
+ // end generic symbol
+ case 1:
+ yield return symbol;
+ symbol = "";
+ openGenerics--;
+ break;
+
+ // continue accumulating nested type symbol
+ default:
+ symbol += ch;
+ openGenerics--;
+ break;
+ }
+ break;
+
+ // continue symbol
+ default:
+ symbol += ch;
+ break;
+ }
+ }
+
+ if (symbol != "")
+ yield return symbol;
+ }
+ }
+}
diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs
index aa3e743c..2f0c1b15 100644
--- a/src/SMAPI/Metadata/InstructionMetadata.cs
+++ b/src/SMAPI/Metadata/InstructionMetadata.cs
@@ -17,7 +17,7 @@ namespace StardewModdingAPI.Metadata
*********/
/// <summary>The assembly names to which to heuristically detect broken references.</summary>
/// <remarks>The current implementation only works correctly with assemblies that should always be present.</remarks>
- private readonly string[] ValidateReferencesToAssemblies = { "StardewModdingAPI", "Stardew Valley", "StardewValley" };
+ private readonly string[] ValidateReferencesToAssemblies = { "StardewModdingAPI", "Stardew Valley", "StardewValley", "Netcode" };
/*********
diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj
index c13f5e30..57c2c9e8 100644
--- a/src/SMAPI/StardewModdingAPI.csproj
+++ b/src/SMAPI/StardewModdingAPI.csproj
@@ -110,6 +110,7 @@
<Compile Include="Framework\ContentManagers\IContentManager.cs" />
<Compile Include="Framework\ContentManagers\ModContentManager.cs" />
<Compile Include="Framework\Models\ModFolderExport.cs" />
+ <Compile Include="Framework\ModLoading\TypeReferenceComparer.cs" />
<Compile Include="Framework\Patching\GamePatcher.cs" />
<Compile Include="Framework\Patching\IHarmonyPatch.cs" />
<Compile Include="Framework\Serialisation\ColorConverter.cs" />