chore(unity): 固定Unity CLI Loop本地包并移除UnitySkills依赖
This commit is contained in:
@@ -0,0 +1,591 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Lightweight C# source scanner that determines top-level structure without Roslyn.
|
||||
/// Tracks string literals, comments, and brace depth to classify source into one of three modes.
|
||||
/// </summary>
|
||||
internal static class SourceShaper
|
||||
{
|
||||
public static SourceShapeResult Analyze(string source)
|
||||
{
|
||||
Debug.Assert(source != null, "source must not be null");
|
||||
|
||||
SourceShapeResult result = new SourceShapeResult();
|
||||
int length = source.Length;
|
||||
int pos = 0;
|
||||
int braceDepth = 0;
|
||||
|
||||
while (pos < length)
|
||||
{
|
||||
pos = SkipWhitespace(source, pos);
|
||||
if (pos >= length) break;
|
||||
|
||||
if (braceDepth == 0)
|
||||
{
|
||||
if (TryMatchLineComment(source, pos, out int afterComment))
|
||||
{
|
||||
pos = afterComment;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryMatchBlockComment(source, pos, out int afterBlock))
|
||||
{
|
||||
pos = afterBlock;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (StartsWithKeyword(source, pos, "using"))
|
||||
{
|
||||
int segmentStart = pos;
|
||||
int afterUsing = pos + 5;
|
||||
afterUsing = SkipWhitespace(source, afterUsing);
|
||||
|
||||
if (StartsWithKeyword(source, afterUsing, "static"))
|
||||
{
|
||||
int end = FindSemicolon(source, segmentStart);
|
||||
result.UsingDirectives.Add(source.Substring(segmentStart, end - segmentStart + 1).TrimEnd());
|
||||
pos = end + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// "using var" and "using (" are using-statements, not using-directives
|
||||
if (afterUsing < length && (StartsWithKeyword(source, afterUsing, "var") || source[afterUsing] == '('))
|
||||
{
|
||||
result.HasTopLevelStatements = true;
|
||||
int end = FindStatementEnd(source, segmentStart, ref braceDepth);
|
||||
result.TopLevelBodyBuilder.AppendLine(source.Substring(segmentStart, end - segmentStart + 1).TrimEnd());
|
||||
pos = end + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
int semiEnd = FindSemicolon(source, segmentStart);
|
||||
result.UsingDirectives.Add(source.Substring(segmentStart, semiEnd - segmentStart + 1).TrimEnd());
|
||||
pos = semiEnd + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (StartsWithKeyword(source, pos, "namespace"))
|
||||
{
|
||||
result.HasNamespaceDeclaration = true;
|
||||
pos = SkipBlock(source, pos, ref braceDepth);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsTypeDeclarationKeyword(source, pos))
|
||||
{
|
||||
result.HasTypeDeclaration = true;
|
||||
pos = SkipBlock(source, pos, ref braceDepth);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (StartsWithKeyword(source, pos, "global") && StartsWithKeyword(source, SkipWhitespace(source, pos + 6), "using"))
|
||||
{
|
||||
int segmentStart = pos;
|
||||
int semiEnd = FindSemicolon(source, segmentStart);
|
||||
result.UsingDirectives.Add(source.Substring(segmentStart, semiEnd - segmentStart + 1).TrimEnd());
|
||||
pos = semiEnd + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pos < length && source[pos] == '[')
|
||||
{
|
||||
int afterAttr = SkipAttributeBlock(source, pos);
|
||||
int nextNonWs = SkipWhitespace(source, afterAttr);
|
||||
if (nextNonWs < length && IsTypeDeclarationKeyword(source, nextNonWs))
|
||||
{
|
||||
result.HasTypeDeclaration = true;
|
||||
pos = SkipBlock(source, nextNonWs, ref braceDepth);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (IsAccessModifier(source, pos))
|
||||
{
|
||||
int afterMod = SkipAccessModifiers(source, pos);
|
||||
if (IsTypeDeclarationKeyword(source, afterMod))
|
||||
{
|
||||
result.HasTypeDeclaration = true;
|
||||
pos = SkipBlock(source, afterMod, ref braceDepth);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
result.HasTopLevelStatements = true;
|
||||
int stmtStart = pos;
|
||||
int stmtEnd = FindStatementEnd(source, stmtStart, ref braceDepth);
|
||||
result.TopLevelBodyBuilder.AppendLine(source.Substring(stmtStart, stmtEnd - stmtStart + 1).TrimEnd());
|
||||
pos = stmtEnd + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
pos = AdvanceInsideBlock(source, pos, ref braceDepth);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static string WrapIfNeeded(string source, string namespaceName, string className)
|
||||
{
|
||||
SourceShapeResult shape = Analyze(source);
|
||||
|
||||
// Raw mode: namespace or type declaration without top-level statements → pass through
|
||||
if ((shape.HasNamespaceDeclaration || shape.HasTypeDeclaration) && !shape.HasTopLevelStatements)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
// Mixed mode: both type declarations and top-level statements → error
|
||||
if ((shape.HasNamespaceDeclaration || shape.HasTypeDeclaration) && shape.HasTopLevelStatements)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Script mode: wrap top-level statements
|
||||
string body = shape.TopLevelBodyBuilder.ToString().TrimEnd();
|
||||
|
||||
bool hasReturn = TopLevelReturnDetector.HasTopLevelReturn(body);
|
||||
if (!hasReturn)
|
||||
{
|
||||
body = string.IsNullOrWhiteSpace(body)
|
||||
? "return null;"
|
||||
: body + "\nreturn null;";
|
||||
}
|
||||
|
||||
return WrapperTemplate.Build(shape.UsingDirectives, namespaceName, className, body);
|
||||
}
|
||||
|
||||
internal static int SkipWhitespace(string s, int pos)
|
||||
{
|
||||
Debug.Assert(s != null, "s must not be null");
|
||||
Debug.Assert(pos >= 0, "pos must be non-negative");
|
||||
while (pos < s.Length && char.IsWhiteSpace(s[pos])) pos++;
|
||||
return pos;
|
||||
}
|
||||
|
||||
internal static bool StartsWithKeyword(string s, int pos, string keyword)
|
||||
{
|
||||
Debug.Assert(s != null, "s must not be null");
|
||||
Debug.Assert(keyword != null, "keyword must not be null");
|
||||
Debug.Assert(pos >= 0, "pos must be non-negative");
|
||||
if (pos + keyword.Length > s.Length) return false;
|
||||
for (int i = 0; i < keyword.Length; i++)
|
||||
{
|
||||
if (s[pos + i] != keyword[i]) return false;
|
||||
}
|
||||
// Keyword must be followed by non-identifier char
|
||||
int afterPos = pos + keyword.Length;
|
||||
if (afterPos < s.Length && (char.IsLetterOrDigit(s[afterPos]) || s[afterPos] == '_'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsTypeDeclarationKeyword(string s, int pos)
|
||||
{
|
||||
return StartsWithKeyword(s, pos, "class") ||
|
||||
StartsWithKeyword(s, pos, "struct") ||
|
||||
StartsWithKeyword(s, pos, "interface") ||
|
||||
StartsWithKeyword(s, pos, "enum") ||
|
||||
StartsWithKeyword(s, pos, "record");
|
||||
}
|
||||
|
||||
private static bool IsAccessModifier(string s, int pos)
|
||||
{
|
||||
return StartsWithKeyword(s, pos, "public") ||
|
||||
StartsWithKeyword(s, pos, "internal") ||
|
||||
StartsWithKeyword(s, pos, "private") ||
|
||||
StartsWithKeyword(s, pos, "protected") ||
|
||||
StartsWithKeyword(s, pos, "static") ||
|
||||
StartsWithKeyword(s, pos, "sealed") ||
|
||||
StartsWithKeyword(s, pos, "abstract") ||
|
||||
StartsWithKeyword(s, pos, "partial");
|
||||
}
|
||||
|
||||
private static int SkipAccessModifiers(string s, int pos)
|
||||
{
|
||||
while (pos < s.Length && IsAccessModifier(s, pos))
|
||||
{
|
||||
int wordEnd = pos;
|
||||
while (wordEnd < s.Length && (char.IsLetterOrDigit(s[wordEnd]) || s[wordEnd] == '_')) wordEnd++;
|
||||
pos = SkipWhitespace(s, wordEnd);
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
private static bool TryMatchLineComment(string s, int pos, out int afterComment)
|
||||
{
|
||||
afterComment = pos;
|
||||
if (pos + 1 < s.Length && s[pos] == '/' && s[pos + 1] == '/')
|
||||
{
|
||||
int end = pos + 2;
|
||||
while (end < s.Length && s[end] != '\n') end++;
|
||||
if (end < s.Length) end++; // skip \n
|
||||
afterComment = end;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryMatchBlockComment(string s, int pos, out int afterBlock)
|
||||
{
|
||||
afterBlock = pos;
|
||||
if (pos + 1 < s.Length && s[pos] == '/' && s[pos + 1] == '*')
|
||||
{
|
||||
int end = pos + 2;
|
||||
while (end + 1 < s.Length && !(s[end] == '*' && s[end + 1] == '/')) end++;
|
||||
afterBlock = end + 2 < s.Length ? end + 2 : s.Length;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static int FindSemicolon(string s, int pos)
|
||||
{
|
||||
while (pos < s.Length)
|
||||
{
|
||||
if (s[pos] == ';') return pos;
|
||||
pos = AdvanceOneToken(s, pos);
|
||||
}
|
||||
return s.Length - 1;
|
||||
}
|
||||
|
||||
private static int FindStatementEnd(string s, int pos, ref int braceDepth)
|
||||
{
|
||||
while (pos < s.Length)
|
||||
{
|
||||
char c = s[pos];
|
||||
if (c == '{')
|
||||
{
|
||||
braceDepth++;
|
||||
pos++;
|
||||
while (pos < s.Length && braceDepth > 0)
|
||||
{
|
||||
pos = AdvanceInsideBlock(s, pos, ref braceDepth);
|
||||
}
|
||||
return pos - 1;
|
||||
}
|
||||
if (c == ';') return pos;
|
||||
pos = AdvanceOneToken(s, pos);
|
||||
}
|
||||
return s.Length - 1;
|
||||
}
|
||||
|
||||
private static int SkipBlock(string s, int pos, ref int braceDepth)
|
||||
{
|
||||
// Advance past keywords until we hit the opening brace
|
||||
while (pos < s.Length && s[pos] != '{')
|
||||
{
|
||||
if (s[pos] == ';') return pos + 1; // forward declaration
|
||||
pos = AdvanceOneToken(s, pos);
|
||||
}
|
||||
if (pos < s.Length && s[pos] == '{')
|
||||
{
|
||||
braceDepth++;
|
||||
pos++;
|
||||
while (pos < s.Length && braceDepth > 0)
|
||||
{
|
||||
pos = AdvanceInsideBlock(s, pos, ref braceDepth);
|
||||
}
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
private static int SkipAttributeBlock(string s, int pos)
|
||||
{
|
||||
Debug.Assert(s[pos] == '[', "SkipAttributeBlock must start at '['");
|
||||
int depth = 1;
|
||||
pos++;
|
||||
while (pos < s.Length && depth > 0)
|
||||
{
|
||||
if (s[pos] == '[') depth++;
|
||||
else if (s[pos] == ']') depth--;
|
||||
else pos = AdvanceOneToken(s, pos) - 1; // -1 because loop will pos++ via fallthrough
|
||||
pos++;
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
private static int AdvanceInsideBlock(string s, int pos, ref int braceDepth)
|
||||
{
|
||||
char c = s[pos];
|
||||
if (c == '{') { braceDepth++; return pos + 1; }
|
||||
if (c == '}') { braceDepth--; return pos + 1; }
|
||||
return AdvanceOneToken(s, pos);
|
||||
}
|
||||
|
||||
internal static int AdvanceOneTokenPublic(string s, int pos)
|
||||
{
|
||||
return AdvanceOneToken(s, pos);
|
||||
}
|
||||
|
||||
private static int AdvanceOneToken(string s, int pos)
|
||||
{
|
||||
if (pos >= s.Length) return s.Length;
|
||||
char c = s[pos];
|
||||
|
||||
// Line comment
|
||||
if (c == '/' && pos + 1 < s.Length && s[pos + 1] == '/')
|
||||
{
|
||||
int end = pos + 2;
|
||||
while (end < s.Length && s[end] != '\n') end++;
|
||||
return end < s.Length ? end + 1 : s.Length;
|
||||
}
|
||||
|
||||
// Block comment
|
||||
if (c == '/' && pos + 1 < s.Length && s[pos + 1] == '*')
|
||||
{
|
||||
int end = pos + 2;
|
||||
while (end + 1 < s.Length && !(s[end] == '*' && s[end + 1] == '/')) end++;
|
||||
return end + 2 < s.Length ? end + 2 : s.Length;
|
||||
}
|
||||
|
||||
// Verbatim string (@"...")
|
||||
if (c == '@' && pos + 1 < s.Length && s[pos + 1] == '"')
|
||||
{
|
||||
int end = pos + 2;
|
||||
while (end < s.Length)
|
||||
{
|
||||
if (s[end] == '"')
|
||||
{
|
||||
if (end + 1 < s.Length && s[end + 1] == '"') { end += 2; continue; }
|
||||
return end + 1;
|
||||
}
|
||||
end++;
|
||||
}
|
||||
return s.Length;
|
||||
}
|
||||
|
||||
// Raw string literal (""" ... """)
|
||||
if (c == '"' && pos + 2 < s.Length && s[pos + 1] == '"' && s[pos + 2] == '"')
|
||||
{
|
||||
int end = pos + 3;
|
||||
while (end + 2 < s.Length)
|
||||
{
|
||||
if (s[end] == '"' && s[end + 1] == '"' && s[end + 2] == '"') return end + 3;
|
||||
end++;
|
||||
}
|
||||
return s.Length;
|
||||
}
|
||||
|
||||
// Regular string literal ("...")
|
||||
if (c == '"')
|
||||
{
|
||||
int end = pos + 1;
|
||||
while (end < s.Length && s[end] != '"')
|
||||
{
|
||||
if (s[end] == '\\') end++; // skip escaped char
|
||||
end++;
|
||||
}
|
||||
return end < s.Length ? end + 1 : s.Length;
|
||||
}
|
||||
|
||||
// Char literal ('x')
|
||||
if (c == '\'')
|
||||
{
|
||||
int end = pos + 1;
|
||||
while (end < s.Length && s[end] != '\'')
|
||||
{
|
||||
if (s[end] == '\\') end++;
|
||||
end++;
|
||||
}
|
||||
return end < s.Length ? end + 1 : s.Length;
|
||||
}
|
||||
|
||||
// Interpolated string ($"...")
|
||||
if (c == '$' && pos + 1 < s.Length && s[pos + 1] == '"')
|
||||
{
|
||||
return SkipInterpolatedString(s, pos + 2);
|
||||
}
|
||||
|
||||
return pos + 1;
|
||||
}
|
||||
|
||||
private static int SkipInterpolatedString(string s, int pos)
|
||||
{
|
||||
int end = pos;
|
||||
while (end < s.Length)
|
||||
{
|
||||
if (s[end] == '\\')
|
||||
{
|
||||
end += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (s[end] == '{')
|
||||
{
|
||||
if (end + 1 < s.Length && s[end + 1] == '{')
|
||||
{
|
||||
end += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
end = SkipInterpolationHole(s, end + 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (s[end] == '"' && end + 1 < s.Length && s[end + 1] == '"')
|
||||
{
|
||||
end += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (s[end] == '"')
|
||||
{
|
||||
return end + 1;
|
||||
}
|
||||
|
||||
end++;
|
||||
}
|
||||
|
||||
return s.Length;
|
||||
}
|
||||
|
||||
private static int SkipInterpolationHole(string s, int pos)
|
||||
{
|
||||
int depth = 1;
|
||||
int end = pos;
|
||||
|
||||
while (end < s.Length && depth > 0)
|
||||
{
|
||||
if (s[end] == '\\')
|
||||
{
|
||||
end += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (s[end] == '@' && end + 1 < s.Length && s[end + 1] == '"')
|
||||
{
|
||||
end = SkipVerbatimString(s, end + 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (s[end] == '$' && end + 1 < s.Length && s[end + 1] == '"')
|
||||
{
|
||||
end = SkipInterpolatedString(s, end + 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (s[end] == '"' && end + 2 < s.Length && s[end + 1] == '"' && s[end + 2] == '"')
|
||||
{
|
||||
end = SkipRawString(s, end + 3);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (s[end] == '"')
|
||||
{
|
||||
end = SkipRegularString(s, end + 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (s[end] == '\'')
|
||||
{
|
||||
end = SkipCharLiteral(s, end + 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (s[end] == '{')
|
||||
{
|
||||
depth++;
|
||||
end++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (s[end] == '}')
|
||||
{
|
||||
depth--;
|
||||
end++;
|
||||
continue;
|
||||
}
|
||||
|
||||
end++;
|
||||
}
|
||||
|
||||
return end;
|
||||
}
|
||||
|
||||
private static int SkipRegularString(string s, int pos)
|
||||
{
|
||||
int end = pos;
|
||||
while (end < s.Length && s[end] != '"')
|
||||
{
|
||||
if (s[end] == '\\')
|
||||
{
|
||||
end++;
|
||||
}
|
||||
|
||||
end++;
|
||||
}
|
||||
|
||||
return end < s.Length ? end + 1 : s.Length;
|
||||
}
|
||||
|
||||
private static int SkipVerbatimString(string s, int pos)
|
||||
{
|
||||
int end = pos;
|
||||
while (end < s.Length)
|
||||
{
|
||||
if (s[end] == '"')
|
||||
{
|
||||
if (end + 1 < s.Length && s[end + 1] == '"')
|
||||
{
|
||||
end += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
return end + 1;
|
||||
}
|
||||
|
||||
end++;
|
||||
}
|
||||
|
||||
return s.Length;
|
||||
}
|
||||
|
||||
private static int SkipRawString(string s, int pos)
|
||||
{
|
||||
int end = pos;
|
||||
while (end + 2 < s.Length)
|
||||
{
|
||||
if (s[end] == '"' && s[end + 1] == '"' && s[end + 2] == '"')
|
||||
{
|
||||
return end + 3;
|
||||
}
|
||||
|
||||
end++;
|
||||
}
|
||||
|
||||
return s.Length;
|
||||
}
|
||||
|
||||
private static int SkipCharLiteral(string s, int pos)
|
||||
{
|
||||
int end = pos;
|
||||
while (end < s.Length && s[end] != '\'')
|
||||
{
|
||||
if (s[end] == '\\')
|
||||
{
|
||||
end++;
|
||||
}
|
||||
|
||||
end++;
|
||||
}
|
||||
|
||||
return end < s.Length ? end + 1 : s.Length;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class SourceShapeResult
|
||||
{
|
||||
public List<string> UsingDirectives { get; } = new List<string>();
|
||||
public bool HasNamespaceDeclaration { get; set; }
|
||||
public bool HasTypeDeclaration { get; set; }
|
||||
public bool HasTopLevelStatements { get; set; }
|
||||
public StringBuilder TopLevelBodyBuilder { get; } = new StringBuilder();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user