rewrite TypeReference comparison to handle more edge cases, exit earlier if possible, and encapsulate a bit more
This commit is contained in:
parent
0079110870
commit
530b120014
|
@ -1,21 +1,23 @@
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text.RegularExpressions;
|
using System.Linq;
|
||||||
using Mono.Cecil;
|
using Mono.Cecil;
|
||||||
|
|
||||||
namespace StardewModdingAPI.Framework.ModLoading
|
namespace StardewModdingAPI.Framework.ModLoading
|
||||||
{
|
{
|
||||||
/// <summary>Performs heuristic equality checks for <see cref="TypeReference"/> instances.</summary>
|
/// <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<!0,Netcode.NetRoot`1<!1>></c>
|
||||||
|
/// and <c>System.Collections.Generic.Dictionary`2<TKey,Netcode.NetRoot`1<TValue>></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>
|
internal class TypeReferenceComparer : IEqualityComparer<TypeReference>
|
||||||
{
|
{
|
||||||
/*********
|
|
||||||
** Properties
|
|
||||||
*********/
|
|
||||||
/// <summary>A pattern matching type name substrings to strip for display.</summary>
|
|
||||||
private readonly Regex StripTypeNamePattern = new Regex(@"`\d+(?=<)", RegexOptions.Compiled);
|
|
||||||
|
|
||||||
private List<char> symbolBoundaries = new List<char> { '<', '>', ',' };
|
|
||||||
|
|
||||||
|
|
||||||
/*********
|
/*********
|
||||||
** Public methods
|
** Public methods
|
||||||
*********/
|
*********/
|
||||||
|
@ -24,25 +26,13 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||||
/// <param name="b">The second object to compare.</param>
|
/// <param name="b">The second object to compare.</param>
|
||||||
public bool Equals(TypeReference a, TypeReference b)
|
public bool Equals(TypeReference a, TypeReference b)
|
||||||
{
|
{
|
||||||
string typeA = this.GetComparableTypeID(a);
|
if (a == null || b == null)
|
||||||
string typeB = this.GetComparableTypeID(b);
|
return a == b;
|
||||||
|
|
||||||
string placeholderType = "", actualType = "";
|
return
|
||||||
|
a == b
|
||||||
if (this.HasPlaceholder(typeA))
|
|| a.FullName == b.FullName
|
||||||
{
|
|| this.HeuristicallyEquals(a, b);
|
||||||
placeholderType = typeA;
|
|
||||||
actualType = typeB;
|
|
||||||
}
|
|
||||||
else if (this.HasPlaceholder(typeB))
|
|
||||||
{
|
|
||||||
placeholderType = typeB;
|
|
||||||
actualType = typeA;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
return typeA == typeB;
|
|
||||||
|
|
||||||
return this.PlaceholderTypeValidates(placeholderType, actualType);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Get a hash code for the specified object.</summary>
|
/// <summary>Get a hash code for the specified object.</summary>
|
||||||
|
@ -57,153 +47,155 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||||
/*********
|
/*********
|
||||||
** Private methods
|
** Private methods
|
||||||
*********/
|
*********/
|
||||||
/// <summary>Get a unique string representation of a type.</summary>
|
/// <summary>Get whether two types are heuristically equal based on generic type token substitution.</summary>
|
||||||
/// <param name="type">The type reference.</param>
|
/// <param name="typeA">The first type to compare.</param>
|
||||||
private string GetComparableTypeID(TypeReference type)
|
/// <param name="typeB">The second type to compare.</param>
|
||||||
|
private bool HeuristicallyEquals(TypeReference typeA, TypeReference typeB)
|
||||||
{
|
{
|
||||||
return this.StripTypeNamePattern.Replace(type.FullName, "");
|
bool HeuristicallyEquals(string typeNameA, string typeNameB, IDictionary<string, string> tokenMap)
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Determine whether this type ID has a placeholder such as !0.</summary>
|
|
||||||
/// <param name="typeID">The type to check.</param>
|
|
||||||
/// <returns>true if the type ID contains a placeholder, false if not.</returns>
|
|
||||||
private bool HasPlaceholder(string typeID)
|
|
||||||
{
|
|
||||||
return typeID.Contains("!0");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary> returns whether this type ID is a placeholder, i.e., it begins with "!".</summary>
|
|
||||||
/// <param name="symbol">The symbol to validate.</param>
|
|
||||||
/// <returns>true if the symbol is a placeholder, false if not</returns>
|
|
||||||
private bool IsPlaceholder(string symbol)
|
|
||||||
{
|
|
||||||
return symbol.StartsWith("!");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary> Traverses and parses out symbols from a type which does not contain placeholder values.</summary>
|
|
||||||
/// <param name="type">The type to traverse.</param>
|
|
||||||
/// <param name="typeSymbols">A List in which to store the parsed symbols.</param>
|
|
||||||
private void TraverseActualType(string type, List<SymbolLocation> typeSymbols)
|
|
||||||
{
|
|
||||||
int depth = 0;
|
|
||||||
string symbol = "";
|
|
||||||
|
|
||||||
foreach (char c in type)
|
|
||||||
{
|
{
|
||||||
if (this.symbolBoundaries.Contains(c))
|
// 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++)
|
||||||
{
|
{
|
||||||
typeSymbols.Add(new SymbolLocation(symbol, depth));
|
if (!HeuristicallyEquals(symbolsA[i], symbolsB[i], tokenMap))
|
||||||
symbol = "";
|
|
||||||
switch (c)
|
|
||||||
{
|
|
||||||
case '<':
|
|
||||||
depth++;
|
|
||||||
break;
|
|
||||||
case '>':
|
|
||||||
depth--;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
symbol += c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary> Determines whether two symbols in a type ID match, accounting for placeholders such as !0.</summary>
|
|
||||||
/// <param name="symbolA">A symbol in a typename which contains placeholders.</param>
|
|
||||||
/// <param name="symbolB">A symbol in a typename which does not contain placeholders.</param>
|
|
||||||
/// <param name="placeholderMap">A dictionary containing a mapping of placeholders to concrete types.</param>
|
|
||||||
/// <returns>true if the symbols match, false if not.</returns>
|
|
||||||
private bool SymbolsMatch(SymbolLocation symbolA, SymbolLocation symbolB, Dictionary<string, string> placeholderMap)
|
|
||||||
{
|
|
||||||
if (symbolA.depth != symbolB.depth)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (!this.IsPlaceholder(symbolA.symbol))
|
|
||||||
{
|
|
||||||
return symbolA.symbol == symbolB.symbol;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (placeholderMap.ContainsKey(symbolA.symbol))
|
|
||||||
{
|
|
||||||
return placeholderMap[symbolA.symbol] == symbolB.symbol;
|
|
||||||
}
|
|
||||||
|
|
||||||
placeholderMap[symbolA.symbol] = symbolB.symbol;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary> Determines whether a type which has placeholders correctly resolves to the concrete type provided. </summary>
|
|
||||||
/// <param name="type">A type containing placeholders such as !0.</param>
|
|
||||||
/// <param name="typeSymbols">The list of symbols extracted from the concrete type.</param>
|
|
||||||
/// <returns>true if the type resolves correctly, false if not.</returns>
|
|
||||||
private bool PlaceholderTypeResolvesToActualType(string type, List<SymbolLocation> typeSymbols)
|
|
||||||
{
|
|
||||||
Dictionary<string, string> placeholderMap = new Dictionary<string, string>();
|
|
||||||
|
|
||||||
int depth = 0, symbolCount = 0;
|
|
||||||
string symbol = "";
|
|
||||||
|
|
||||||
foreach (char c in type)
|
|
||||||
{
|
|
||||||
if (this.symbolBoundaries.Contains(c))
|
|
||||||
{
|
|
||||||
bool match = this.SymbolsMatch(new SymbolLocation(symbol, depth), typeSymbols[symbolCount], placeholderMap);
|
|
||||||
if (typeSymbols.Count <= symbolCount ||
|
|
||||||
!match)
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
symbolCount++;
|
|
||||||
symbol = "";
|
|
||||||
switch (c)
|
|
||||||
{
|
|
||||||
case '<':
|
|
||||||
depth++;
|
|
||||||
break;
|
|
||||||
case '>':
|
|
||||||
depth--;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
|
||||||
symbol += c;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return HeuristicallyEquals(typeA.FullName, typeB.FullName, new Dictionary<string, string>());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Determines whether a type with placeholders in it matches a type without placeholders.</summary>
|
/// <summary>Map a generic type placeholder (like <c>!0</c>) to its actual type.</summary>
|
||||||
/// <param name="placeholderType">The type with placeholders in it.</param>
|
/// <param name="placeholder">The token placeholder.</param>
|
||||||
/// <param name="actualType">The type without placeholders.</param>
|
/// <param name="type">The actual type.</param>
|
||||||
/// <returns>true if the placeholder type can resolve to the actual type, false if not.</returns>
|
/// <param name="map">The map of token to map substitutions.</param>
|
||||||
private bool PlaceholderTypeValidates(string placeholderType, string actualType)
|
/// <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)
|
||||||
{
|
{
|
||||||
List<SymbolLocation> typeSymbols = new List<SymbolLocation>();
|
if (map.TryGetValue(placeholder, out string result))
|
||||||
|
return result;
|
||||||
|
|
||||||
this.TraverseActualType(actualType, typeSymbols);
|
map[placeholder] = type;
|
||||||
return PlaceholderTypeResolvesToActualType(placeholderType, typeSymbols);
|
return type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Get the top-level type symbols in a type name (e.g. <code>List</code> and <code>NetRef<T></code> in <code>List<NetRef<T>></code>)</summary>
|
||||||
|
/// <param name="typeName">The full type name.</param>
|
||||||
/*********
|
private IEnumerable<string> GetTypeSymbols(string typeName)
|
||||||
** Inner classes
|
|
||||||
*********/
|
|
||||||
protected class SymbolLocation
|
|
||||||
{
|
{
|
||||||
public string symbol;
|
int openGenerics = 0;
|
||||||
public int depth;
|
|
||||||
|
|
||||||
public SymbolLocation(string symbol, int depth)
|
Queue<char> queue = new Queue<char>(typeName);
|
||||||
|
string symbol = "";
|
||||||
|
while (queue.Any())
|
||||||
{
|
{
|
||||||
this.symbol = symbol;
|
char ch = queue.Dequeue();
|
||||||
this.depth = depth;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue