using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using UnityEditor.Compilation; namespace io.github.hatayama.uLoopMCP { /// /// Resolves missing using directives by retrying compilation with candidate namespaces. /// Parses CS0246/CS0103 from CompilerMessage and looks up types via AssemblyTypeIndex. /// internal sealed class AutoUsingResolver { private const int MaxRetries = 3; public async Task ResolveAsync( string sourcePath, string dllPath, string originalSource, List currentReferences, System.Func, CancellationToken, Task> buildFunc, CancellationToken ct) { Debug.Assert(originalSource != null, "originalSource must not be null"); string currentSource = originalSource; Dictionary> ambiguousCandidates = new(); HashSet addedNamespaces = new(System.StringComparer.Ordinal); List mutableReferences = currentReferences != null ? new List(currentReferences) : new List(); HashSet addedAssemblyReferences = new(System.StringComparer.OrdinalIgnoreCase); double referenceResolutionMilliseconds = 0; for (int attempt = 0; attempt < MaxRetries; attempt++) { File.WriteAllText(sourcePath, currentSource); // AssemblyBuilder retries must stay on Unity's main thread because the compiler API is not thread-safe. CompilerMessage[] messages = await buildFunc(sourcePath, dllPath, mutableReferences, ct); List unresolvedTypes = ExtractUnresolvedTypes(messages); if (unresolvedTypes.Count == 0) { return new AutoUsingResult( currentSource, messages, ambiguousCandidates, addedNamespaces, addedAssemblyReferences.ToList(), referenceResolutionMilliseconds); } HashSet namespacesToAdd = new(System.StringComparer.Ordinal); List assemblyReferencesToAdd = new List(); AssemblyTypeIndex index = AssemblyTypeIndex.Instance; Stopwatch resolutionStopwatch = Stopwatch.StartNew(); foreach (string typeName in unresolvedTypes) { if (string.IsNullOrEmpty(typeName)) continue; List candidates = index.FindNamespacesForType(typeName); if (candidates.Count == 1 && addedNamespaces.Add(candidates[0])) { namespacesToAdd.Add(candidates[0]); } else if (candidates.Count > 1) { ambiguousCandidates[typeName] = candidates; } List assemblyCandidates = index.FindAssemblyLocationsForIdentifier(typeName); if (assemblyCandidates.Count == 1) { AddAssemblyReferenceIfMissing(assemblyReferencesToAdd, mutableReferences, assemblyCandidates[0]); } } resolutionStopwatch.Stop(); referenceResolutionMilliseconds += resolutionStopwatch.Elapsed.TotalMilliseconds; if (namespacesToAdd.Count == 0 && assemblyReferencesToAdd.Count == 0) { break; } if (namespacesToAdd.Count > 0) { currentSource = InsertUsingDirectives(currentSource, namespacesToAdd); } foreach (string assemblyReference in assemblyReferencesToAdd) { addedAssemblyReferences.Add(assemblyReference); mutableReferences.Add(assemblyReference); } } // Final compilation with all added usings File.WriteAllText(sourcePath, currentSource); CompilerMessage[] finalMessages = await buildFunc(sourcePath, dllPath, mutableReferences, ct); return new AutoUsingResult( currentSource, finalMessages, ambiguousCandidates, addedNamespaces, addedAssemblyReferences.ToList(), referenceResolutionMilliseconds); } private static List ExtractUnresolvedTypes(CompilerMessage[] messages) { List types = new(); foreach (CompilerMessage msg in messages) { if (msg.type != CompilerMessageType.Error) continue; // CS0246: The type or namespace name 'X' could not be found // CS0103: The name 'X' does not exist in the current context if (!msg.message.Contains("CS0246") && !msg.message.Contains("CS0103") && !msg.message.Contains("CS1069")) { continue; } string typeName = CompilationDiagnosticMessageParser.ExtractTypeNameFromMessage(msg.message); if (!string.IsNullOrEmpty(typeName)) { types.Add(typeName); } } return types; } internal static string InsertUsingDirectives(string source, IEnumerable namespaces) { StringBuilder sb = new StringBuilder(); foreach (string ns in namespaces) { sb.AppendLine($"using {ns};"); } sb.Append(source); return sb.ToString(); } internal static void AddAssemblyReferenceIfMissing( List assemblyReferencesToAdd, List currentReferences, string assemblyReference) { if (string.IsNullOrEmpty(assemblyReference)) { return; } if (ContainsAssemblyReference(currentReferences, assemblyReference)) { return; } if (ContainsAssemblyReference(assemblyReferencesToAdd, assemblyReference)) { return; } assemblyReferencesToAdd.Add(assemblyReference); } internal static bool ContainsAssemblyReference(List references, string assemblyReference) { string assemblyIdentity = GetAssemblyIdentityKey(assemblyReference); foreach (string existingReference in references) { if (string.Equals( GetAssemblyIdentityKey(existingReference), assemblyIdentity, System.StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } private static string GetAssemblyIdentityKey(string assemblyReference) { if (string.IsNullOrEmpty(assemblyReference)) { return string.Empty; } string fileName = Path.GetFileNameWithoutExtension(assemblyReference); if (string.IsNullOrEmpty(fileName)) { return assemblyReference; } return fileName; } } internal sealed class AutoUsingResult { public string UpdatedSource { get; } public CompilerMessage[] Messages { get; } public Dictionary> AmbiguousTypeCandidates { get; } public IReadOnlyCollection AddedNamespaces { get; } public IReadOnlyCollection AddedAssemblyReferences { get; } public double ReferenceResolutionMilliseconds { get; } public AutoUsingResult( string updatedSource, CompilerMessage[] messages, Dictionary> ambiguousTypeCandidates, IReadOnlyCollection addedNamespaces, IReadOnlyCollection addedAssemblyReferences, double referenceResolutionMilliseconds) { UpdatedSource = updatedSource; Messages = messages; AmbiguousTypeCandidates = ambiguousTypeCandidates; AddedNamespaces = addedNamespaces; AddedAssemblyReferences = addedAssemblyReferences; ReferenceResolutionMilliseconds = referenceResolutionMilliseconds; } } }