2025年5月8日

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Forms;
using Excel = Microsoft.Office.Interop.Excel;   // 参照: Microsoft.Office.Interop.Excel

namespace LegacyFormDoc
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            // this.button1.Click += button1_Click; // デザイナで結線済みなら不要
        }

        private void button1_Click(object sender, EventArgs e)
        {
            try
            {
                LegacyScanner.Run();
                MessageBox.Show("解析完了:出力フォルダを開きます。", "完了", MessageBoxButtons.OK, MessageBoxIcon.Information);
                System.Diagnostics.Process.Start(LegacyScanner.OutputDir);
            }
            catch (Exception ex)
            {
                MessageBox.Show("実行時エラー: " + ex.Message, "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }
    }

    internal static class LegacyScanner
    {
        // ★ 固定対象パス(必要に応じて変更)
        private const string TARGET_ROOT = @"C:\Repo\LegacyApp";
        internal static readonly string OutputDir = Path.Combine(TARGET_ROOT, "_scanout");

        private static readonly string[] BaseProps = new[]
        {
            "Text","TabIndex","Dock","Anchor","Enabled","Visible","ReadOnly","MaxLength","Location","Size","Tag"
        };

        // ===== 正規表現 =====
        private static readonly Regex ReClassHead = new Regex(@"\bclass\s+(\w+)\b",
            RegexOptions.CultureInvariant | RegexOptions.Compiled);
        private static readonly Regex ReInit = new Regex(@"void\s+InitializeComponent\s*\(",
            RegexOptions.CultureInvariant | RegexOptions.Compiled);
        private static readonly Regex ReNew = new Regex(@"(?:this\.)?(\w+)\s*=\s*new\s+([\w\.\<\>]+)\s*\(",
            RegexOptions.Multiline | RegexOptions.CultureInvariant | RegexOptions.Compiled);

        private static readonly Regex ReTextStr = new Regex(@"(?:this\.)?(\w+)\.Text\s*=\s*""([^""]*)""\s*;",
            RegexOptions.Multiline | RegexOptions.CultureInvariant | RegexOptions.Compiled);
        // 右辺にダブルクォートがある行は除外(文字列リテラルと重複させない)
        private static readonly Regex ReTextExp = new Regex(
            @"(?:this\.)?(\w+)\.Text\s*=\s*(?![^;]*"")([^;]+?)\s*;",
            RegexOptions.Multiline | RegexOptions.CultureInvariant | RegexOptions.Compiled);

        private static readonly Regex ReTab = new Regex(@"(?:this\.)?(\w+)\.TabIndex\s*=\s*(\d+)\s*;",
            RegexOptions.Multiline | RegexOptions.CultureInvariant | RegexOptions.Compiled);
        private static readonly Regex ReDock = new Regex(@"(?:this\.)?(\w+)\.Dock\s*=\s*([\w\.]+)\s*;",
            RegexOptions.Multiline | RegexOptions.CultureInvariant | RegexOptions.Compiled);
        private static readonly Regex ReAnchor = new Regex(@"(?:this\.)?(\w+)\.Anchor\s*=\s*([\w\|\s\.]+)\s*;",
            RegexOptions.Multiline | RegexOptions.CultureInvariant | RegexOptions.Compiled);
        private static readonly Regex ReBool = new Regex(@"(?:this\.)?(\w+)\.(Enabled|Visible|ReadOnly)\s*=\s*(true|false)\s*;",
            RegexOptions.Multiline | RegexOptions.CultureInvariant | RegexOptions.Compiled);
        private static readonly Regex ReMaxLen = new Regex(@"(?:this\.)?(\w+)\.MaxLength\s*=\s*(\d+)\s*;",
            RegexOptions.Multiline | RegexOptions.CultureInvariant | RegexOptions.Compiled);
        private static readonly Regex ReLoc = new Regex(@"(?:this\.)?(\w+)\.Location\s*=\s*new\s+System\.Drawing\.Point\s*\(\s*(-?\d+)\s*,\s*(-?\d+)\s*\)\s*;",
            RegexOptions.Multiline | RegexOptions.CultureInvariant | RegexOptions.Compiled);
        private static readonly Regex ReSize = new Regex(@"(?:this\.)?(\w+)\.Size\s*=\s*new\s+System\.Drawing\.Size\s*\(\s*(\d+)\s*,\s*(\d+)\s*\)\s*;",
            RegexOptions.Multiline | RegexOptions.CultureInvariant | RegexOptions.Compiled);
        private static readonly Regex ReTagStr = new Regex(@"(?:this\.)?(\w+)\.Tag\s*=\s*""([^""]*)""\s*;",
            RegexOptions.Multiline | RegexOptions.CultureInvariant | RegexOptions.Compiled);
        private static readonly Regex ReTagExp = new Regex(@"(?:this\.)?(\w+)\.Tag\s*=\s*(?![^;]*"")([^;]+?)\s*;",
            RegexOptions.Multiline | RegexOptions.CultureInvariant | RegexOptions.Compiled);

        // ===== モデル =====
        private sealed class ControlRow { public string File; public string Class; public string ControlId; public string Type; public int Line; }
        private sealed class PropRow    { public string File; public string Class; public string ControlId; public string Prop; public string Value; public int Line; }
        private sealed class ClassSpan  { public string Name; public int Open1; public int Close1; }

        public static void Run()
        {
            if (!Directory.Exists(TARGET_ROOT))
                throw new DirectoryNotFoundException("対象パスが存在しません: " + TARGET_ROOT);

            Directory.CreateDirectory(OutputDir);

            var controls = new List<ControlRow>();
            var props    = new List<PropRow>();
            var propUniq = new HashSet<string>(StringComparer.Ordinal); // 重複排除

            foreach (var path in EnumerateFiles(TARGET_ROOT, "*.cs"))
            {
                string full = SafeReadAllText(path);
                if (string.IsNullOrEmpty(full)) continue;

                var classSpans = BuildClassSpans(full);

                foreach (Match m in ReInit.Matches(full))
                {
                    int headEnd = m.Index + m.Length;
                    int blockStart = FindNextOpenBraceStrict(full, headEnd);
                    if (blockStart <= 0) continue;
                    int blockEnd = FindMatchingBraceStrict(full, blockStart);
                    if (blockEnd <= 0) continue;

                    string block = full.Substring(blockStart - 1, (blockEnd - blockStart + 1));
                    int baseLine = LineOfPos(full, blockStart);

                    string cls = ResolveEnclosingClass(classSpans, blockStart, blockEnd);
                    if (string.IsNullOrEmpty(cls))
                    {
                        var mhead = ReClassHead.Match(full);
                        cls = mhead.Success ? mhead.Groups[1].Value : "";
                    }

                    foreach (Match mm in ReNew.Matches(block))
                    {
                        controls.Add(new ControlRow
                        {
                            File = path,
                            Class = cls,
                            ControlId = mm.Groups[1].Value,
                            Type = mm.Groups[2].Value,
                            Line = baseLine + EstimateLineFrom0(block, mm.Index) - 1
                        });
                    }

                    AddProp(props, propUniq, ReTextStr, block, path, cls, "Text", baseLine);
                    AddProp(props, propUniq, ReTextExp, block, path, cls, "Text", baseLine);
                    AddProp(props, propUniq, ReTab,     block, path, cls, "TabIndex", baseLine);
                    AddProp(props, propUniq, ReDock,    block, path, cls, "Dock", baseLine);
                    AddProp(props, propUniq, ReAnchor,  block, path, cls, "Anchor", baseLine);
                    AddProp2(props,propUniq, ReBool,    block, path, cls, baseLine);
                    AddProp(props, propUniq, ReMaxLen,  block, path, cls, "MaxLength", baseLine);
                    AddPropPoint(props,propUniq, ReLoc, block, path, cls, "Location", baseLine);
                    AddPropSize(props, propUniq, ReSize,block, path, cls, "Size", baseLine);
                    AddProp(props, propUniq, ReTagStr,  block, path, cls, "Tag", baseLine);
                    AddProp(props, propUniq, ReTagExp,  block, path, cls, "Tag", baseLine);
                }
            }

            // 参考CSV(要件はExcel整形だが従来出力も維持)
            WriteControlsCsv(controls);
            WritePropsCsv(props);
            WriteMatrixCsv_ForReference(controls, props);

            // Excel(A3以降:枠線・フィルター・全セル非折返し・AutoFit)
            WriteMatrixExcelFormatted(controls, props);
        }

        // ==== クラススパン解析 ====
        private static List<ClassSpan> BuildClassSpans(string full)
        {
            var spans = new List<ClassSpan>();
            var matches = ReClassHead.Matches(full);
            foreach (Match m in matches)
            {
                string name = m.Groups[1].Value;
                int braceOpen = FindNextOpenBraceStrict(full, m.Index + m.Length);
                if (braceOpen <= 0) continue;
                int braceClose = FindMatchingBraceStrict(full, braceOpen);
                if (braceClose <= 0) continue;
                spans.Add(new ClassSpan { Name = name, Open1 = braceOpen, Close1 = braceClose });
            }
            spans.Sort((a, b) => a.Open1.CompareTo(b.Open1));
            return spans;
        }

        private static string ResolveEnclosingClass(List<ClassSpan> spans, int methodOpen1, int methodClose1)
        {
            for (int i = spans.Count - 1; i >= 0; i--)
            {
                var s = spans[i];
                if (s.Open1 <= methodOpen1 && methodClose1 <= s.Close1)
                    return s.Name;
            }
            return "";
        }

        // ==== CSV(参考。全セル ="..." で文字列固定) ====
        private static void WriteControlsCsv(List<ControlRow> controls)
        {
            string path = Path.Combine(OutputDir, "Controls.csv");
            using (var sw = new StreamWriter(path, false, Encoding.GetEncoding(932)))
            {
                sw.WriteLine(string.Join(",", CsvText("File"), CsvText("Class"), CsvText("ControlId"), CsvText("Type"), CsvText("Line")));
                foreach (var c in controls)
                {
                    sw.WriteLine(string.Join(",",
                        CsvText(c.File), CsvText(c.Class), CsvText(c.ControlId), CsvText(c.Type), CsvText(c.Line.ToString())));
                }
            }
        }

        private static void WritePropsCsv(List<PropRow> props)
        {
            string path = Path.Combine(OutputDir, "Props.csv");
            using (var sw = new StreamWriter(path, false, Encoding.GetEncoding(932)))
            {
                sw.WriteLine(string.Join(",", CsvText("File"), CsvText("Class"), CsvText("ControlId"), CsvText("Prop"), CsvText("Value"), CsvText("Line")));
                foreach (var p in props)
                {
                    sw.WriteLine(string.Join(",",
                        CsvText(p.File), CsvText(p.Class), CsvText(p.ControlId), CsvText(p.Prop), CsvText(p.Value), CsvText(p.Line.ToString())));
                }
            }
        }

        private static void WriteMatrixCsv_ForReference(List<ControlRow> controls, List<PropRow> props)
        {
            string path = Path.Combine(OutputDir, "Matrix.csv");

            var propSet = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
            foreach (var b in BaseProps) propSet.Add(b);
            foreach (var p in props.Select(x => x.Prop)) if (!string.IsNullOrEmpty(p)) propSet.Add(p);
            var propList = propSet.ToList();

            var groups = controls
                .GroupBy(c => c.File + "|" + c.Class)
                .ToDictionary(g => g.Key, g => g.ToList(), StringComparer.OrdinalIgnoreCase);

            using (var sw = new StreamWriter(path, false, Encoding.GetEncoding(932)))
            {
                bool firstSection = true;
                foreach (var kv in groups)
                {
                    if (!firstSection) sw.WriteLine();
                    firstSection = false;

                    var parts = kv.Key.Split('|');
                    string file = parts.Length > 0 ? parts[0] : "";
                    string cls  = parts.Length > 1 ? parts[1] : "";

                    sw.WriteLine(string.Join(",", CsvText("FILE"),  CsvText(file)));
                    sw.WriteLine(string.Join(",", CsvText("Class"), CsvText(cls)));

                    sw.Write(string.Join(",", CsvText("ControlId"), CsvText("Type")));
                    foreach (var col in propList) sw.Write("," + CsvText(col));
                    sw.WriteLine();

                    var rowKeys = new SortedDictionary<string, ControlRow>(StringComparer.OrdinalIgnoreCase);
                    foreach (var c in kv.Value)
                        if (!rowKeys.ContainsKey(c.ControlId)) rowKeys.Add(c.ControlId, c);

                    var propMap = new Dictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
                    foreach (var c in kv.Value)
                    {
                        var pick = props.Where(p => p.File == file && p.Class == cls && p.ControlId == c.ControlId);
                        foreach (var pr in pick)
                        {
                            if (!propMap.ContainsKey(c.ControlId)) propMap[c.ControlId] = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
                            string cur;
                            if (propMap[c.ControlId].TryGetValue(pr.Prop, out cur))
                            {
                                if (!string.Equals(cur, pr.Value, StringComparison.Ordinal))
                                    propMap[c.ControlId][pr.Prop] = cur + ", " + pr.Value;
                            }
                            else
                            {
                                propMap[c.ControlId][pr.Prop] = pr.Value ?? "";
                            }
                        }
                    }

                    foreach (var row in rowKeys)
                    {
                        var c = row.Value;
                        sw.Write(string.Join(",", CsvText(c.ControlId), CsvText(c.Type)));
                        foreach (var col in propList)
                        {
                            string v = (propMap.ContainsKey(c.ControlId) && propMap[c.ControlId].ContainsKey(col))
                                ? propMap[c.ControlId][col] : "";
                            sw.Write("," + CsvText(v));
                        }
                        sw.WriteLine();
                    }
                }
            }
        }

        //==== Excel(A3以降:枠線・フィルター・全セル非折返し・AutoFit・改行除去) ====
        private static void WriteMatrixExcelFormatted(List<ControlRow> controls, List<PropRow> props)
        {
            var propSet = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
            foreach (var b in BaseProps) propSet.Add(b);
            foreach (var p in props.Select(x => x.Prop)) if (!string.IsNullOrEmpty(p)) propSet.Add(p);
            var propList = propSet.ToList();

            var groups = controls
                .GroupBy(c => c.File + "|" + c.Class)
                .Select(g => new { Key = g.Key, Items = g.ToList() })
                .ToList();

            string xlsx = Path.Combine(OutputDir, "Matrix.xlsx");

            Excel.Application app = null;
            Excel.Workbook wb = null;

            try
            {
                app = new Excel.Application();
                app.DisplayAlerts = false;
                wb = app.Workbooks.Add();

                var usedNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

                if (groups.Count == 0)
                {
                    Excel.Worksheet ws0 = (Excel.Worksheet)wb.Worksheets[1];
                    ws0.Cells.NumberFormat = "@";
                    ws0.Cells.Clear();
                    ws0.Name = UniqueSheetName(usedNames, "Matrix");
                    ws0.Cells[1, 1] = "FILE";
                    ws0.Cells[1, 2] = "(no data)";
                    ws0.Cells[2, 1] = "Class";
                    ws0.Cells[2, 2] = "(no data)";
                    ws0.Cells[3, 1] = "ControlId";
                    ws0.Cells[3, 2] = "Type";
                    Excel.Range header0 = ws0.Range[ws0.Cells[3, 1], ws0.Cells[3, 2]];
                    header0.Font.Bold = true;
                    ws0.Cells.WrapText = false;              // 非折返し
                    ((Excel.Range)ws0.Columns).ColumnWidth = 8; // 初期
                    ((Excel.Range)ws0.Columns[1]).AutoFit();
                    ((Excel.Range)ws0.Columns[2]).AutoFit();
                    wb.SaveAs(xlsx);
                    return;
                }

                for (int gi = 0; gi < groups.Count; gi++)
                {
                    var grp = groups[gi];
                    var parts = grp.Key.Split('|');
                    string file = parts.Length > 0 ? parts[0] : "";
                    string cls  = parts.Length > 1 ? parts[1] : "";

                    string baseName = string.IsNullOrEmpty(cls) ? Path.GetFileNameWithoutExtension(file) : cls;
                    string sheetName = UniqueSheetName(usedNames, baseName);

                    Excel.Worksheet ws;
                    if (gi == 0)
                    {
                        ws = (Excel.Worksheet)wb.Worksheets[1];
                        ws.Cells.Clear();
                    }
                    else
                    {
                        ws = (Excel.Worksheet)wb.Worksheets.Add(After: wb.Worksheets[wb.Worksheets.Count]);
                    }
                    ws.Name = sheetName;

                    // 全セル:文字列・非折返し
                    ws.Cells.NumberFormat = "@";
                    ws.Cells.WrapText = false;

                    // A1:FILE, A2:Class(改行を除去して書き込み)
                    ws.Cells[1, 1] = "FILE";
                    ws.Cells[1, 2] = Sanitize(file);
                    ws.Cells[2, 1] = "Class";
                    ws.Cells[2, 2] = Sanitize(cls);

                    // A3: ヘッダ
                    int headerRow = 3;
                    int col = 1;
                    ws.Cells[headerRow, col++] = "ControlId";
                    ws.Cells[headerRow, col++] = "Type";
                    foreach (var pcol in propList)
                        ws.Cells[headerRow, col++] = pcol;

                    // 行キー:ControlId
                    var rowKeys = new SortedDictionary<string, ControlRow>(StringComparer.OrdinalIgnoreCase);
                    foreach (var c in grp.Items)
                        if (!rowKeys.ContainsKey(c.ControlId)) rowKeys.Add(c.ControlId, c);

                    // props 集計
                    var propMap = new Dictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
                    foreach (var c in grp.Items)
                    {
                        var pick = props.Where(p => p.File == file && p.Class == cls && p.ControlId == c.ControlId);
                        foreach (var pr in pick)
                        {
                            if (!propMap.ContainsKey(c.ControlId)) propMap[c.ControlId] = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
                            string cur;
                            if (propMap[c.ControlId].TryGetValue(pr.Prop, out cur))
                            {
                                if (!string.Equals(cur, pr.Value, StringComparison.Ordinal))
                                    propMap[c.ControlId][pr.Prop] = cur + ", " + pr.Value;
                            }
                            else
                            {
                                propMap[c.ControlId][pr.Prop] = pr.Value ?? "";
                            }
                        }
                    }

                    // データ書き込み(A4~)※改行・タブを除去
                    int row = headerRow + 1;
                    foreach (var k in rowKeys.Keys)
                    {
                        var c = rowKeys[k];
                        col = 1;
                        ws.Cells[row, col++] = Sanitize(c.ControlId);
                        ws.Cells[row, col++] = Sanitize(c.Type);
                        foreach (var pcol in propList)
                        {
                            string v = (propMap.ContainsKey(c.ControlId) && propMap[c.ControlId].ContainsKey(pcol))
                                ? propMap[c.ControlId][pcol] : "";
                            ws.Cells[row, col++] = Sanitize(v);
                        }
                        row++;
                    }

                    int lastRow = Math.Max(headerRow, row - 1);
                    int lastCol = 2 + propList.Count;

                    // 枠線(A3~最終セル)
                    Excel.Range rng = ws.Range[ws.Cells[headerRow, 1], ws.Cells[lastRow, lastCol]];
                    rng.Borders.LineStyle = Excel.XlLineStyle.xlContinuous;
                    rng.Borders.Weight = Excel.XlBorderWeight.xlThin;
                    rng.WrapText = false; // 念押し

                    // フィルター(A3 見出し行)
                    Excel.Range header = ws.Range[ws.Cells[headerRow, 1], ws.Cells[headerRow, lastCol]];
                    header.AutoFilter(1, Type.Missing, Excel.XlAutoFilterOperator.xlAnd, Type.Missing, true);
                    header.Font.Bold = true;

                    // 初期幅を小さく→AutoFit(全列)
                    ((Excel.Range)ws.Columns).ColumnWidth = 8;
                    Excel.Range used = ws.UsedRange;
                    used.Columns.AutoFit();
                    used.Rows.AutoFit(); // 非折返しだが行高も最適化

                    // 先頭列/2列目は最小幅を確保(見やすさ優先)
                    var col1 = (Excel.Range)ws.Columns[1];
                    var col2 = (Excel.Range)ws.Columns[2];
                    col1.ColumnWidth = Math.Max(12, col1.ColumnWidth);
                    col2.ColumnWidth = Math.Max(18, col2.ColumnWidth);

                    // A1/A2 見出し太字
                    ((Excel.Range)ws.Cells[1, 1]).Font.Bold = true;
                    ((Excel.Range)ws.Cells[2, 1]).Font.Bold = true;

                    // A3 を選択
                    ((Excel.Range)ws.Cells[3, 1]).Select();
                }

                wb.SaveAs(xlsx);
            }
            finally
            {
                if (wb != null) { wb.Close(false); System.Runtime.InteropServices.Marshal.ReleaseComObject(wb); }
                if (app != null) { app.Quit(); System.Runtime.InteropServices.Marshal.ReleaseComObject(app); }
            }
        }

        // 改行・タブの除去+空白正規化(セルは1行で表示)
        private static string Sanitize(string s)
        {
            if (string.IsNullOrEmpty(s)) return "";
            s = s.Replace("\r", " ").Replace("\n", " ").Replace("\t", " ");
            // 連続空白を1個に
            while (s.Contains("  ")) s = s.Replace("  ", " ");
            return s.Trim();
        }

        // シート名の安全化+重複時の連番付与
        private static string UniqueSheetName(HashSet<string> used, string baseName)
        {
            string name = MakeSafeSheetName(baseName);
            if (!used.Add(name))
            {
                int n = 2;
                while (!used.Add(name + "_" + n)) n++;
                name = name + "_" + n;
            }
            return name;
        }

        private static string MakeSafeSheetName(string name)
        {
            if (string.IsNullOrEmpty(name)) name = "Sheet";
            foreach (var ch in new[] { '\\', '/', '?', '*', '[', ']', ':', '\'' })
                name = name.Replace(ch.ToString(), "_");
            if (name.Length > 31) name = name.Substring(0, 31);
            if (name.EndsWith(".")) name = name.TrimEnd('.');
            if (string.IsNullOrEmpty(name)) name = "Sheet";
            return name;
        }

        //==== 文字列として CSV セルに書く(参考出力用)
        private static string CsvText(string s)
        {
            if (s == null) s = "";
            string inner = "=\"" + s.Replace("\"", "\"\"") + "\""; // ="..."
            string csv = "\"" + inner.Replace("\"", "\"\"") + "\"";
            return csv;
        }

        //==== 解析基盤 ====
        private static IEnumerable<string> EnumerateFiles(string root, string pattern)
        {
            var stack = new Stack<string>();
            stack.Push(root);
            while (stack.Count > 0)
            {
                var dir = stack.Pop();
                string[] subdirs;
                try { subdirs = Directory.GetDirectories(dir); }
                catch { continue; }
                foreach (var d in subdirs) stack.Push(d);

                string[] files;
                try { files = Directory.GetFiles(dir, pattern); }
                catch { continue; }
                foreach (var f in files) yield return f;
            }
        }

        private static string SafeReadAllText(string path)
        {
            try { return File.ReadAllText(path, DetectEncoding(path)); }
            catch { return ""; }
        }

        private static Encoding DetectEncoding(string path)
        {
            using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read))
            {
                var bom = new byte[Math.Min(4, fs.Length)];
                fs.Read(bom, 0, bom.Length);
                if (bom.Length >= 3 && bom[0] == 0xEF && bom[1] == 0xBB && bom[2] == 0xBF) return new UTF8Encoding(true);
                if (bom.Length >= 2 && bom[0] == 0xFF && bom[1] == 0xFE) return Encoding.Unicode; // UTF-16LE
            }
            return Encoding.Default; // CP932 想定
        }

        private static int LineOfPos(string txt, int pos1Based)
        {
            if (pos1Based < 1) return 1;
            int cnt = 1;
            for (int i = 0; i < pos1Based && i < txt.Length; i++)
                if (txt[i] == '\n') cnt++;
            return cnt;
        }

        private static int EstimateLineFrom0(string block, int zeroBasedPos)
        {
            int pos = zeroBasedPos;
            if (pos < 0) return 1;
            int cnt = 1;
            for (int i = 0; i <= pos && i < block.Length; i++)
                if (block[i] == '\n') cnt++;
            return cnt;
        }

        //==== ブレース探索(コメント/文字列/逐語的文字列/文字リテラル対応) ====
        private static int FindNextOpenBraceStrict(string s, int startPos1Based)
        {
            bool inSL = false, inML = false, inStr = false, inChar = false, verb = false;
            for (int i = startPos1Based - 1; i < s.Length; i++)
            {
                char c = s[i];
                char nxt = (i + 1 < s.Length) ? s[i + 1] : '\0';

                if (inSL) { if (c == '\r' || c == '\n') inSL = false; continue; }
                if (inML) { if (c == '*' && nxt == '/') { inML = false; i++; } continue; }
                if (inStr)
                {
                    if (verb) { if (c == '"' && nxt == '"') { i++; continue; } if (c == '"') { inStr = false; verb = false; } continue; }
                    else      { if (c == '\\') { i++; continue; } if (c == '"') { inStr = false; } continue; }
                }
                if (inChar) { if (c == '\\') { i++; continue; } if (c == '\'') { inChar = false; } continue; }

                if (c == '/' && nxt == '/') { inSL = true; i++; continue; }
                if (c == '/' && nxt == '*') { inML = true; i++; continue; }
                if (c == '"')
                {
                    char prev = (i > 0) ? s[i - 1] : '\0';
                    if (prev == '@') { inStr = true; verb = true; } else { inStr = true; verb = false; }
                    continue;
                }
                if (c == '\'') { inChar = true; continue; }
                if (c == '{') return i + 1; // 1-based
            }
            return 0;
        }

        private static int FindMatchingBraceStrict(string s, int posOpen1Based)
        {
            bool inSL = false, inML = false, inStr = false, inChar = false, verb = false;
            int depth = 0;
            for (int i = posOpen1Based - 1; i < s.Length; i++)
            {
                char c = s[i];
                char nxt = (i + 1 < s.Length) ? s[i + 1] : '\0';

                if (inSL) { if (c == '\r' || c == '\n') inSL = false; continue; }
                if (inML) { if (c == '*' && nxt == '/') { inML = false; i++; } continue; }
                if (inStr)
                {
                    if (verb) { if (c == '"' && nxt == '"') { i++; continue; } if (c == '"') { inStr = false; verb = false; } continue; }
                    else      { if (c == '\\') { i++; continue; } if (c == '"') { inStr = false; } continue; }
                }
                if (inChar) { if (c == '\\') { i++; continue; } if (c == '\'') { inChar = false; } continue; }

                if (c == '/' && nxt == '/') { inSL = true; i++; continue; }
                if (c == '/' && nxt == '*') { inML = true; i++; continue; }
                if (c == '"')
                {
                    char prev = (i > 0) ? s[i - 1] : '\0';
                    if (prev == '@') { inStr = true; verb = true; } else { inStr = true; verb = false; }
                    continue;
                }
                if (c == '\'') { inChar = true; continue; }

                if (c == '{') depth++;
                else if (c == '}')
                {
                    depth--;
                    if (depth == 0) return i + 1; // 1-based
                }
            }
            return 0;
        }

        //==== プロパティ抽出(重複排除つき) ====
        private static void AddProp(List<PropRow> list, HashSet<string> uniq, Regex re, string block, string file, string cls, string propName, int baseLine)
        {
            foreach (Match m in re.Matches(block))
            {
                if (!m.Success || m.Groups.Count < 3) continue;
                string id = m.Groups[1].Value ?? "";
                string val = (m.Groups[2].Value ?? "").Trim();
                int line = baseLine + EstimateLineFrom0(block, m.Index) - 1;
                string key = file + "|" + cls + "|" + id + "|" + propName + "|" + val + "|" + line.ToString();
                if (!uniq.Add(key)) continue;
                list.Add(new PropRow { File = file, Class = cls, ControlId = id, Prop = propName, Value = val, Line = line });
            }
        }

        private static void AddProp2(List<PropRow> list, HashSet<string> uniq, Regex re, string block, string file, string cls, int baseLine)
        {
            foreach (Match m in re.Matches(block))
            {
                if (!m.Success || m.Groups.Count < 4) continue;
                string id = m.Groups[1].Value ?? "";
                string prop = m.Groups[2].Value ?? "";
                string val = (m.Groups[3].Value ?? "").ToLowerInvariant().Trim();
                int line = baseLine + EstimateLineFrom0(block, m.Index) - 1;
                string key = file + "|" + cls + "|" + id + "|" + prop + "|" + val + "|" + line.ToString();
                if (!uniq.Add(key)) continue;
                list.Add(new PropRow { File = file, Class = cls, ControlId = id, Prop = prop, Value = val, Line = line });
            }
        }

        private static void AddPropPoint(List<PropRow> list, HashSet<string> uniq, Regex re, string block, string file, string cls, string propName, int baseLine)
        {
            foreach (Match m in re.Matches(block))
            {
                if (!m.Success || m.Groups.Count < 4) continue;
                string id = m.Groups[1].Value ?? "";
                string x = (m.Groups[2].Value ?? "").Trim();
                string y = (m.Groups[3].Value ?? "").Trim();
                string val = x + "," + y;
                int line = baseLine + EstimateLineFrom0(block, m.Index) - 1;
                string key = file + "|" + cls + "|" + id + "|" + propName + "|" + val + "|" + line.ToString();
                if (!uniq.Add(key)) continue;
                list.Add(new PropRow { File = file, Class = cls, ControlId = id, Prop = propName, Value = val, Line = line });
            }
        }

        private static void AddPropSize(List<PropRow> list, HashSet<string> uniq, Regex re, string block, string file, string cls, string propName, int baseLine)
        {
            foreach (Match m in re.Matches(block))
            {
                if (!m.Success || m.Groups.Count < 4) continue;
                string id = m.Groups[1].Value ?? "";
                string w = (m.Groups[2].Value ?? "").Trim();
                string h = (m.Groups[3].Value ?? "").Trim();
                string val = w + "," + h;
                int line = baseLine + EstimateLineFrom0(block, m.Index) - 1;
                string key = file + "|" + cls + "|" + id + "|" + propName + "|" + val + "|" + line.ToString();
                if (!uniq.Add(key)) continue;
                list.Add(new PropRow { File = file, Class = cls, ControlId = id, Prop = propName, Value = val, Line = line });
            }
        }
    }
}

C#

Posted by temochic