- [이 메서드 반영]: 캐럿이 놓인 클래스 멤버를 통째로(사용자 결정: C# + C/C++, 메서드 · 생성자 · 속성 · 이벤트 · 연산자 · 필드 · enum, 로컬 함수 안이면 감싸는 멤버) - MemberRanges: Roslyn 없이 주석 · 문자열(축자 · 보간 · raw · C++ raw · 숫자 구분자)을 가리고 #if 갈래마다 다시 읽어 중괄호 짝으로 범위. 못 잡으면 단추를 끄고 까닭 한 줄 - 덩어리 · 메서드 반영(ApplyGroup): 머지 주석 한 줄(메서드 이름 위 · 덩어리 첫 줄 위), 로그 한 줄, Undo · Redo 한 번(LineEdit.Group). 덩어리가 줄마다 주석을 달던 것도 고침. 주석만 든 편집(NoteOnly)은 직접 수정 · 편집 수에 안 셈 - 메서드 반영 저장본 = 같은 줄 줄 단위 반영 저장본(바이트). MemberTest 83건(23벌 1,395건) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
824 lines
34 KiB
C#
824 lines
34 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
|
|
namespace MergeHub.Services
|
|
{
|
|
/// <summary>메서드 단위 반영을 할 수 있는 언어(11차 F-2).</summary>
|
|
public enum MemberLanguage { None, CSharp, Cpp }
|
|
|
|
/// <summary>
|
|
/// 클래스 멤버 하나가 차지하는 줄(0부터). <see cref="Start"/> 는 바로 위 설명 주석 · 특성 · 감싸는 <c>#if</c> 까지,
|
|
/// <see cref="NameLine"/> 은 이름이 있는 선언 줄(머지 주석이 들어갈 자리), <see cref="End"/> 는 닫는 <c>}</c> 나 <c>;</c> 줄.
|
|
/// </summary>
|
|
public sealed record MemberRange(int Start, int NameLine, int End, string Name)
|
|
{
|
|
public bool Contains(int line) => line >= Start && line <= End;
|
|
}
|
|
|
|
/// <summary>
|
|
/// C# · C/C++ 소스에서 <b>클래스 멤버</b>(메서드 · 생성자 · 소멸자 · 속성 · 인덱서 · 이벤트 · 연산자 · 필드 · enum)의
|
|
/// 줄 범위를 잡는다(11차 F-2). Roslyn 없이 중괄호 짝으로 센다.
|
|
///
|
|
/// 속지 않아야 할 것:
|
|
/// · 문자열 · 문자 · 주석 속 중괄호 - 먼저 가린다(<see cref="Mask"/>). C# 축자 · 보간 · raw(<c>"""</c>),
|
|
/// C++ raw(<c>R"x(...)x"</c>) · 숫자 구분자(<c>1'000</c>)까지.
|
|
/// · <c>#if</c> 로 갈린 줄 - 갈래(#elif · #else)마다 <c>#if</c> 자리의 상태에서 다시 읽고, <c>#endif</c> 뒤에는 첫 갈래의
|
|
/// 상태로 이어 간다. 여는 줄만 갈린 메서드(<c>void A(int x) {</c> / <c>void A() {</c>)도 짝이 맞는다.
|
|
/// · 필드 초기화 · 람다 · switch 식 · C++ 생성자 초기화 목록의 중괄호 - 멤버 몸통이 아니다.
|
|
///
|
|
/// 로컬 함수 · 람다는 멤버가 아니다 - 그 안에 있으면 감싸는 멤버 통째다(사용자 결정 "클래스 멤버 전부").
|
|
/// 클래스 · 네임스페이스 선언 줄 자체는 어느 멤버에도 들지 않는다. 범위를 못 잡으면 없음으로 둔다 -
|
|
/// 틀린 범위를 반영하는 것보다 단추를 끄는 편이 낫다.
|
|
/// </summary>
|
|
public static class MemberRanges
|
|
{
|
|
public static MemberLanguage LanguageOf(string? path)
|
|
{
|
|
switch (Path.GetExtension(path ?? string.Empty).ToLowerInvariant())
|
|
{
|
|
case ".cs":
|
|
return MemberLanguage.CSharp;
|
|
case ".c":
|
|
case ".cc":
|
|
case ".cpp":
|
|
case ".cxx":
|
|
case ".c++":
|
|
case ".h":
|
|
case ".hh":
|
|
case ".hpp":
|
|
case ".hxx":
|
|
case ".inl":
|
|
return MemberLanguage.Cpp;
|
|
default:
|
|
return MemberLanguage.None;
|
|
}
|
|
}
|
|
|
|
/// <summary>줄 목록에서 멤버 범위들(시작 줄 순).</summary>
|
|
public static IReadOnlyList<MemberRange> Find(IReadOnlyList<string> lines, MemberLanguage language)
|
|
{
|
|
if (language == MemberLanguage.None || lines.Count == 0) return Array.Empty<MemberRange>();
|
|
|
|
var masked = Mask(lines, language);
|
|
var raw = new List<Raw>();
|
|
new Scanner(masked, language, raw).Run();
|
|
|
|
// 같은 시작 줄이 두 갈래에서 두 번 닫힐 수 있다 - 늦게 닫힌 쪽(범위가 넓은 쪽)을 쓴다.
|
|
var members = raw
|
|
.GroupBy(r => r.DeclStart)
|
|
.Select(g => g.OrderByDescending(r => r.End).First())
|
|
.Select(r => Finish(r, lines, masked, language))
|
|
.OrderBy(m => m.Start)
|
|
.ToList();
|
|
|
|
// 겹치면 뒤의 것을 버린다(짝이 틀어진 파일). 겹친 범위로 반영하는 것보다 안 잡는 편이 낫다.
|
|
var result = new List<MemberRange>(members.Count);
|
|
foreach (var m in members)
|
|
{
|
|
if (result.Count > 0 && m.Start <= result[result.Count - 1].End) continue;
|
|
result.Add(m);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>그 줄(0부터)을 품은 멤버.</summary>
|
|
public static MemberRange? At(IReadOnlyList<MemberRange> members, int line) =>
|
|
members.FirstOrDefault(m => m.Contains(line));
|
|
|
|
/// <summary>
|
|
/// 화면 행의 줄 번호(반영해 올 쪽, 1부터 · 없으면 null)로 캐럿 행이 속한 멤버를 찾는다.
|
|
/// 그쪽에 없는 행(내 쪽에만 있는 줄)은 위아래로 가장 가까운 행이 <b>같은 멤버</b>일 때만 그 멤버다.
|
|
/// </summary>
|
|
public static MemberRange? AtRow(IReadOnlyList<int?> sourceNumbers, int row, IReadOnlyList<MemberRange> members)
|
|
{
|
|
if (row < 0 || row >= sourceNumbers.Count) return null;
|
|
if (sourceNumbers[row] is int here) return At(members, here - 1);
|
|
|
|
int? up = null, down = null;
|
|
for (var r = row - 1; r >= 0; r--) if (sourceNumbers[r] is int n) { up = n; break; }
|
|
for (var r = row + 1; r < sourceNumbers.Count; r++) if (sourceNumbers[r] is int n) { down = n; break; }
|
|
if (up is null || down is null) return null;
|
|
|
|
var a = At(members, up.Value - 1);
|
|
var b = At(members, down.Value - 1);
|
|
return a is not null && ReferenceEquals(a, b) ? a : null;
|
|
}
|
|
|
|
/// <summary>그 멤버가 차지하는 화면 행(처음 · 끝). 사이의 내 쪽에만 있는 행도 든다.</summary>
|
|
public static (int First, int Last)? RowsOf(IReadOnlyList<int?> sourceNumbers, MemberRange member)
|
|
{
|
|
int first = -1, last = -1;
|
|
for (var r = 0; r < sourceNumbers.Count; r++)
|
|
{
|
|
if (sourceNumbers[r] is not int n || n - 1 < member.Start || n - 1 > member.End) continue;
|
|
if (first < 0) first = r;
|
|
last = r;
|
|
}
|
|
|
|
return first < 0 ? null : (first, last);
|
|
}
|
|
|
|
// ================= 범위 다듬기 =================
|
|
|
|
private static MemberRange Finish(Raw r, IReadOnlyList<string> lines, Masked masked, MemberLanguage language)
|
|
{
|
|
var nameLine = r.DeclStart;
|
|
for (var l = r.DeclStart; l <= r.End; l++)
|
|
{
|
|
if (masked.Preprocessor[l]) continue;
|
|
var code = masked.Code[l].Trim();
|
|
if (code.Length == 0 || IsAttributeOnly(code) || IsTemplateOnly(code)) continue;
|
|
nameLine = l;
|
|
break;
|
|
}
|
|
|
|
var start = r.DeclStart;
|
|
var notePath = language == MemberLanguage.CSharp ? "a.cs" : "a.cpp";
|
|
while (true)
|
|
{
|
|
var above = start - 1;
|
|
if (above < 0) break;
|
|
|
|
// 이 멤버를 감싸는 #if (짝 #endif 가 멤버 안) - 여는 줄만 갈린 메서드
|
|
if (masked.Preprocessor[above] && masked.IfEnd.TryGetValue(above, out var endif) && endif <= r.End)
|
|
{
|
|
start = above;
|
|
continue;
|
|
}
|
|
|
|
// 바로 위 주석 줄(설명 · /// 문서). 빈 줄 · 머지 주석에서 멈춘다 - 머지 주석은 이력이라 반영으로 지우면 안 된다.
|
|
if (!masked.Preprocessor[above] && masked.Code[above].Trim().Length == 0 &&
|
|
lines[above].Trim().Length > 0 && !MergeNote.LooksLikeNote(lines[above], notePath))
|
|
{
|
|
start = above;
|
|
continue;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
return new MemberRange(start, nameLine, r.End, NameOf(r.Header));
|
|
}
|
|
|
|
private static bool IsAttributeOnly(string code)
|
|
{
|
|
if (!code.StartsWith("[", StringComparison.Ordinal)) return false;
|
|
var depth = 0;
|
|
for (var i = 0; i < code.Length; i++)
|
|
{
|
|
if (code[i] == '[') depth++;
|
|
else if (code[i] == ']') depth--;
|
|
else if (depth == 0 && !char.IsWhiteSpace(code[i])) return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static bool IsTemplateOnly(string code) =>
|
|
code.StartsWith("template", StringComparison.Ordinal) && code.EndsWith(">", StringComparison.Ordinal);
|
|
|
|
private static readonly Regex Operator = new Regex(@"\boperator\s*([^\s(]+|\(\))", RegexOptions.Compiled);
|
|
private static readonly Regex Ident = new Regex(@"[~A-Za-z_][\w:~]*$", RegexOptions.Compiled);
|
|
private static readonly Regex EnumName = new Regex(@"\benum\s+(?:class\s+|struct\s+)?([A-Za-z_]\w*)", RegexOptions.Compiled);
|
|
|
|
/// <summary>토스트 · 로그에 쓸 이름. 틀려도 범위에는 영향이 없다.</summary>
|
|
public static string NameOf(string header)
|
|
{
|
|
var h = Regex.Replace(header ?? string.Empty, @"\s+", " ").Trim();
|
|
|
|
// 앞에 붙은 특성([Obsolete("...")] · [[nodiscard]])은 뗀다 - 그 안의 괄호를 메서드 괄호로 읽는다.
|
|
while (h.StartsWith("[", StringComparison.Ordinal))
|
|
{
|
|
var depth = 0;
|
|
var close = -1;
|
|
for (var k = 0; k < h.Length; k++)
|
|
{
|
|
if (h[k] == '[') depth++;
|
|
else if (h[k] == ']' && --depth == 0) { close = k; break; }
|
|
}
|
|
|
|
if (close < 0) break;
|
|
h = h.Substring(close + 1).TrimStart();
|
|
}
|
|
|
|
var en = EnumName.Match(h);
|
|
if (en.Success) return en.Groups[1].Value;
|
|
|
|
var op = Operator.Match(h);
|
|
if (op.Success) return "operator " + op.Groups[1].Value;
|
|
|
|
if (h.Contains("this[")) return "this[]";
|
|
|
|
var cut = h.Length;
|
|
foreach (var stop in new[] { "=>", "=", "(", "{", ";" })
|
|
{
|
|
var at = TopLevelIndex(h, stop);
|
|
if (at >= 0 && at < cut) cut = at;
|
|
}
|
|
|
|
var head = StripGenerics(h.Substring(0, cut).TrimEnd());
|
|
var m = Ident.Match(head);
|
|
return m.Success ? m.Value : head;
|
|
}
|
|
|
|
private static int TopLevelIndex(string s, string token)
|
|
{
|
|
var angle = 0;
|
|
for (var i = 0; i + token.Length <= s.Length; i++)
|
|
{
|
|
if (s[i] == '<') angle++;
|
|
else if (s[i] == '>' && angle > 0) angle--;
|
|
if (angle == 0 && string.CompareOrdinal(s, i, token, 0, token.Length) == 0) return i;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
private static string StripGenerics(string s)
|
|
{
|
|
while (s.EndsWith(">", StringComparison.Ordinal) || s.EndsWith("]", StringComparison.Ordinal))
|
|
{
|
|
var close = s[s.Length - 1];
|
|
var open = close == '>' ? '<' : '[';
|
|
var depth = 0;
|
|
var i = s.Length - 1;
|
|
for (; i >= 0; i--)
|
|
{
|
|
if (s[i] == close) depth++;
|
|
else if (s[i] == open && --depth == 0) break;
|
|
}
|
|
|
|
if (i <= 0) break;
|
|
s = s.Substring(0, i).TrimEnd();
|
|
}
|
|
|
|
return s;
|
|
}
|
|
|
|
// ================= 훑기 =================
|
|
|
|
private sealed class Raw
|
|
{
|
|
public int DeclStart;
|
|
public int End;
|
|
public string Header = string.Empty;
|
|
}
|
|
|
|
private enum FrameKind { Container, Member, Init }
|
|
|
|
private sealed class Frame
|
|
{
|
|
public FrameKind Kind;
|
|
public int Depth;
|
|
public int DeclStart;
|
|
public string Header = string.Empty;
|
|
|
|
public Frame Clone() => (Frame)MemberwiseClone();
|
|
}
|
|
|
|
/// <summary>훑는 도중의 상태. #if 갈래마다 떠 놓고 되돌린다.</summary>
|
|
private sealed class State
|
|
{
|
|
public List<Frame> Stack = new List<Frame> { new Frame { Kind = FrameKind.Container } };
|
|
public int DeclStart = -1;
|
|
public StringBuilder Decl = new StringBuilder();
|
|
public int Paren;
|
|
public int Bracket;
|
|
public bool Assign;
|
|
public bool Arrow;
|
|
public bool InitList;
|
|
public char LastSignificant;
|
|
public Raw? Pending;
|
|
|
|
public State Clone()
|
|
{
|
|
var c = (State)MemberwiseClone();
|
|
c.Stack = Stack.Select(f => f.Clone()).ToList();
|
|
c.Decl = new StringBuilder(Decl.ToString());
|
|
return c;
|
|
}
|
|
|
|
public void ResetDecl()
|
|
{
|
|
DeclStart = -1;
|
|
Decl.Clear();
|
|
Paren = Bracket = 0;
|
|
Assign = Arrow = InitList = false;
|
|
LastSignificant = '\0';
|
|
}
|
|
}
|
|
|
|
private sealed class Branch
|
|
{
|
|
public State Snapshot = null!;
|
|
public State? FirstEnd;
|
|
}
|
|
|
|
private static readonly Regex AccessLabel = new Regex(@"^(public|private|protected)(\s+(slots|Q_SLOTS))?$|^(signals|Q_SIGNALS)$", RegexOptions.Compiled);
|
|
private static readonly Regex TypeWord = new Regex(@"\b(class|struct|interface|record|namespace|union)\b", RegexOptions.Compiled);
|
|
private static readonly Regex EnumWord = new Regex(@"\benum\b", RegexOptions.Compiled);
|
|
private static readonly Regex TemplatePrefix = new Regex(@"^\s*template\s*<", RegexOptions.Compiled);
|
|
|
|
private sealed class Scanner
|
|
{
|
|
private readonly Masked _m;
|
|
private readonly MemberLanguage _lang;
|
|
private readonly List<Raw> _out;
|
|
private State _s = new State();
|
|
private readonly Stack<Branch> _branches = new Stack<Branch>();
|
|
|
|
public Scanner(Masked masked, MemberLanguage language, List<Raw> output)
|
|
{
|
|
_m = masked;
|
|
_lang = language;
|
|
_out = output;
|
|
}
|
|
|
|
public void Run()
|
|
{
|
|
for (var line = 0; line < _m.Code.Length; line++)
|
|
{
|
|
if (_m.Preprocessor[line])
|
|
{
|
|
Directive(_m.Code[line]);
|
|
continue;
|
|
}
|
|
|
|
var text = _m.Code[line];
|
|
for (var i = 0; i < text.Length; i++) Step(text, i, line);
|
|
if (_s.DeclStart >= 0) _s.Decl.Append(' ');
|
|
}
|
|
|
|
if (_s.Pending is not null) Emit(_s.Pending);
|
|
}
|
|
|
|
private void Directive(string code)
|
|
{
|
|
var word = Regex.Match(code.TrimStart().TrimStart('#').TrimStart(), @"^[a-z]+").Value;
|
|
switch (word)
|
|
{
|
|
case "if":
|
|
case "ifdef":
|
|
case "ifndef":
|
|
_branches.Push(new Branch { Snapshot = _s.Clone() });
|
|
break;
|
|
case "elif":
|
|
case "elifdef":
|
|
case "elifndef":
|
|
case "else":
|
|
if (_branches.Count == 0) break;
|
|
var b = _branches.Peek();
|
|
b.FirstEnd ??= _s;
|
|
_s = b.Snapshot.Clone();
|
|
break;
|
|
case "endif":
|
|
if (_branches.Count == 0) break;
|
|
var done = _branches.Pop();
|
|
if (done.FirstEnd is not null)
|
|
{
|
|
// 다른 갈래에서 떠 있던 선언 · 끝난 몸통은 버리고 첫 갈래로 잇는다.
|
|
// 다른 갈래에서 온전히 닫힌 멤버는 이미 내보냈다.
|
|
if (_s.Pending is not null && done.FirstEnd.Pending?.DeclStart != _s.Pending.DeclStart) Emit(_s.Pending);
|
|
_s = done.FirstEnd;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
private Frame Top => _s.Stack[_s.Stack.Count - 1];
|
|
|
|
private void Step(string text, int i, int line)
|
|
{
|
|
var ch = text[i];
|
|
var top = Top;
|
|
|
|
if (top.Kind == FrameKind.Member)
|
|
{
|
|
if (ch == '{') top.Depth++;
|
|
else if (ch == '}' && --top.Depth == 0)
|
|
{
|
|
_s.Stack.RemoveAt(_s.Stack.Count - 1);
|
|
_s.Pending = new Raw { DeclStart = top.DeclStart, End = line, Header = top.Header };
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (top.Kind == FrameKind.Init)
|
|
{
|
|
_s.Decl.Append(ch);
|
|
if (ch == '{') top.Depth++;
|
|
else if (ch == '}' && --top.Depth == 0)
|
|
{
|
|
_s.Stack.RemoveAt(_s.Stack.Count - 1);
|
|
_s.LastSignificant = '}';
|
|
}
|
|
return;
|
|
}
|
|
|
|
// ---- 멤버 자리(클래스 · 네임스페이스 · 파일 바로 안) ----
|
|
if (char.IsWhiteSpace(ch))
|
|
{
|
|
// 낱말 사이는 한 칸으로 남긴다 - "namespace weld" 가 붙으면 선언 낱말을 못 알아본다.
|
|
if (_s.DeclStart >= 0 && _s.Decl.Length > 0 && _s.Decl[_s.Decl.Length - 1] != ' ') _s.Decl.Append(' ');
|
|
return;
|
|
}
|
|
|
|
if (_s.Pending is not null)
|
|
{
|
|
// 속성 몸통 뒤 초기화: public int X { get; set; } = 1;
|
|
if (ch == '=' && Next(text, i) != '=')
|
|
{
|
|
var p = _s.Pending;
|
|
_s.Pending = null;
|
|
_s.ResetDecl();
|
|
_s.DeclStart = p.DeclStart;
|
|
_s.Decl.Append(p.Header).Append(" {} ");
|
|
_s.Assign = true;
|
|
_s.Decl.Append(ch);
|
|
_s.LastSignificant = ch;
|
|
return;
|
|
}
|
|
|
|
Emit(_s.Pending);
|
|
_s.Pending = null;
|
|
}
|
|
|
|
if (_s.DeclStart < 0) _s.DeclStart = line;
|
|
|
|
if (ch == '(') _s.Paren++;
|
|
else if (ch == ')') { if (_s.Paren > 0) _s.Paren--; }
|
|
else if (ch == '[') _s.Bracket++;
|
|
else if (ch == ']') { if (_s.Bracket > 0) _s.Bracket--; }
|
|
|
|
if (_s.Paren > 0 || _s.Bracket > 0 || ch == ')' || ch == ']')
|
|
{
|
|
_s.Decl.Append(ch);
|
|
_s.LastSignificant = ch;
|
|
return;
|
|
}
|
|
|
|
switch (ch)
|
|
{
|
|
case ';':
|
|
var header = _s.Decl.ToString();
|
|
if (!IsNotMember(header)) Emit(new Raw { DeclStart = _s.DeclStart, End = line, Header = header });
|
|
_s.ResetDecl();
|
|
return;
|
|
|
|
case ':':
|
|
if (_lang == MemberLanguage.Cpp && AccessLabel.IsMatch(_s.Decl.ToString().Trim()))
|
|
{
|
|
_s.ResetDecl();
|
|
return;
|
|
}
|
|
if (_lang == MemberLanguage.Cpp && Next(text, i) != ':' && Prev(text, i) != ':' && _s.Decl.ToString().Contains(")"))
|
|
_s.InitList = true;
|
|
break;
|
|
|
|
case '=':
|
|
if (_lang == MemberLanguage.CSharp && Next(text, i) == '>') _s.Arrow = true;
|
|
else if (IsAssignment(text, i)) _s.Assign = true;
|
|
break;
|
|
|
|
case '{':
|
|
Open(line);
|
|
return;
|
|
|
|
case '}':
|
|
// 멤버 자리에서 닫히는 중괄호는 감싸던 클래스 · 네임스페이스의 끝이다.
|
|
_s.ResetDecl();
|
|
if (_s.Stack.Count > 1) _s.Stack.RemoveAt(_s.Stack.Count - 1);
|
|
return;
|
|
}
|
|
|
|
_s.Decl.Append(ch);
|
|
_s.LastSignificant = ch;
|
|
}
|
|
|
|
private void Open(int line)
|
|
{
|
|
var header = _s.Decl.ToString();
|
|
var prev = _s.LastSignificant;
|
|
|
|
var initBrace = _s.Assign || _s.Arrow ||
|
|
(_lang == MemberLanguage.Cpp && _s.InitList && (char.IsLetterOrDigit(prev) || prev == '_' || prev == '>'));
|
|
if (initBrace)
|
|
{
|
|
_s.Decl.Append('{');
|
|
_s.Stack.Add(new Frame { Kind = FrameKind.Init, Depth = 1 });
|
|
return;
|
|
}
|
|
|
|
var words = WordsBeforeParen(header);
|
|
if (!EnumWord.IsMatch(words) && (TypeWord.IsMatch(words) || IsExternBlock(header)))
|
|
{
|
|
_s.ResetDecl();
|
|
_s.Stack.Add(new Frame { Kind = FrameKind.Container });
|
|
return;
|
|
}
|
|
|
|
_s.Stack.Add(new Frame { Kind = FrameKind.Member, Depth = 1, DeclStart = _s.DeclStart < 0 ? line : _s.DeclStart, Header = header });
|
|
_s.ResetDecl();
|
|
}
|
|
|
|
private void Emit(Raw raw) => _out.Add(raw);
|
|
|
|
private static char Next(string text, int i) => i + 1 < text.Length ? text[i + 1] : '\0';
|
|
private static char Prev(string text, int i) => i > 0 ? text[i - 1] : '\0';
|
|
|
|
/// <summary>대입인지. <c>==</c> · <c>!=</c> · <c><=</c> · <c>+=</c> · <c>operator=</c> 는 아니다.</summary>
|
|
private bool IsAssignment(string text, int i)
|
|
{
|
|
if (Next(text, i) == '=') return false;
|
|
var p = Prev(text, i);
|
|
if ("=!<>+-*/%&|^".IndexOf(p) >= 0) return false;
|
|
var before = _s.Decl.ToString().TrimEnd();
|
|
return !before.EndsWith("operator", StringComparison.Ordinal);
|
|
}
|
|
|
|
/// <summary>선언이 아닌 것: using · namespace X; · typedef 는 반영 단위로 삼지 않는다.</summary>
|
|
private static bool IsNotMember(string header)
|
|
{
|
|
var h = header.Trim();
|
|
if (h.Length == 0) return true;
|
|
return Regex.IsMatch(h, @"^(using|namespace|global\s+using)\b");
|
|
}
|
|
|
|
private static bool IsExternBlock(string header) =>
|
|
Regex.IsMatch(header.Trim(), @"^extern\s+_+$");
|
|
|
|
/// <summary>첫 <c>(</c> 앞 낱말들. C++ template<...> 머리는 뗀다(<c>template<class T> T Max(...)</c>).</summary>
|
|
private static string WordsBeforeParen(string header)
|
|
{
|
|
var h = header;
|
|
if (TemplatePrefix.IsMatch(h))
|
|
{
|
|
var open = h.IndexOf('<');
|
|
var depth = 0;
|
|
for (var k = open; k < h.Length; k++)
|
|
{
|
|
if (h[k] == '<') depth++;
|
|
else if (h[k] == '>' && --depth == 0) { h = h.Substring(k + 1); break; }
|
|
}
|
|
}
|
|
|
|
var paren = h.IndexOf('(');
|
|
return paren < 0 ? h : h.Substring(0, paren);
|
|
}
|
|
}
|
|
|
|
// ================= 주석 · 문자열 가리기 =================
|
|
|
|
private sealed class Masked
|
|
{
|
|
public string[] Code = Array.Empty<string>();
|
|
public bool[] Preprocessor = Array.Empty<bool>();
|
|
|
|
/// <summary>#if · #ifdef · #ifndef 줄 → 짝 #endif 줄.</summary>
|
|
public Dictionary<int, int> IfEnd = new Dictionary<int, int>();
|
|
}
|
|
|
|
private enum Mode { Code, LineComment, BlockComment, Str, Verbatim, Interp, InterpVerbatim, Char, RawStr }
|
|
|
|
/// <summary>
|
|
/// 줄마다 코드만 남긴다(주석은 공백, 문자열 속은 '_'). 길이는 원문과 같다.
|
|
/// <see cref="CSharpStyle"/> 의 가리기와 같은 뜻이지만 raw 문자열 · C++ 까지 알아야 해서 따로 둔다 -
|
|
/// 스타일 맞춤(F-12 기본값)을 이 일로 흔들지 않는다.
|
|
/// </summary>
|
|
private static Masked Mask(IReadOnlyList<string> lines, MemberLanguage language)
|
|
{
|
|
var cpp = language == MemberLanguage.Cpp;
|
|
var src = string.Join("\n", lines);
|
|
var mask = src.ToCharArray();
|
|
var n = src.Length;
|
|
var modes = new Stack<Mode>();
|
|
modes.Push(Mode.Code);
|
|
var holes = new Stack<int>();
|
|
string rawEnd = string.Empty;
|
|
|
|
var pre = new bool[lines.Count];
|
|
var line = 0;
|
|
var lineStart = true;
|
|
var continued = false; // C++ 지시문이 \ 로 다음 줄에 이어짐
|
|
|
|
for (var i = 0; i < n;)
|
|
{
|
|
var ch = src[i];
|
|
var next = i + 1 < n ? src[i + 1] : '\0';
|
|
|
|
if (ch == '\n')
|
|
{
|
|
var top = modes.Peek();
|
|
if (top is Mode.LineComment or Mode.Str or Mode.Char or Mode.Interp) { modes.Pop(); }
|
|
if (cpp && pre[line] && i > 0 && src[i - 1] == '\\') continued = true;
|
|
line++;
|
|
lineStart = true;
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
if (lineStart)
|
|
{
|
|
lineStart = false;
|
|
if (continued) { pre[line] = true; continued = false; }
|
|
else if (modes.Peek() == Mode.Code && holes.Count == 0)
|
|
{
|
|
var k = i;
|
|
while (k < n && src[k] != '\n' && char.IsWhiteSpace(src[k])) k++;
|
|
if (k < n && src[k] == '#') pre[line] = true;
|
|
}
|
|
}
|
|
|
|
switch (modes.Peek())
|
|
{
|
|
case Mode.Code:
|
|
if (ch == '/' && next == '/') { modes.Push(Mode.LineComment); Blank(mask, i, 2); i += 2; continue; }
|
|
if (ch == '/' && next == '*') { modes.Push(Mode.BlockComment); Blank(mask, i, 2); i += 2; continue; }
|
|
|
|
if (!cpp && TryRawCSharp(src, i, out var quotes, out var prefix))
|
|
{
|
|
rawEnd = new string('"', quotes);
|
|
modes.Push(Mode.RawStr);
|
|
Hide(mask, i, prefix + quotes);
|
|
i += prefix + quotes;
|
|
continue;
|
|
}
|
|
|
|
if (cpp && TryRawCpp(src, i, out var open, out var delimiter))
|
|
{
|
|
rawEnd = ")" + delimiter + "\"";
|
|
modes.Push(Mode.RawStr);
|
|
Hide(mask, i, open);
|
|
i += open;
|
|
continue;
|
|
}
|
|
|
|
if (!cpp)
|
|
{
|
|
if ((ch == '$' && next == '@' && At(src, i + 2) == '"') || (ch == '@' && next == '$' && At(src, i + 2) == '"'))
|
|
{ modes.Push(Mode.InterpVerbatim); Hide(mask, i, 3); i += 3; continue; }
|
|
if (ch == '$' && next == '"') { modes.Push(Mode.Interp); Hide(mask, i, 2); i += 2; continue; }
|
|
if (ch == '@' && next == '"') { modes.Push(Mode.Verbatim); Hide(mask, i, 2); i += 2; continue; }
|
|
}
|
|
|
|
if (ch == '"') { modes.Push(Mode.Str); Hide(mask, i, 1); i++; continue; }
|
|
|
|
if (ch == '\'')
|
|
{
|
|
// C++14 숫자 구분자 1'000'000 - 숫자 속의 ' 는 문자 따옴표가 아니다.
|
|
if (cpp && InNumber(src, i)) { i++; continue; }
|
|
modes.Push(Mode.Char);
|
|
Hide(mask, i, 1);
|
|
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 == '/') { Blank(mask, i, 2); 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 (next != '\n' && i + 1 < n) mask[i + 1] = '_'; i += next == '\n' ? 1 : 2; continue; }
|
|
if (ch == quote) modes.Pop();
|
|
mask[i] = '_';
|
|
i++;
|
|
continue;
|
|
|
|
case Mode.Verbatim:
|
|
if (ch == '"' && next == '"') { Hide(mask, i, 2); i += 2; continue; }
|
|
if (ch == '"') modes.Pop();
|
|
mask[i] = '_';
|
|
i++;
|
|
continue;
|
|
|
|
case Mode.Interp:
|
|
case Mode.InterpVerbatim:
|
|
var verbatim = modes.Peek() == Mode.InterpVerbatim;
|
|
if ((ch == '{' && next == '{') || (ch == '}' && next == '}') || (verbatim && ch == '"' && next == '"'))
|
|
{ Hide(mask, i, 2); 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;
|
|
|
|
case Mode.RawStr:
|
|
if (string.CompareOrdinal(src, i, rawEnd, 0, rawEnd.Length) == 0)
|
|
{
|
|
Hide(mask, i, rawEnd.Length);
|
|
i += rawEnd.Length;
|
|
modes.Pop();
|
|
continue;
|
|
}
|
|
mask[i] = '_';
|
|
i++;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
var code = new string(mask).Split('\n');
|
|
var result = new Masked { Code = code, Preprocessor = pre };
|
|
|
|
// #if 짝 맞추기(첫 갈래 끝이 아니라 #endif 까지)
|
|
var open2 = new Stack<int>();
|
|
for (var l = 0; l < code.Length; l++)
|
|
{
|
|
if (!pre[l]) continue;
|
|
var word = Regex.Match(code[l].TrimStart().TrimStart('#').TrimStart(), @"^[a-z]+").Value;
|
|
if (word is "if" or "ifdef" or "ifndef") open2.Push(l);
|
|
else if (word == "endif" && open2.Count > 0) result.IfEnd[open2.Pop()] = l;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static char At(string s, int i) => i < s.Length ? s[i] : '\0';
|
|
|
|
private static void Blank(char[] mask, int at, int count)
|
|
{
|
|
for (var k = 0; k < count && at + k < mask.Length; k++) mask[at + k] = ' ';
|
|
}
|
|
|
|
private static void Hide(char[] mask, int at, int count)
|
|
{
|
|
for (var k = 0; k < count && at + k < mask.Length; k++) mask[at + k] = '_';
|
|
}
|
|
|
|
/// <summary>C# 11 raw 문자열: <c>"""</c>(세 개 이상), 앞에 <c>$</c> 가 붙을 수 있다.</summary>
|
|
private static bool TryRawCSharp(string s, int i, out int quotes, out int prefix)
|
|
{
|
|
quotes = 0;
|
|
prefix = 0;
|
|
var k = i;
|
|
while (k < s.Length && s[k] == '$') k++;
|
|
prefix = k - i;
|
|
while (k < s.Length && s[k] == '"') { quotes++; k++; }
|
|
if (quotes >= 3) return true;
|
|
quotes = 0;
|
|
prefix = 0;
|
|
return false;
|
|
}
|
|
|
|
/// <summary>C++11 raw 문자열: <c>R"구분자(</c> … <c>)구분자"</c>, 앞에 u8 · u · U · L 이 붙을 수 있다.</summary>
|
|
private static bool TryRawCpp(string s, int i, out int open, out string delimiter)
|
|
{
|
|
open = 0;
|
|
delimiter = string.Empty;
|
|
if (s[i] != 'R' || At(s, i + 1) != '"') return false;
|
|
|
|
var b = i - 1;
|
|
if (b >= 0 && s[b] == '8' && b >= 1 && s[b - 1] == 'u') b -= 2;
|
|
else if (b >= 0 && (s[b] == 'u' || s[b] == 'U' || s[b] == 'L')) b--;
|
|
if (b >= 0 && (char.IsLetterOrDigit(s[b]) || s[b] == '_')) return false;
|
|
|
|
var k = i + 2;
|
|
while (k < s.Length && s[k] != '(' && s[k] != '\n' && k - i < 20) k++;
|
|
if (k >= s.Length || s[k] != '(') return false;
|
|
|
|
delimiter = s.Substring(i + 2, k - (i + 2));
|
|
open = k - i + 1;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>이 ' 가 숫자 글자 사이의 구분자인지(앞 낱말이 숫자로 시작하고 뒤가 숫자 · 16진 글자).</summary>
|
|
private static bool InNumber(string s, int i)
|
|
{
|
|
if (i == 0 || i + 1 >= s.Length || !char.IsLetterOrDigit(s[i + 1])) return false;
|
|
var k = i - 1;
|
|
while (k >= 0 && (char.IsLetterOrDigit(s[k]) || s[k] == '\'' || s[k] == '.')) k--;
|
|
return k + 1 < i && char.IsDigit(s[k + 1]);
|
|
}
|
|
}
|
|
}
|