Files
2026-07-13 12:49:17 +08:00

822 lines
31 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Runtime.Helpers;
using Newtonsoft.Json.Linq;
using UnityEditor;
namespace MCPForUnity.Editor.Tools
{
[McpForUnityTool("unity_reflect", AutoRegister = false, Group = "docs")]
public static class UnityReflect
{
private static Dictionary<string, Type[]> _assemblyTypeCache;
private static readonly object CacheLock = new();
private static readonly ConcurrentDictionary<Type, string[]> ExtensionMethodCache = new();
private static readonly string[] NamespacePrefixes =
{
"UnityEngine.",
"UnityEditor.",
"UnityEngine.UI.",
"Unity.Cinemachine.",
"UnityEngine.AI.",
"UnityEngine.Rendering.Universal.",
"UnityEngine.Rendering.HighDefinition.",
"UnityEngine.InputSystem.",
"UnityEngine.ProBuilder.",
"UnityEngine.Tilemaps.",
"UnityEngine.EventSystems.",
"UnityEngine.Rendering.",
"UnityEngine.SceneManagement.",
"UnityEngine.Animations.",
"UnityEngine.Playables.",
"UnityEngine.UIElements."
};
private static readonly Dictionary<Type, string> FriendlyTypeNames = new()
{
{ typeof(void), "void" },
{ typeof(int), "int" },
{ typeof(float), "float" },
{ typeof(bool), "bool" },
{ typeof(string), "string" },
{ typeof(double), "double" },
{ typeof(long), "long" },
{ typeof(object), "object" },
{ typeof(byte), "byte" },
{ typeof(short), "short" },
{ typeof(char), "char" },
{ typeof(decimal), "decimal" },
{ typeof(uint), "uint" },
{ typeof(ulong), "ulong" },
{ typeof(ushort), "ushort" },
{ typeof(sbyte), "sbyte" }
};
[InitializeOnLoadMethod]
private static void OnLoad()
{
AssemblyReloadEvents.afterAssemblyReload += InvalidateCache;
}
private static void InvalidateCache()
{
lock (CacheLock)
{
_assemblyTypeCache = null;
}
ExtensionMethodCache.Clear();
}
private static Dictionary<string, Type[]> GetAssemblyTypeCache()
{
lock (CacheLock)
{
if (_assemblyTypeCache != null)
return _assemblyTypeCache;
_assemblyTypeCache = new Dictionary<string, Type[]>();
foreach (var asm in UnityAssembliesCompat.GetLoadedAssemblies())
{
try
{
_assemblyTypeCache[asm.FullName] = asm.GetExportedTypes();
}
catch
{
// Some assemblies throw on GetExportedTypes
}
}
return _assemblyTypeCache;
}
}
public static object HandleCommand(JObject @params)
{
if (EditorApplication.isCompiling)
return new ErrorResponse("Cannot reflect while Unity is compiling. Wait for domain reload to complete.");
if (@params == null)
return new ErrorResponse("Parameters cannot be null.");
var p = new ToolParams(@params);
var actionResult = p.GetRequired("action");
if (!actionResult.IsSuccess)
return new ErrorResponse(actionResult.ErrorMessage);
string action = actionResult.Value.ToLowerInvariant();
try
{
switch (action)
{
case "get_type":
return GetTypeInfo(p);
case "get_member":
return GetMemberInfo(p);
case "search":
return SearchTypes(p);
default:
return new ErrorResponse(
$"Unknown action: '{action}'. Supported actions: get_type, get_member, search.");
}
}
catch (Exception ex)
{
return new ErrorResponse(ex.Message, new { stackTrace = ex.StackTrace });
}
}
// === get_type ===
private static object GetTypeInfo(ToolParams p)
{
var classResult = p.GetRequired("class_name", "'class_name' parameter is required for get_type.");
if (!classResult.IsSuccess)
return new ErrorResponse(classResult.ErrorMessage);
string className = classResult.Value;
string normalizedName = NormalizeGenericName(className);
// Check for ambiguity first (only for short names without namespace)
if (!normalizedName.Contains('.') && !normalizedName.Contains('`'))
{
var matches = FindAllTypesByShortName(normalizedName);
if (matches.Count > 1)
{
return new SuccessResponse($"Ambiguous type name '{className}'.", new
{
found = true,
ambiguous = true,
query = className,
matches = matches.Select(t => t.FullName).OrderBy(n => n).ToArray(),
hint = "Use the fully qualified name (e.g., 'UnityEngine.UI.Button') to disambiguate."
});
}
}
var type = ResolveType(normalizedName);
if (type == null)
{
return new SuccessResponse($"Type '{className}' not found.", new
{
found = false,
query = className
});
}
// Open generic type definitions (e.g. List<T>) segfault Mono on Unity 2021.3
// in mono_metadata_generic_param_equal_internal when any member reflection is
// performed. Return minimal info using only safe property accesses.
if (type.IsGenericTypeDefinition)
{
return new SuccessResponse($"Type info for '{type.Name}'.", new
{
found = true,
name = type.Name,
full_name = type.FullName,
@namespace = type.Namespace,
assembly = type.Assembly.GetName().Name,
is_generic_type_definition = true,
hint = "Open generic type — consult docs for member details."
});
}
var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly;
var methods = type.GetMethods(flags)
.Where(m => !m.IsSpecialName)
.Select(m => m.Name)
.Distinct()
.OrderBy(n => n)
.ToArray();
var properties = type.GetProperties(flags)
.Select(pr => pr.Name)
.Distinct()
.OrderBy(n => n)
.ToArray();
var fields = type.GetFields(flags)
.Select(f => f.Name)
.Distinct()
.OrderBy(n => n)
.ToArray();
var events = type.GetEvents(flags)
.Select(e => e.Name)
.Distinct()
.OrderBy(n => n)
.ToArray();
var obsoleteMembers = GetObsoleteMembers(type, flags);
var extensionMethods = FindExtensionMethods(type);
var interfaces = type.GetInterfaces()
.Select(i => FormatTypeName(i))
.OrderBy(n => n)
.ToArray();
return new SuccessResponse($"Type info for '{FormatTypeName(type)}'.", new
{
found = true,
name = FormatTypeName(type),
full_name = type.FullName,
@namespace = type.Namespace,
assembly = type.Assembly.GetName().Name,
base_class = type.BaseType != null ? FormatTypeName(type.BaseType) : null,
interfaces,
is_abstract = type.IsAbstract,
is_sealed = type.IsSealed,
is_static = type.IsAbstract && type.IsSealed,
is_enum = type.IsEnum,
is_interface = type.IsInterface,
members = new
{
methods,
properties,
fields,
events
},
extension_methods = extensionMethods,
obsolete_members = obsoleteMembers
});
}
// === get_member ===
private static object GetMemberInfo(ToolParams p)
{
var classResult = p.GetRequired("class_name", "'class_name' parameter is required for get_member.");
if (!classResult.IsSuccess)
return new ErrorResponse(classResult.ErrorMessage);
var memberResult = p.GetRequired("member_name", "'member_name' parameter is required for get_member.");
if (!memberResult.IsSuccess)
return new ErrorResponse(memberResult.ErrorMessage);
string className = classResult.Value;
string memberName = memberResult.Value;
string normalizedName = NormalizeGenericName(className);
if (!normalizedName.Contains('.') && !normalizedName.Contains('`'))
{
var matches = FindAllTypesByShortName(normalizedName);
if (matches.Count > 1)
{
return new SuccessResponse($"Ambiguous type name '{className}'.", new
{
found = true,
ambiguous = true,
query = className,
matches = matches.Select(t => t.FullName).OrderBy(n => n).ToArray(),
hint = "Use the fully qualified name to disambiguate before requesting member details."
});
}
}
var type = ResolveType(normalizedName);
if (type == null)
{
return new SuccessResponse($"Type '{className}' not found.", new
{
found = false,
query = className
});
}
if (type.IsGenericTypeDefinition)
{
return new SuccessResponse(
$"Open generic type '{type.Name}' — consult docs for member details.", new
{
found = false,
type_name = type.FullName,
member_name = memberName,
is_generic_type_definition = true,
hint = "Open generic type — consult docs for member details."
});
}
// Use flags without DeclaredOnly to find inherited members
var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static;
// Try methods first
var methods = type.GetMethods(flags)
.Where(m => !m.IsSpecialName && m.Name == memberName)
.ToArray();
if (methods.Length > 0)
{
var overloads = methods.Select(m => FormatMethodDetail(m)).ToArray();
return new SuccessResponse($"Member '{memberName}' on '{FormatTypeName(type)}'.", new
{
found = true,
type_name = FormatTypeName(type),
member_name = memberName,
member_type = "method",
overload_count = overloads.Length,
overloads
});
}
// Try properties
var prop = type.GetProperty(memberName, flags);
if (prop != null)
{
return new SuccessResponse($"Member '{memberName}' on '{FormatTypeName(type)}'.", new
{
found = true,
type_name = FormatTypeName(type),
member_name = memberName,
member_type = "property",
property_type = FormatTypeName(prop.PropertyType),
can_read = prop.CanRead,
can_write = prop.CanWrite,
is_static = (prop.GetMethod ?? prop.SetMethod)?.IsStatic ?? false,
is_obsolete = prop.GetCustomAttribute<ObsoleteAttribute>() != null,
declaring_type = prop.DeclaringType != type ? FormatTypeName(prop.DeclaringType) : null
});
}
// Try fields
var field = type.GetField(memberName, flags);
if (field != null)
{
return new SuccessResponse($"Member '{memberName}' on '{FormatTypeName(type)}'.", new
{
found = true,
type_name = FormatTypeName(type),
member_name = memberName,
member_type = "field",
field_type = FormatTypeName(field.FieldType),
is_static = field.IsStatic,
is_readonly = field.IsInitOnly,
is_constant = field.IsLiteral,
constant_value = field.IsLiteral ? field.GetRawConstantValue() : null,
is_obsolete = field.GetCustomAttribute<ObsoleteAttribute>() != null,
declaring_type = field.DeclaringType != type ? FormatTypeName(field.DeclaringType) : null
});
}
// Try events
var evt = type.GetEvent(memberName, flags);
if (evt != null)
{
return new SuccessResponse($"Member '{memberName}' on '{FormatTypeName(type)}'.", new
{
found = true,
type_name = FormatTypeName(type),
member_name = memberName,
member_type = "event",
event_handler_type = FormatTypeName(evt.EventHandlerType),
is_obsolete = evt.GetCustomAttribute<ObsoleteAttribute>() != null,
declaring_type = evt.DeclaringType != type ? FormatTypeName(evt.DeclaringType) : null
});
}
// Try extension methods as a last resort
var extMethods = FindExtensionMethodInfos(type, memberName);
if (extMethods.Length > 0)
{
var overloads = extMethods.Select(m => FormatMethodDetail(m)).ToArray();
return new SuccessResponse($"Extension method '{memberName}' on '{FormatTypeName(type)}'.", new
{
found = true,
type_name = FormatTypeName(type),
member_name = memberName,
member_type = "extension_method",
overload_count = overloads.Length,
overloads,
declaring_type = FormatTypeName(extMethods[0].DeclaringType)
});
}
return new SuccessResponse($"Member '{memberName}' not found on '{FormatTypeName(type)}'.", new
{
found = false,
type_name = FormatTypeName(type),
member_name = memberName
});
}
// === search ===
private static object SearchTypes(ToolParams p)
{
var queryResult = p.GetRequired("query", "'query' parameter is required for search.");
if (!queryResult.IsSuccess)
return new ErrorResponse(queryResult.ErrorMessage);
string query = queryResult.Value;
string scope = p.Get("scope", "unity").ToLowerInvariant();
if (scope != "unity" && scope != "packages" && scope != "project" && scope != "all")
{
return new ErrorResponse(
$"Invalid scope: '{scope}'. Supported: unity, packages, project, all.");
}
var cache = GetAssemblyTypeCache();
string queryLower = query.ToLowerInvariant();
var candidates = new List<(Type type, int rank)>();
foreach (var kvp in cache)
{
var asm = kvp.Value.Length > 0 ? kvp.Value[0].Assembly : null;
if (asm == null) continue;
string asmName = asm.GetName().Name;
if (!MatchesScope(asmName, scope))
continue;
foreach (var t in kvp.Value)
{
if (t.Name == null) continue;
string nameLower = t.Name.ToLowerInvariant();
string fullNameLower = t.FullName?.ToLowerInvariant() ?? nameLower;
if (nameLower == queryLower || fullNameLower == queryLower)
candidates.Add((t, 0)); // Exact match
else if (nameLower.StartsWith(queryLower))
candidates.Add((t, 1)); // Starts with
else if (nameLower.Contains(queryLower) || fullNameLower.Contains(queryLower))
candidates.Add((t, 2)); // Contains
}
}
var results = candidates
.OrderBy(c => c.rank)
.ThenBy(c => c.type.FullName)
.Take(25)
.Select(c => new
{
name = c.type.Name,
full_name = c.type.FullName,
@namespace = c.type.Namespace,
assembly = c.type.Assembly.GetName().Name,
is_class = c.type.IsClass,
is_enum = c.type.IsEnum,
is_interface = c.type.IsInterface,
is_struct = c.type.IsValueType && !c.type.IsEnum
})
.ToArray();
return new SuccessResponse($"Found {results.Length} type(s) matching '{query}' (scope: {scope}).", new
{
query,
scope,
count = results.Length,
results,
truncated = candidates.Count > 25
});
}
// --- Type Resolution ---
private static Type ResolveType(string className)
{
// Use the shared UnityTypeResolver which handles caching,
// namespace prefixes, player-over-editor priority, and TypeCache fallback.
var type = UnityTypeResolver.ResolveAny(className);
if (type != null) return type;
// UnityTypeResolver doesn't try our extended namespace prefixes,
// so fall back to assembly cache scan for edge cases.
var cache = GetAssemblyTypeCache();
foreach (var prefix in NamespacePrefixes)
{
string fullName = prefix + className;
foreach (var kvp in cache)
{
type = Array.Find(kvp.Value, t => t.FullName == fullName);
if (type != null) return type;
}
}
return null;
}
private static List<Type> FindAllTypesByShortName(string shortName)
{
var matches = new List<Type>();
foreach (var kvp in GetAssemblyTypeCache())
{
foreach (var t in kvp.Value)
{
if (t.Name == shortName)
matches.Add(t);
}
}
return matches;
}
// --- Generic Name Normalization ---
private static string NormalizeGenericName(string name)
{
// Parse List<T> -> List`1, Dictionary<K,V> -> Dictionary`2
var match = Regex.Match(name, @"^(.+)<(.+)>$");
if (!match.Success) return name;
string baseName = match.Groups[1].Value;
string typeArgs = match.Groups[2].Value;
// Count generic args by tracking nesting depth
int argCount = 1;
int depth = 0;
foreach (char c in typeArgs)
{
if (c == '<') depth++;
else if (c == '>') depth--;
else if (c == ',' && depth == 0) argCount++;
}
return $"{baseName}`{argCount}";
}
// --- Type Name Formatting ---
private static string FormatTypeName(Type type)
{
if (type == null) return "null";
if (FriendlyTypeNames.TryGetValue(type, out var friendly))
return friendly;
if (type.IsArray)
return FormatTypeName(type.GetElementType()) + "[]";
if (type.IsByRef)
return FormatTypeName(type.GetElementType());
if (type.IsGenericType)
{
string baseName = type.Name;
int backtickIndex = baseName.IndexOf('`');
if (backtickIndex > 0)
baseName = baseName.Substring(0, backtickIndex);
var args = type.GetGenericArguments();
string argsStr = string.Join(", ", args.Select(FormatTypeName));
return $"{baseName}<{argsStr}>";
}
// For nested types, use dot notation
if (type.IsNested && type.DeclaringType != null)
return FormatTypeName(type.DeclaringType) + "." + type.Name;
return type.Name;
}
// --- Method Formatting ---
private static object FormatMethodDetail(MethodInfo m)
{
var parameters = m.GetParameters().Select(param =>
{
string prefix = "";
if (param.IsOut) prefix = "out ";
else if (param.ParameterType.IsByRef) prefix = "ref ";
return new
{
name = param.Name,
type = prefix + FormatTypeName(param.ParameterType),
has_default = param.HasDefaultValue,
default_value = param.HasDefaultValue ? FormatDefaultValue(param.DefaultValue) : null,
is_params = param.IsDefined(typeof(ParamArrayAttribute))
};
}).ToArray();
string signature = FormatMethodSignature(m);
var obsoleteAttr = m.GetCustomAttribute<ObsoleteAttribute>();
return new
{
signature,
return_type = FormatTypeName(m.ReturnType),
parameters,
is_static = m.IsStatic,
is_virtual = m.IsVirtual && !m.IsFinal,
is_abstract = m.IsAbstract,
is_generic = m.IsGenericMethod,
generic_arguments = m.IsGenericMethod
? m.GetGenericArguments().Select(a => a.Name).ToArray()
: null,
is_obsolete = obsoleteAttr != null,
obsolete_message = obsoleteAttr?.Message,
declaring_type = m.DeclaringType != m.ReflectedType
? FormatTypeName(m.DeclaringType)
: null
};
}
private static string FormatMethodSignature(MethodInfo m)
{
string staticPrefix = m.IsStatic ? "static " : "";
string returnType = FormatTypeName(m.ReturnType);
string name = m.Name;
if (m.IsGenericMethod)
{
var genArgs = m.GetGenericArguments();
name += "<" + string.Join(", ", genArgs.Select(a => a.Name)) + ">";
}
var paramStrings = m.GetParameters().Select(param =>
{
string prefix = "";
if (param.IsOut) prefix = "out ";
else if (param.ParameterType.IsByRef) prefix = "ref ";
if (param.IsDefined(typeof(ParamArrayAttribute)))
prefix = "params ";
return prefix + FormatTypeName(param.ParameterType) + " " + param.Name;
});
return $"{staticPrefix}{returnType} {name}({string.Join(", ", paramStrings)})";
}
private static object FormatDefaultValue(object value)
{
if (value == null) return "null";
if (value is string s) return $"\"{s}\"";
if (value is bool b) return b ? "true" : "false";
return value;
}
// --- Obsolete Member Detection ---
private static string[] GetObsoleteMembers(Type type, BindingFlags flags)
{
var obsolete = new HashSet<string>();
foreach (var m in type.GetMethods(flags).Where(m => !m.IsSpecialName))
{
if (m.GetCustomAttribute<ObsoleteAttribute>() != null)
obsolete.Add(m.Name);
}
foreach (var pr in type.GetProperties(flags))
{
if (pr.GetCustomAttribute<ObsoleteAttribute>() != null)
obsolete.Add(pr.Name);
}
foreach (var f in type.GetFields(flags))
{
if (f.GetCustomAttribute<ObsoleteAttribute>() != null)
obsolete.Add(f.Name);
}
foreach (var e in type.GetEvents(flags))
{
if (e.GetCustomAttribute<ObsoleteAttribute>() != null)
obsolete.Add(e.Name);
}
return obsolete.Count > 0 ? obsolete.OrderBy(n => n).ToArray() : Array.Empty<string>();
}
// --- Extension Method Discovery ---
private static string[] FindExtensionMethods(Type targetType)
{
if (ExtensionMethodCache.TryGetValue(targetType, out var cached))
return cached;
var extensionNames = new HashSet<string>();
var cache = GetAssemblyTypeCache();
foreach (var kvp in cache)
{
// Extract assembly name from the FullName key (e.g., "UnityEngine, Version=...")
string asmName = kvp.Key.Split(',')[0];
if (!asmName.StartsWith("UnityEngine") && !asmName.StartsWith("UnityEditor") && !asmName.StartsWith("Unity."))
continue;
foreach (var t in kvp.Value)
{
if (!t.IsAbstract || !t.IsSealed) continue; // Static classes only
if (!t.IsDefined(typeof(ExtensionAttribute), false)) continue;
foreach (var method in t.GetMethods(BindingFlags.Public | BindingFlags.Static))
{
if (!method.IsDefined(typeof(ExtensionAttribute), false)) continue;
var firstParam = method.GetParameters().FirstOrDefault();
if (firstParam == null) continue;
var paramType = firstParam.ParameterType;
if (paramType.IsAssignableFrom(targetType) || targetType.IsSubclassOf(paramType)
|| paramType == targetType
|| (paramType.IsGenericType && IsGenericMatch(paramType, targetType)))
{
extensionNames.Add(method.Name);
}
}
}
}
var result = extensionNames.Count > 0
? extensionNames.OrderBy(n => n).ToArray()
: Array.Empty<string>();
ExtensionMethodCache.TryAdd(targetType, result);
return result;
}
private static MethodInfo[] FindExtensionMethodInfos(Type targetType, string methodName)
{
var results = new List<MethodInfo>();
var cache = GetAssemblyTypeCache();
foreach (var kvp in cache)
{
string asmName = kvp.Key.Split(',')[0];
if (!asmName.StartsWith("UnityEngine") && !asmName.StartsWith("UnityEditor") && !asmName.StartsWith("Unity."))
continue;
foreach (var t in kvp.Value)
{
if (!t.IsAbstract || !t.IsSealed) continue;
if (!t.IsDefined(typeof(ExtensionAttribute), false)) continue;
foreach (var method in t.GetMethods(BindingFlags.Public | BindingFlags.Static))
{
if (method.Name != methodName) continue;
if (!method.IsDefined(typeof(ExtensionAttribute), false)) continue;
var firstParam = method.GetParameters().FirstOrDefault();
if (firstParam == null) continue;
var paramType = firstParam.ParameterType;
if (paramType.IsAssignableFrom(targetType) || targetType.IsSubclassOf(paramType)
|| paramType == targetType
|| (paramType.IsGenericType && IsGenericMatch(paramType, targetType)))
{
results.Add(method);
}
}
}
}
return results.ToArray();
}
private static bool IsGenericMatch(Type genericParamType, Type targetType)
{
if (!genericParamType.IsGenericType) return false;
var genDef = genericParamType.GetGenericTypeDefinition();
// Check if targetType implements or inherits from the generic definition
if (targetType.IsGenericType && targetType.GetGenericTypeDefinition() == genDef)
return true;
foreach (var iface in targetType.GetInterfaces())
{
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == genDef)
return true;
}
return false;
}
// --- Scope Matching ---
private static bool MatchesScope(string assemblyName, string scope)
{
switch (scope)
{
case "unity":
return assemblyName.StartsWith("UnityEngine")
|| assemblyName.StartsWith("UnityEditor")
|| assemblyName.StartsWith("Unity.");
case "packages":
return !assemblyName.StartsWith("System")
&& !assemblyName.StartsWith("mscorlib")
&& !assemblyName.StartsWith("netstandard");
case "project":
return assemblyName == "Assembly-CSharp"
|| assemblyName == "Assembly-CSharp-Editor"
|| assemblyName.StartsWith("Assembly-CSharp-firstpass")
|| assemblyName.StartsWith("Assembly-CSharp-Editor-firstpass");
case "all":
return true;
default:
return false;
}
}
}
}