Files
Shutong Wu 777e8a9c7a fix: trust the pipeline flag when a domain reload is deferred (#1276)
EditorApplication.isCompiling conflates three states: actually compiling,
compilation queued, and finished-but-reload-deferred. A project holding
EditorApplication.LockReloadAssemblies sits in the third state for as long
as the lock is held, with no compilation running, and isCompiling stays
true the whole time.

Eight call sites gated on that raw flag, so they refused work indefinitely:
the stdio bridge would not start, unity_reflect and manage_scriptable_object
returned "Unity is compiling", refresh_unity reported the wrong resulting
state and never completed its wait, the stdio reload handler deferred its
resume, and TestJobManager both mis-attributed its init timeout and reported
a bogus "compiling" block reason.

Route all eight through EditorStateCache.GetActualIsCompiling(), which falls
back to the event-tracked CompilationPipeline flag, and drop the isPlaying
gate that previously limited the workaround to play mode.

Verified live: with LockReloadAssemblies held after RequestScriptCompilation,
EditorApplication.isCompiling is true while the pipeline flag is false, so
CompilationPipeline.compilationFinished does fire while the reload is held.
2026-07-28 12:05:30 -04:00

823 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.Editor.Services;
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 (EditorStateCache.GetActualIsCompiling())
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;
}
}
}
}