Files

592 lines
20 KiB
C#

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();
}
}