using System.Collections.Generic; using System.Diagnostics; namespace io.github.hatayama.uLoopMCP { /// /// Scans wrapped source for potential type identifiers and pre-injects using directives /// before the first compilation, avoiding AutoUsingResolver retry loops (~590ms/retry saved). /// Only applies to script mode (wrapped source from WrapperTemplate); raw mode is excluded. /// internal static class PreUsingResolver { private static readonly HashSet ExcludedIdentifiers = new(System.StringComparer.Ordinal) { "abstract", "as", "async", "await", "base", "bool", "break", "byte", "case", "catch", "char", "checked", "class", "const", "continue", "decimal", "default", "delegate", "do", "double", "else", "enum", "event", "explicit", "extern", "false", "finally", "fixed", "float", "for", "foreach", "goto", "if", "implicit", "in", "int", "interface", "internal", "is", "lock", "long", "namespace", "new", "null", "object", "operator", "out", "override", "params", "private", "protected", "public", "readonly", "record", "ref", "return", "sbyte", "sealed", "short", "sizeof", "stackalloc", "static", "string", "struct", "switch", "this", "throw", "true", "try", "typeof", "uint", "ulong", "unchecked", "unsafe", "ushort", "using", "var", "virtual", "void", "volatile", "while", "yield", "String", "Int32", "Int64", "Boolean", "Single", "Double", "Byte", "Object", "Void", "Decimal", "Char", "UInt32", "UInt64", "Int16", "UInt16", "SByte", "IntPtr", "UIntPtr", "Task", "Dictionary", "CancellationToken", "ExecuteAsync", "Execute" }; public static PreUsingResult Resolve(string wrappedSource, AssemblyTypeIndex index) { Debug.Assert(wrappedSource != null, "wrappedSource must not be null"); Debug.Assert(index != null, "index must not be null"); HashSet existingNamespaces = ExtractExistingNamespaces(wrappedSource); string userCodeSection = ExtractUserCodeSection(wrappedSource); HashSet candidateTypes = ExtractTypeIdentifiers(userCodeSection); HashSet qualifiedTypeIdentifiers = ExtractQualifiedTypeIdentifiers(userCodeSection); HashSet namespacesToAdd = new(System.StringComparer.Ordinal); List assemblyReferencesToAdd = new List(); foreach (string typeName in candidateTypes) { List namespaces = index.FindNamespacesForType(typeName); if (namespaces.Count == 1 && !existingNamespaces.Contains(namespaces[0])) { namespacesToAdd.Add(namespaces[0]); } List assemblyReferences = index.FindAssemblyLocationsForType(typeName); if (assemblyReferences.Count == 1) { AddAssemblyReferenceIfMissing(assemblyReferencesToAdd, assemblyReferences[0]); } } foreach (string typeName in qualifiedTypeIdentifiers) { List assemblyReferences = index.FindAssemblyLocationsForIdentifier(typeName); if (assemblyReferences.Count == 1) { AddAssemblyReferenceIfMissing(assemblyReferencesToAdd, assemblyReferences[0]); } } if (namespacesToAdd.Count == 0) { return new PreUsingResult( wrappedSource, System.Array.Empty(), assemblyReferencesToAdd); } string updatedSource = AutoUsingResolver.InsertUsingDirectives(wrappedSource, namespacesToAdd); return new PreUsingResult(updatedSource, namespacesToAdd, assemblyReferencesToAdd); } private static HashSet ExtractExistingNamespaces(string source) { HashSet namespaces = new(System.StringComparer.Ordinal); int pos = 0; int length = source.Length; while (pos < length) { pos = SourceShaper.SkipWhitespace(source, pos); if (pos >= length) break; if (source[pos] == '#') { pos = SkipToEndOfLine(source, pos); continue; } // Skip comments that appear before using directives int skipped = SourceShaper.AdvanceOneTokenPublic(source, pos); if (skipped > pos + 1 && !char.IsLetterOrDigit(source[pos]) && source[pos] != '_') { pos = skipped; continue; } // Handle "global using Ns;" — advance past "global" to reach "using" int usingPos = pos; if (SourceShaper.StartsWithKeyword(source, pos, "global")) { usingPos = SourceShaper.SkipWhitespace(source, pos + 6); } if (!SourceShaper.StartsWithKeyword(source, usingPos, "using")) { break; } int afterUsing = SourceShaper.SkipWhitespace(source, usingPos + 5); if (SourceShaper.StartsWithKeyword(source, afterUsing, "static") || SourceShaper.StartsWithKeyword(source, afterUsing, "var") || (afterUsing < length && source[afterUsing] == '(')) { pos = SkipToSemicolon(source, pos); continue; } int semiPos = source.IndexOf(';', afterUsing); if (semiPos > afterUsing) { string ns = source.Substring(afterUsing, semiPos - afterUsing).Trim(); int eqIdx = ns.IndexOf('='); if (eqIdx >= 0) { ns = ns.Substring(eqIdx + 1).Trim(); } if (ns.Length > 0) { namespaces.Add(ns); } } pos = semiPos >= 0 ? semiPos + 1 : length; } return namespaces; } internal static HashSet ExtractTypeIdentifiers(string source) { HashSet identifiers = new(System.StringComparer.Ordinal); int pos = 0; int length = source.Length; bool prevWasDot = false; while (pos < length) { char c = source[pos]; if (char.IsWhiteSpace(c)) { pos++; continue; } int advanced = SourceShaper.AdvanceOneTokenPublic(source, pos); if (advanced > pos + 1 && !char.IsLetterOrDigit(source[pos]) && source[pos] != '_') { prevWasDot = false; pos = advanced; continue; } if (c == '.') { prevWasDot = true; pos++; continue; } if (char.IsLetter(c) || c == '_') { int start = pos; while (pos < length && (char.IsLetterOrDigit(source[pos]) || source[pos] == '_')) { pos++; } if (!prevWasDot && char.IsUpper(c)) { string identifier = source.Substring(start, pos - start); // Member initializers (Name = ...), named arguments (Name: ...), // and labels (Name:) are not type candidates; skip them. // Exclude == so comparisons are not mistakenly filtered. int next = SourceShaper.SkipWhitespace(source, pos); bool looksLikeMemberOrLabel = next < length && (source[next] == ':' || (source[next] == '=' && (next + 1 >= length || source[next + 1] != '='))); if (!looksLikeMemberOrLabel && !ExcludedIdentifiers.Contains(identifier)) { identifiers.Add(identifier); } } prevWasDot = false; continue; } prevWasDot = false; pos = advanced; } return identifiers; } internal static HashSet ExtractQualifiedTypeIdentifiers(string source) { HashSet identifiers = new(System.StringComparer.Ordinal); int pos = 0; int length = source.Length; bool sawDot = false; List qualifiedChainParts = new List(); while (pos < length) { char c = source[pos]; if (char.IsWhiteSpace(c)) { pos++; continue; } int advanced = SourceShaper.AdvanceOneTokenPublic(source, pos); if (advanced > pos + 1 && !char.IsLetterOrDigit(source[pos]) && source[pos] != '_') { sawDot = false; qualifiedChainParts.Clear(); pos = advanced; continue; } if (c == '.') { sawDot = true; pos++; continue; } if (char.IsLetter(c) || c == '_') { int start = pos; while (pos < length && (char.IsLetterOrDigit(source[pos]) || source[pos] == '_')) { pos++; } string identifier = source.Substring(start, pos - start); if (!sawDot) { qualifiedChainParts.Clear(); qualifiedChainParts.Add(identifier); } else { qualifiedChainParts.Add(identifier); if (ShouldResolveQualifiedTypeAssembly(qualifiedChainParts)) { identifiers.Add(string.Join(".", qualifiedChainParts)); } } sawDot = false; continue; } sawDot = false; qualifiedChainParts.Clear(); pos = advanced; } return identifiers; } private static bool ShouldResolveQualifiedTypeAssembly(IReadOnlyList qualifiedChainParts) { return qualifiedChainParts != null && qualifiedChainParts.Count >= 2; } // WrapperTemplate marks user code with #line directives; // scanning only this region avoids false positives from boilerplate identifiers private static string ExtractUserCodeSection(string wrappedSource) { string startMarker = WrapperTemplate.UserCodeStartMarker; string endMarker = WrapperTemplate.UserCodeEndMarker; int startIdx = wrappedSource.IndexOf(startMarker, System.StringComparison.Ordinal); // Wrapped source from WrapperTemplate should always contain the marker Debug.Assert(startIdx >= 0, "UserCodeStartMarker not found in wrappedSource"); if (startIdx < 0) { return wrappedSource; } int codeStart = wrappedSource.IndexOf('\n', startIdx); Debug.Assert(codeStart >= 0, "newline after UserCodeStartMarker not found"); if (codeStart < 0) { return wrappedSource; } codeStart++; int endIdx = wrappedSource.IndexOf(endMarker, codeStart, System.StringComparison.Ordinal); Debug.Assert(endIdx >= 0, "UserCodeEndMarker not found in wrappedSource"); if (endIdx < 0) { return wrappedSource.Substring(codeStart); } return wrappedSource.Substring(codeStart, endIdx - codeStart); } private static int SkipToEndOfLine(string s, int pos) { while (pos < s.Length && s[pos] != '\n') pos++; if (pos < s.Length) pos++; return pos; } private static int SkipToSemicolon(string s, int pos) { int semi = s.IndexOf(';', pos); return semi >= 0 ? semi + 1 : s.Length; } private static void AddAssemblyReferenceIfMissing( List assemblyReferencesToAdd, string assemblyReference) { if (string.IsNullOrEmpty(assemblyReference)) { return; } foreach (string existingReference in assemblyReferencesToAdd) { if (string.Equals(existingReference, assemblyReference, System.StringComparison.OrdinalIgnoreCase)) { return; } } assemblyReferencesToAdd.Add(assemblyReference); } } internal sealed class PreUsingResult { public string UpdatedSource { get; } public IReadOnlyCollection AddedNamespaces { get; } public IReadOnlyCollection AddedAssemblyReferences { get; } public PreUsingResult( string updatedSource, IReadOnlyCollection addedNamespaces, IReadOnlyCollection addedAssemblyReferences) { UpdatedSource = updatedSource; AddedNamespaces = addedNamespaces; AddedAssemblyReferences = addedAssemblyReferences; } } }