using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
namespace MergeHub.Services
{
/// C# 여는 중괄호 · else/catch/finally 를 어디에 둘지.
public enum CSharpBraceStyle
{
/// 맞추지 않는다. 파일 그대로 견준다.
None = 0,
///
/// AURORA. Visual Studio [텍스트 편집기 > C# > 코드 스타일 > 서식 > 줄 바꿈] 에서
/// 형식 · 메서드와 로컬 함수의 여는 중괄호만 새 줄, 나머지(속성 · 접근자 · 익명 메서드 · 제어 블록 ·
/// 익명 형식 · 초기화 · 람다)는 같은 줄. else · catch · finally 는 같은 줄(} else {).
///
Aurora = 1,
/// Allman. 여는 중괄호는 모두 새 줄, else · catch · finally 도 새 줄(Visual Studio 기본값과 같다).
Allman = 2,
}
///
/// C# 코드의 중괄호 줄바꿈을 한 가지 스타일로 맞춘다(6차 추가 - C# 코드 스타일 맞춤).
///
/// 같은 코드인데 한쪽은 if (x) {, 다른 쪽은 if (x) + 다음 줄 { 로 쓰면
/// 줄 단위 비교에서 그 자리마다 차이가 둘씩 뜬다. 스타일이 다른 사람 코드를 병합할 때 진짜 변경이 묻힌다.
/// 그래서 비교하기 전에 양쪽을 같은 스타일로 맞춘다.
///
/// 줄의 위치만 옮긴다 - 여는 중괄호를 앞 줄 끝으로 올리거나 다음 줄로 내리고,
/// } 와 else/catch/finally 를 붙이거나 뗀다. 들여쓰기 · 공백 · 그 밖의 서식은 건드리지 않는다.
/// 주석 · 문자열 · 보간 문자열 · 전처리 줄 안의 중괄호는 코드로 보지 않는다.
/// 앞 줄 끝에 주석이 있으면 올리지 않는다(중괄호가 주석 안으로 들어가 버린다).
/// 스타일을 판단할 수 없는 중괄호(그냥 블록 · 중첩 배열 초기화)는 그대로 둔다.
///
public static class CSharpStyle
{
/// 중괄호가 여는 것의 갈래. Visual Studio 옵션의 항목과 같게 나눈다.
public enum BraceKind
{
/// 판단하지 않는다(그냥 블록 · 알 수 없음). 옮기지 않는다.
Unknown,
Type,
Method,
Property,
Accessor,
AnonymousMethod,
Control,
AnonymousType,
Initializer,
Lambda,
}
/// 이 파일에 스타일 맞춤을 걸지. C# 파일(.cs)만.
public static bool Applies(string? path, CSharpBraceStyle style) =>
style != CSharpBraceStyle.None && !string.IsNullOrEmpty(path) &&
string.Equals(Path.GetExtension(path), ".cs", StringComparison.OrdinalIgnoreCase);
public static string Label(CSharpBraceStyle style) => style switch
{
CSharpBraceStyle.Aurora => "AURORA",
CSharpBraceStyle.Allman => "Allman",
_ => "끔",
};
///
/// 글 전체를 스타일에 맞춘다. 바꿀 것이 없으면 받은 글을 그대로 돌려준다(줄끝도 그대로).
/// 바꾸면 줄끝은 글에 CRLF 가 하나라도 있으면 CRLF, 아니면 LF 로 잇는다.
///
public static string Format(string text, CSharpBraceStyle style)
{
if (style == CSharpBraceStyle.None || string.IsNullOrEmpty(text)) return text;
var newLine = text.IndexOf("\r\n", StringComparison.Ordinal) >= 0 ? "\r\n" : "\n";
var endsWithNewLine = text.EndsWith("\n", StringComparison.Ordinal);
var lines = new List(text.Replace("\r\n", "\n").Split('\n'));
if (endsWithNewLine) lines.RemoveAt(lines.Count - 1);
var any = false;
for (var pass = 0; pass < 8; pass++)
{
var changed = PlaceKeywords(lines, style);
changed |= PlaceBraces(lines, style);
if (!changed) break;
any = true;
}
if (!any) return text;
var result = string.Join(newLine, lines);
return endsWithNewLine ? result + newLine : result;
}
/// 그 갈래의 여는 중괄호를 새 줄에 둘지.
public static bool OpenBraceOnNewLine(BraceKind kind, CSharpBraceStyle style) =>
style == CSharpBraceStyle.Allman || kind is BraceKind.Type or BraceKind.Method;
// ================= else · catch · finally =================
private static readonly Regex KeywordLine = new Regex(@"^(else|catch|finally)\b", RegexOptions.Compiled);
private static readonly Regex CloseThenKeyword = new Regex(@"^\}\s*(else|catch|finally)\b", RegexOptions.Compiled);
private static bool PlaceKeywords(List lines, CSharpBraceStyle style)
{
var mask = Mask(lines);
var changed = false;
for (var i = lines.Count - 1; i >= 1; i--)
{
if (mask.Inside[i] || mask.Preprocessor[i]) continue;
var code = mask.Code[i].Trim();
if (style == CSharpBraceStyle.Aurora)
{
// } → } else {
// else {
if (!KeywordLine.IsMatch(code)) continue;
var p = i - 1;
if (mask.Inside[p] || mask.Preprocessor[p]) continue;
if (lines[p].Trim() != "}" || mask.Code[p].Trim() != "}") continue;
lines[p] = lines[p].TrimEnd() + " " + lines[i].TrimStart();
lines.RemoveAt(i);
changed = true;
}
else
{
// } else { → }
// else {
if (!CloseThenKeyword.IsMatch(code) || !lines[i].TrimStart().StartsWith("}", StringComparison.Ordinal)) continue;
var indent = Indent(lines[i]);
var rest = lines[i].TrimStart().Substring(1).TrimStart();
lines[i] = indent + "}";
lines.Insert(i + 1, indent + rest);
changed = true;
}
}
return changed;
}
// ================= 여는 중괄호 =================
private static bool PlaceBraces(List lines, CSharpBraceStyle style)
{
var mask = Mask(lines);
var changed = false;
for (var i = lines.Count - 1; i >= 0; i--)
{
if (mask.Inside[i] || mask.Preprocessor[i]) continue;
var code = mask.Code[i];
var first = FirstNonSpace(code);
if (first < 0) continue;
// (가) 줄이 '{' 로 시작한다 → 같은 줄 스타일이면 앞 줄 끝으로 올린다.
if (code[first] == '{')
{
if (i == 0) continue;
var p = i - 1;
if (mask.Inside[p] || mask.Preprocessor[p]) continue;
var prevCode = mask.Code[p].TrimEnd();
if (prevCode.Trim().Length == 0) continue;
// 앞 줄 끝에 주석이 있으면 올리지 않는다 - 원문 끝과 코드 끝이 같아야 한다.
if (lines[p].TrimEnd().Length != prevCode.Length) continue;
var (kind, _) = Classify(mask, i, first);
if (kind == BraceKind.Unknown || OpenBraceOnNewLine(kind, style)) continue;
lines[p] = lines[p].TrimEnd() + " " + lines[i].TrimStart();
lines.RemoveAt(i);
changed = true;
continue;
}
// (나) 코드 뒤 줄 끝에 '{' 가 있다 → 새 줄 스타일이면 다음 줄로 내린다.
var trimmed = code.TrimEnd();
if (trimmed.Length == 0 || trimmed[trimmed.Length - 1] != '{') continue;
var brace = trimmed.Length - 1;
if (code.Substring(0, brace).Trim().Length == 0) continue;
if (code.Substring(0, brace).TrimStart().StartsWith("}", StringComparison.Ordinal)) continue;
var (k, headerLine) = Classify(mask, i, brace);
if (k == BraceKind.Unknown || !OpenBraceOnNewLine(k, style)) continue;
var original = lines[i];
lines[i] = original.Substring(0, brace).TrimEnd();
lines.Insert(i + 1, Indent(lines[headerLine]) + original.Substring(brace));
changed = true;
}
return changed;
}
///
/// (line, col) 의 여는 중괄호가 무엇을 여는지. 앞쪽 코드를 거꾸로 읽어 머리(선언 · 제어문 등)를 모은다.
/// 머리가 시작한 줄 번호도 준다 - 중괄호를 내릴 때 그 줄의 들여쓰기를 따른다.
///
private static (BraceKind Kind, int HeaderLine) Classify(Masked mask, int line, int col)
{
var chars = new List(128);
int paren = 0, bracket = 0, angle = 0;
var start = line;
var pos = col - 1;
var l = line;
while (chars.Count < 4000)
{
if (pos < 0)
{
l--;
if (l < 0 || mask.Preprocessor[l]) break;
pos = mask.Code[l].Length - 1;
chars.Add(' ');
continue;
}
var text = mask.Code[l];
var ch = text[pos];
if (ch == ')') paren++;
else if (ch == '(') { if (paren == 0) break; paren--; }
else if (ch == ']') bracket++;
else if (ch == '[') { if (bracket == 0) break; bracket--; }
else if (ch == '>' && !(pos > 0 && (text[pos - 1] == '=' || text[pos - 1] == '-')) &&
!(pos + 1 < text.Length && text[pos + 1] == '='))
angle++;
else if (ch == '<') { if (angle > 0) angle--; }
else if (paren == 0 && bracket == 0 && (ch == ';' || ch == '{' || ch == '}')) break;
else if (paren == 0 && bracket == 0 && angle == 0 && ch == ',') break;
if (!char.IsWhiteSpace(ch)) start = l;
chars.Add(ch);
pos--;
}
chars.Reverse();
return (KindOfHeader(new string(chars.ToArray())), start);
}
private static readonly HashSet ControlWords = new HashSet(StringComparer.Ordinal)
{
"if", "else", "for", "foreach", "while", "do", "switch", "try", "catch", "finally",
"using", "lock", "fixed", "unsafe", "checked", "unchecked",
};
private static readonly Regex Spaces = new Regex(@"\s+", RegexOptions.Compiled);
private static readonly Regex CaseLabel = new Regex(@"^(case\b.*|default)\s*:$", RegexOptions.Compiled);
private static readonly Regex AccessorHeader = new Regex(@"^((public|private|protected|internal)\s+)*(get|set|init|add|remove)$", RegexOptions.Compiled);
private static readonly Regex TypeKeyword = new Regex(@"\b(class|struct|interface|enum|record|namespace)\b", RegexOptions.Compiled);
private static readonly Regex WhereClause = new Regex(@"\bwhere\s+\w+\s*:", RegexOptions.Compiled);
/// 중괄호 앞 머리 글로 갈래를 정한다. 검사에서 직접 부를 수 있게 열어 둔다.
public static BraceKind KindOfHeader(string header)
{
var h = Spaces.Replace(header ?? string.Empty, " ").Trim();
// 앞에 붙은 특성([Test] 등)은 뗀다.
while (h.StartsWith("[", StringComparison.Ordinal))
{
var close = MatchingClose(h, 0, '[', ']');
if (close < 0) break;
h = h.Substring(close + 1).TrimStart();
}
if (h.Length == 0) return BraceKind.Unknown;
if (h.EndsWith("=>", StringComparison.Ordinal)) return BraceKind.Lambda;
if (CaseLabel.IsMatch(h)) return BraceKind.Control;
var firstWord = Regex.Match(h, @"^[A-Za-z_]\w*").Value;
if (ControlWords.Contains(firstWord)) return BraceKind.Control;
if (AccessorHeader.IsMatch(h)) return BraceKind.Accessor;
var paren = h.IndexOf('(');
var type = TypeKeyword.Match(h);
if (type.Success && (paren < 0 || type.Index < paren)) return BraceKind.Type;
if (Regex.IsMatch(h, @"\bdelegate\b")) return BraceKind.AnonymousMethod;
// 제약이 붙은 제네릭 메서드: void M() where T : new()
if (WhereClause.IsMatch(h) && paren >= 0) return BraceKind.Method;
if (h == "new") return BraceKind.AnonymousType;
if (EndsWithCreation(h)) return BraceKind.Initializer;
if (HasAssignment(h)) return BraceKind.Initializer;
if (h.EndsWith("switch", StringComparison.Ordinal)) return BraceKind.Initializer;
if (h.EndsWith(")", StringComparison.Ordinal)) return BraceKind.Method;
var last = h[h.Length - 1];
if (char.IsLetterOrDigit(last) || last == '_' || last == '>' || last == ']' || last == '?') return BraceKind.Property;
return BraceKind.Unknown;
}
///
/// 머리가 new 형식 · new 형식(인자) · new[] · new() 로 끝나는지 - 개체 · 컬렉션 · 배열 초기화.
/// 멤버 숨기기 한정자(public new void M())는 new 뒤에 낱말이 둘 이상이라 아니다.
///
private static bool EndsWithCreation(string h)
{
var at = LastWord(h, "new");
if (at < 0) return false;
var rest = h.Substring(at + 3).Trim();
// 끝의 (인자) 한 덩어리를 뗀다.
if (rest.EndsWith(")", StringComparison.Ordinal))
{
var open = MatchingOpen(rest, rest.Length - 1, '(', ')');
if (open < 0) return false;
rest = rest.Substring(0, open).Trim();
}
if (rest.Length == 0) return true; // new() · new (...)
// 형식 이름 하나(점 · 제네릭 · 배열 표시 포함), 공백으로 나뉜 낱말이 없어야 한다.
var depth = 0;
foreach (var c in rest)
{
if (c == '<' || c == '[') depth++;
else if (c == '>' || c == ']') depth--;
else if (c == ' ' && depth == 0) return false;
else if (depth == 0 && !(char.IsLetterOrDigit(c) || c == '_' || c == '.' || c == '?')) return false;
}
return true;
}
/// 괄호 밖에 대입 '=' 가 있는지(==, !=, <=, >=, => 는 빼고).
private static bool HasAssignment(string h)
{
var depth = 0;
for (var i = 0; i < h.Length; i++)
{
var c = h[i];
if (c == '(' || c == '[') depth++;
else if (c == ')' || c == ']') depth--;
else if (c == '=' && depth == 0)
{
var prev = i > 0 ? h[i - 1] : ' ';
var next = i + 1 < h.Length ? h[i + 1] : ' ';
if (next == '=' || next == '>' || prev == '=' || prev == '!' || prev == '<' || prev == '>') continue;
return true;
}
}
return false;
}
private static int LastWord(string h, string word)
{
var at = h.Length;
while (true)
{
at = h.LastIndexOf(word, at - 1, StringComparison.Ordinal);
if (at < 0) return -1;
var before = at == 0 || !(char.IsLetterOrDigit(h[at - 1]) || h[at - 1] == '_');
var after = at + word.Length >= h.Length || !(char.IsLetterOrDigit(h[at + word.Length]) || h[at + word.Length] == '_');
if (before && after) return at;
if (at == 0) return -1;
}
}
private static int MatchingClose(string s, int open, char o, char c)
{
var depth = 0;
for (var i = open; i < s.Length; i++)
{
if (s[i] == o) depth++;
else if (s[i] == c && --depth == 0) return i;
}
return -1;
}
private static int MatchingOpen(string s, int close, char o, char c)
{
var depth = 0;
for (var i = close; i >= 0; i--)
{
if (s[i] == c) depth++;
else if (s[i] == o && --depth == 0) return i;
}
return -1;
}
private static int FirstNonSpace(string s)
{
for (var i = 0; i < s.Length; i++)
if (!char.IsWhiteSpace(s[i])) return i;
return -1;
}
private static string Indent(string line) => line.Substring(0, line.Length - line.TrimStart().Length);
// ================= 주석 · 문자열 가리기 =================
/// 줄마다 코드만 남긴 글(주석은 공백, 문자열 속은 '_'). 길이는 원문과 같다.
private sealed class Masked
{
public Masked(string[] code, bool[] inside, bool[] preprocessor)
{
Code = code;
Inside = inside;
Preprocessor = preprocessor;
}
public string[] Code { get; }
/// 줄이 블록 주석 · 여러 줄 문자열 안에서 시작하는지.
public bool[] Inside { get; }
public bool[] Preprocessor { get; }
}
private enum Mode { Code, LineComment, BlockComment, Str, VerbatimStr, InterpStr, InterpVerbatimStr, Char }
private static Masked Mask(IReadOnlyList lines)
{
var src = string.Join("\n", lines);
var mask = src.ToCharArray();
var modes = new Stack();
modes.Push(Mode.Code);
var holes = new Stack(); // 보간 구멍마다 그 안의 중괄호 깊이
var inside = new bool[lines.Count];
var line = 0;
var n = src.Length;
var i = 0;
while (i < n)
{
var ch = src[i];
var next = i + 1 < n ? src[i + 1] : '\0';
var next2 = i + 2 < n ? src[i + 2] : '\0';
if (ch == '\n')
{
var top = modes.Peek();
if (top is Mode.LineComment or Mode.Str or Mode.Char or Mode.InterpStr) { modes.Pop(); top = modes.Peek(); }
line++;
if (line < inside.Length) inside[line] = top != Mode.Code || holes.Count > 0;
i++;
continue;
}
switch (modes.Peek())
{
case Mode.Code:
if (ch == '/' && next == '/') { modes.Push(Mode.LineComment); mask[i] = ' '; mask[i + 1] = ' '; i += 2; continue; }
if (ch == '/' && next == '*') { modes.Push(Mode.BlockComment); mask[i] = ' '; mask[i + 1] = ' '; i += 2; continue; }
if ((ch == '$' && next == '@' && next2 == '"') || (ch == '@' && next == '$' && next2 == '"'))
{ modes.Push(Mode.InterpVerbatimStr); Hide(mask, i, 3, holes); i += 3; continue; }
if (ch == '$' && next == '"') { modes.Push(Mode.InterpStr); Hide(mask, i, 2, holes); i += 2; continue; }
if (ch == '@' && next == '"') { modes.Push(Mode.VerbatimStr); Hide(mask, i, 2, holes); i += 2; continue; }
if (ch == '"') { modes.Push(Mode.Str); Hide(mask, i, 1, holes); i++; continue; }
if (ch == '\'') { modes.Push(Mode.Char); Hide(mask, i, 1, holes); i++; continue; }
if (holes.Count > 0)
{
if (ch == '{') holes.Push(holes.Pop() + 1);
else if (ch == '}')
{
var depth = holes.Pop();
if (depth == 0) { modes.Pop(); mask[i] = '_'; i++; continue; }
holes.Push(depth - 1);
}
mask[i] = '_';
}
i++;
continue;
case Mode.LineComment:
mask[i] = ' ';
i++;
continue;
case Mode.BlockComment:
if (ch == '*' && next == '/') { mask[i] = ' '; mask[i + 1] = ' '; modes.Pop(); i += 2; continue; }
mask[i] = ' ';
i++;
continue;
case Mode.Str:
case Mode.Char:
var quote = modes.Peek() == Mode.Str ? '"' : '\'';
if (ch == '\\') { mask[i] = '_'; if (i + 1 < n && next != '\n') mask[i + 1] = '_'; i += next == '\n' ? 1 : 2; continue; }
if (ch == quote) { modes.Pop(); mask[i] = '_'; i++; continue; }
mask[i] = '_';
i++;
continue;
case Mode.VerbatimStr:
if (ch == '"' && next == '"') { mask[i] = '_'; mask[i + 1] = '_'; i += 2; continue; }
if (ch == '"') modes.Pop();
mask[i] = '_';
i++;
continue;
case Mode.InterpStr:
case Mode.InterpVerbatimStr:
var verbatim = modes.Peek() == Mode.InterpVerbatimStr;
if ((ch == '{' && next == '{') || (ch == '}' && next == '}') || (verbatim && ch == '"' && next == '"'))
{ mask[i] = '_'; mask[i + 1] = '_'; i += 2; continue; }
if (!verbatim && ch == '\\') { mask[i] = '_'; if (next != '\n' && i + 1 < n) mask[i + 1] = '_'; i += next == '\n' ? 1 : 2; continue; }
if (ch == '"') { modes.Pop(); mask[i] = '_'; i++; continue; }
if (ch == '{') { modes.Push(Mode.Code); holes.Push(0); mask[i] = '_'; i++; continue; }
mask[i] = '_';
i++;
continue;
}
}
var code = new string(mask).Split('\n');
var preprocessor = new bool[code.Length];
for (var k = 0; k < code.Length; k++)
preprocessor[k] = !inside[k] && code[k].TrimStart().StartsWith("#", StringComparison.Ordinal);
return new Masked(code, inside, preprocessor);
}
/// 문자열 여는 기호를 가린다. 보간 구멍 안에서 연 문자열이면 원래도 가려져야 할 자리다.
private static void Hide(char[] mask, int at, int count, Stack holes)
{
for (var k = 0; k < count && at + k < mask.Length; k++) mask[at + k] = '_';
}
}
}