// MIT License - Copyright (c) 2026 wallstop
// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE
namespace WallstopStudios.UnityHelpers.Utils
{
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
///
/// Utility class for resolving and matching types, with special support for generic types.
///
///
///
/// This class provides methods for parsing type name strings (including simplified generic syntax),
/// resolving them to instances, and matching concrete types against patterns
/// including open generic types.
///
///
/// Supported type name formats:
///
/// - System.Collections.Generic.List`1 - Open generic type definition
/// - System.Collections.Generic.List`1[[System.Int32]] - Closed generic type
/// - List<int> - Simplified closed generic syntax
/// - List<> - Simplified open generic syntax
/// - Dictionary<string, int> - Multiple type arguments
/// - Dictionary<,> - Open with multiple type arguments
/// - List<List<int>> - Nested generics
///
///
///
public static class PoolTypeResolver
{
private static readonly Dictionary SimplifiedTypeNameCache = new();
private static readonly object CacheLock = new();
private static readonly Dictionary BuiltInTypeAliases = new(
StringComparer.OrdinalIgnoreCase
)
{
// C# keywords (lowercase) - case-insensitive lookup handles PascalCase variants
{ "int", typeof(int) },
{ "uint", typeof(uint) },
{ "long", typeof(long) },
{ "ulong", typeof(ulong) },
{ "short", typeof(short) },
{ "ushort", typeof(ushort) },
{ "byte", typeof(byte) },
{ "sbyte", typeof(sbyte) },
{ "float", typeof(float) },
{ "double", typeof(double) },
{ "decimal", typeof(decimal) },
{ "bool", typeof(bool) },
{ "char", typeof(char) },
{ "string", typeof(string) },
{ "object", typeof(object) },
{ "void", typeof(void) },
// .NET type names that differ from C# keywords (case-insensitive, so no duplicates)
{ "Int32", typeof(int) },
{ "UInt32", typeof(uint) },
{ "Int64", typeof(long) },
{ "UInt64", typeof(ulong) },
{ "Int16", typeof(short) },
{ "UInt16", typeof(ushort) },
{ "Single", typeof(float) },
{ "Boolean", typeof(bool) },
};
private static readonly Dictionary CommonGenericTypes = new(
StringComparer.Ordinal
)
{
{ "List", typeof(List<>) },
{ "Dictionary", typeof(Dictionary<,>) },
{ "HashSet", typeof(HashSet<>) },
{ "Queue", typeof(Queue<>) },
{ "Stack", typeof(Stack<>) },
{ "LinkedList", typeof(LinkedList<>) },
{ "SortedList", typeof(SortedList<,>) },
{ "SortedDictionary", typeof(SortedDictionary<,>) },
{ "SortedSet", typeof(SortedSet<>) },
{ "KeyValuePair", typeof(KeyValuePair<,>) },
{ "Tuple", typeof(Tuple<>) },
{ "Nullable", typeof(Nullable<>) },
{ "IEnumerable", typeof(IEnumerable<>) },
{ "ICollection", typeof(ICollection<>) },
{ "IList", typeof(IList<>) },
{ "IDictionary", typeof(IDictionary<,>) },
{ "ISet", typeof(ISet<>) },
{ "IReadOnlyList", typeof(IReadOnlyList<>) },
{ "IReadOnlyCollection", typeof(IReadOnlyCollection<>) },
{ "IReadOnlyDictionary", typeof(IReadOnlyDictionary<,>) },
};
///
/// Parses a type name string and resolves it to a .
///
///
/// The type name to resolve. Supports assembly-qualified names,
/// open/closed generic CLR syntax, and simplified C# generic syntax.
///
///
/// The resolved , or null if the type could not be resolved.
///
public static Type ResolveType(string typeName)
{
if (string.IsNullOrWhiteSpace(typeName))
{
return null;
}
string trimmed = typeName.Trim();
// Check cache first
lock (CacheLock)
{
if (SimplifiedTypeNameCache.TryGetValue(trimmed, out Type cached))
{
return cached;
}
}
Type resolved = ResolveTypeInternal(trimmed);
if (resolved != null)
{
lock (CacheLock)
{
SimplifiedTypeNameCache[trimmed] = resolved;
}
}
return resolved;
}
///
/// Checks if a concrete type matches a pattern type (including open generics).
///
/// The concrete type to check.
///
/// The pattern type to match against. Can be an exact type or an open generic definition.
///
///
/// true if the concrete type matches the pattern; otherwise, false.
///
///
///
/// Matching rules:
///
/// - Exact type match always succeeds
/// - Open generic definition matches any closed generic of the same type
/// (e.g., List<> matches List<int>)
/// - For nested generics like List<List<int>>,
/// patterns like List<List<>> match any inner type argument
///
///
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool TypeMatchesPattern(Type concreteType, Type patternType)
{
if (concreteType == null || patternType == null)
{
return false;
}
// Exact match
if (concreteType == patternType)
{
return true;
}
// If pattern is open generic definition
if (patternType.IsGenericTypeDefinition)
{
if (!concreteType.IsGenericType)
{
return false;
}
Type concreteGenericDef = concreteType.IsGenericTypeDefinition
? concreteType
: concreteType.GetGenericTypeDefinition();
return concreteGenericDef == patternType;
}
// If pattern is a partially open generic (e.g., List>)
// This is represented as a closed generic where some type arguments are open
if (patternType.IsGenericType && !patternType.IsGenericTypeDefinition)
{
if (!concreteType.IsGenericType)
{
return false;
}
Type patternGenericDef = patternType.GetGenericTypeDefinition();
Type concreteGenericDef = concreteType.GetGenericTypeDefinition();
if (patternGenericDef != concreteGenericDef)
{
return false;
}
Type[] patternArgs = patternType.GetGenericArguments();
Type[] concreteArgs = concreteType.GetGenericArguments();
if (patternArgs.Length != concreteArgs.Length)
{
return false;
}
for (int i = 0; i < patternArgs.Length; i++)
{
Type patternArg = patternArgs[i];
Type concreteArg = concreteArgs[i];
// If pattern argument is a generic parameter (open), it matches anything
if (patternArg.IsGenericParameter)
{
continue;
}
// Recursively check nested type arguments
if (!TypeMatchesPattern(concreteArg, patternArg))
{
return false;
}
}
return true;
}
return false;
}
///
/// Checks if a concrete type matches a pattern specified as a type name string.
///
/// The concrete type to check.
///
/// The pattern type name to match against. Supports simplified and full CLR syntax.
///
///
/// true if the concrete type matches the pattern; otherwise, false.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool TypeMatchesPattern(Type concreteType, string patternTypeName)
{
if (concreteType == null || string.IsNullOrWhiteSpace(patternTypeName))
{
return false;
}
Type patternType = ResolveType(patternTypeName);
if (patternType == null)
{
return false;
}
return TypeMatchesPattern(concreteType, patternType);
}
///
/// Gets the generic type definition if the type is generic, otherwise returns the type itself.
///
/// The type to process.
///
/// The generic type definition if is a generic type;
/// otherwise, itself.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Type GetGenericPattern(Type type)
{
if (type == null)
{
return null;
}
if (type.IsGenericType && !type.IsGenericTypeDefinition)
{
return type.GetGenericTypeDefinition();
}
return type;
}
///
/// Gets all matching patterns for a type in order of specificity (most specific first).
///
/// The type to get patterns for.
///
/// An enumerable of types representing matching patterns in order of decreasing specificity:
///
/// - Exact type
/// - Inner generic open patterns (for nested generics)
/// - Outer generic open pattern
///
///
///
///
/// For List<List<int>>, this returns:
///
/// - List<List<int>> (exact)
/// - List<List<>> (inner open)
/// - List<> (outer open)
///
///
///
public static IEnumerable GetAllMatchingPatterns(Type type)
{
if (type == null)
{
yield break;
}
// First: exact type
yield return type;
if (!type.IsGenericType || type.IsGenericTypeDefinition)
{
yield break;
}
// For nested generics, get intermediate patterns
Type[] genericArgs = type.GetGenericArguments();
Type genericDef = type.GetGenericTypeDefinition();
// Check if any type arguments are themselves generic types
bool hasNestedGenerics = false;
for (int i = 0; i < genericArgs.Length; i++)
{
if (genericArgs[i].IsGenericType && !genericArgs[i].IsGenericTypeDefinition)
{
hasNestedGenerics = true;
break;
}
}
if (hasNestedGenerics)
{
// Build intermediate patterns with inner generics opened
Type[] openedArgs = new Type[genericArgs.Length];
for (int i = 0; i < genericArgs.Length; i++)
{
Type arg = genericArgs[i];
if (arg.IsGenericType && !arg.IsGenericTypeDefinition)
{
openedArgs[i] = arg.GetGenericTypeDefinition();
}
else
{
openedArgs[i] = arg;
}
}
// Try to construct the partially open type
Type partiallyOpen = TryMakeGenericType(genericDef, openedArgs);
if (partiallyOpen != null && partiallyOpen != type)
{
yield return partiallyOpen;
}
}
// Last: fully open generic definition
yield return genericDef;
}
///
/// Gets the match priority for a pattern type when matching against a concrete type.
/// Lower values indicate higher priority (more specific match).
///
/// The concrete type being matched.
/// The pattern type to get priority for.
///
/// A priority value where:
///
/// - 0 = exact match
/// - 1 = partially open generic (inner args open)
/// - 2 = fully open generic definition
/// - = no match
///
///
public static int GetMatchPriority(Type concreteType, Type patternType)
{
if (concreteType == null || patternType == null)
{
return int.MaxValue;
}
// Exact match has highest priority
if (concreteType == patternType)
{
return 0;
}
if (!TypeMatchesPattern(concreteType, patternType))
{
return int.MaxValue;
}
// Open generic definition has lowest priority among matches
if (patternType.IsGenericTypeDefinition)
{
return 2;
}
// Partially open generic (contains open type arguments)
if (patternType.IsGenericType && ContainsOpenTypeArguments(patternType))
{
return 1;
}
// Exact match (should have been caught above, but just in case)
return 0;
}
///
/// Clears the internal type resolution cache.
///
public static void ClearCache()
{
lock (CacheLock)
{
SimplifiedTypeNameCache.Clear();
}
}
private static bool ContainsOpenTypeArguments(Type type)
{
if (!type.IsGenericType)
{
return false;
}
Type[] args = type.GetGenericArguments();
for (int i = 0; i < args.Length; i++)
{
Type arg = args[i];
if (arg.IsGenericParameter)
{
return true;
}
if (arg.IsGenericType && ContainsOpenTypeArguments(arg))
{
return true;
}
}
return false;
}
private static Type TryMakeGenericType(Type genericDef, Type[] typeArgs)
{
try
{
return genericDef.MakeGenericType(typeArgs);
}
catch
{
return null;
}
}
private static Type ResolveTypeInternal(string typeName)
{
// Try direct Type.GetType first (handles assembly-qualified names)
Type directResolve = Type.GetType(typeName, throwOnError: false);
if (directResolve != null)
{
return directResolve;
}
// Check if it's a built-in type alias
if (BuiltInTypeAliases.TryGetValue(typeName, out Type aliasType))
{
return aliasType;
}
// Check for simplified generic syntax (contains < and >)
if (typeName.Contains("<"))
{
return ParseSimplifiedGeneric(typeName);
}
// Check if it ends with arity marker (e.g., List`1)
if (typeName.Contains("`"))
{
return ResolveBySearchingAssemblies(typeName);
}
// Try searching all loaded assemblies for the type name
return ResolveBySearchingAssemblies(typeName);
}
private static Type ParseSimplifiedGeneric(string typeName)
{
// Parse generic syntax like "List" or "Dictionary>"
int angleBracketIndex = typeName.IndexOf('<');
if (angleBracketIndex < 0)
{
return null;
}
string genericTypeName = typeName.Substring(0, angleBracketIndex).Trim();
string argsSection = typeName.Substring(
angleBracketIndex + 1,
typeName.Length - angleBracketIndex - 2
);
// Check for open generic syntax (e.g., "List<>" or "Dictionary<,>")
if (IsOpenGenericArgs(argsSection))
{
int argCount = CountOpenGenericArgs(argsSection);
return ResolveOpenGenericType(genericTypeName, argCount);
}
// Parse type arguments
using PooledResource> lease = Buffers.List.Get(
out List typeArgStrings
);
ParseGenericArguments(argsSection, typeArgStrings);
if (typeArgStrings.Count == 0)
{
return null;
}
// Resolve the generic type definition
Type genericDef = ResolveOpenGenericType(genericTypeName, typeArgStrings.Count);
if (genericDef == null)
{
return null;
}
// Resolve each type argument
Type[] typeArgs = new Type[typeArgStrings.Count];
for (int i = 0; i < typeArgStrings.Count; i++)
{
Type argType = ResolveType(typeArgStrings[i]);
if (argType == null)
{
return null;
}
typeArgs[i] = argType;
}
// Construct the closed generic type
return TryMakeGenericType(genericDef, typeArgs);
}
private static bool IsOpenGenericArgs(string argsSection)
{
string trimmed = argsSection.Trim();
if (string.IsNullOrEmpty(trimmed))
{
return true;
}
// Check if it's just commas (e.g., "," for Dictionary<,>)
for (int i = 0; i < trimmed.Length; i++)
{
char c = trimmed[i];
if (c != ',' && !char.IsWhiteSpace(c))
{
return false;
}
}
return true;
}
private static int CountOpenGenericArgs(string argsSection)
{
if (string.IsNullOrEmpty(argsSection.Trim()))
{
return 1;
}
int count = 1;
for (int i = 0; i < argsSection.Length; i++)
{
if (argsSection[i] == ',')
{
count++;
}
}
return count;
}
private static Type ResolveOpenGenericType(string typeName, int arity)
{
// Check common generic types first
if (CommonGenericTypes.TryGetValue(typeName, out Type commonType))
{
Type[] genericArgs = commonType.GetGenericArguments();
if (genericArgs.Length == arity)
{
return commonType;
}
}
// Build CLR-style name with arity
string clrName = $"{typeName}`{arity}";
// Try direct resolution
Type resolved = Type.GetType(clrName, throwOnError: false);
if (resolved != null)
{
return resolved;
}
// Search assemblies
return ResolveBySearchingAssemblies(clrName);
}
private static void ParseGenericArguments(string argsSection, List args)
{
int depth = 0;
int start = 0;
for (int i = 0; i < argsSection.Length; i++)
{
char c = argsSection[i];
if (c == '<')
{
depth++;
}
else if (c == '>')
{
depth--;
}
else if (c == ',' && depth == 0)
{
string arg = argsSection.Substring(start, i - start).Trim();
if (!string.IsNullOrEmpty(arg))
{
args.Add(arg);
}
start = i + 1;
}
}
// Add the last argument
if (start < argsSection.Length)
{
string arg = argsSection.Substring(start).Trim();
if (!string.IsNullOrEmpty(arg))
{
args.Add(arg);
}
}
}
private static Type ResolveBySearchingAssemblies(string typeName)
{
// Common namespaces to try
string[] commonNamespaces =
{
"System",
"System.Collections.Generic",
"System.Collections",
"UnityEngine",
};
// Try with common namespace prefixes
foreach (string ns in commonNamespaces)
{
string fullName = $"{ns}.{typeName}";
Type resolved = Type.GetType(fullName, throwOnError: false);
if (resolved != null)
{
return resolved;
}
}
// Search all loaded assemblies
Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
for (int i = 0; i < assemblies.Length; i++)
{
Assembly assembly = assemblies[i];
try
{
Type type = assembly.GetType(typeName, throwOnError: false);
if (type != null)
{
return type;
}
// Try with common namespace prefixes in this assembly
foreach (string ns in commonNamespaces)
{
string fullName = $"{ns}.{typeName}";
type = assembly.GetType(fullName, throwOnError: false);
if (type != null)
{
return type;
}
}
}
catch
{
// Ignore assembly loading errors
}
}
return null;
}
///
/// Gets a human-readable display name for a type.
///
/// The type to get a display name for.
///
/// A simplified type name using C# syntax for generics.
///
public static string GetDisplayName(Type type)
{
if (type == null)
{
return string.Empty;
}
// Check for built-in type alias
foreach (KeyValuePair alias in BuiltInTypeAliases)
{
if (alias.Value == type)
{
return alias.Key;
}
}
if (!type.IsGenericType)
{
return type.Name;
}
if (type.IsGenericTypeDefinition)
{
// Open generic: List<> or Dictionary<,>
string name = type.Name;
int backtickIndex = name.IndexOf('`');
if (backtickIndex >= 0)
{
name = name.Substring(0, backtickIndex);
}
Type[] args = type.GetGenericArguments();
if (args.Length == 1)
{
return $"{name}<>";
}
using PooledResource sbLease = Buffers.StringBuilder.Get(
out StringBuilder sb
);
sb.Append(name);
sb.Append('<');
for (int i = 1; i < args.Length; i++)
{
sb.Append(',');
}
sb.Append('>');
return sb.ToString();
}
else
{
// Closed generic: List or Dictionary
string name = type.Name;
int backtickIndex = name.IndexOf('`');
if (backtickIndex >= 0)
{
name = name.Substring(0, backtickIndex);
}
Type[] args = type.GetGenericArguments();
using PooledResource sbLease = Buffers.StringBuilder.Get(
out StringBuilder sb
);
sb.Append(name);
sb.Append('<');
for (int i = 0; i < args.Length; i++)
{
if (i > 0)
{
sb.Append(", ");
}
sb.Append(GetDisplayName(args[i]));
}
sb.Append('>');
return sb.ToString();
}
}
}
}