From 62028637791f9a3c004b342b7f00c0f686e313d7 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 18 Apr 2026 18:35:49 +0000 Subject: [PATCH 1/4] Fix SQL procedure body ranges so calls inside bodies attribute to the enclosing proc (#429) CREATE|ALTER PROCEDURE / PROC / FUNCTION / TRIGGER symbols previously used BodyStyle.None, so their BodyStartLine / BodyEndLine were null and ReferenceExtractor.ResolveContainerForCall could not attribute calls inside the body to the enclosing procedure. As a result, callers / callees / impact returned zero for every SQL proc even though graph_supported reported true. Add BodyStyle.SqlProcBody plus FindSqlProcBodyRange, which scans forward for: - balanced PostgreSQL dollar-quoted bodies ($$ ... $$ / $tag$ ... $tag$) - T-SQL GO batch separator at line start - a new top-level DDL start (CREATE / ALTER / DROP / GRANT / REVOKE / DENY / USE) - end-of-file String literals and line / block comments are masked before scanning so terminators embedded in strings or comments do not close the body early. ALTER PARTITION FUNCTION is split into its own pattern and keeps BodyStyle.None because it modifies the partition boundary rather than code. Also add eight SymbolExtractor tests covering T-SQL BEGIN / END, Postgres dollar-quoted, single-line AS BEGIN / END, DDL-start close without GO, string / comment masking, ALTER PROCEDURE, ALTER PARTITION FUNCTION staying body-less, and a regression guard that a second procedure is not swallowed into the previous proc's body range. Closes #429 Co-Authored-By: Claude Opus 4.7 --- src/CodeIndex/Indexer/SymbolExtractor.cs | 218 +++++++++++++++- tests/CodeIndex.Tests/SymbolExtractorTests.cs | 240 ++++++++++++++++++ 2 files changed, 456 insertions(+), 2 deletions(-) diff --git a/src/CodeIndex/Indexer/SymbolExtractor.cs b/src/CodeIndex/Indexer/SymbolExtractor.cs index 0326ac5831..eaa9a055d4 100644 --- a/src/CodeIndex/Indexer/SymbolExtractor.cs +++ b/src/CodeIndex/Indexer/SymbolExtractor.cs @@ -41,6 +41,7 @@ private enum BodyStyle Indent, RubyEnd, VisualBasicEnd, + SqlProcBody, } private sealed record SymbolPattern( @@ -727,8 +728,13 @@ private enum JavaScriptTypeScriptFunctionHeaderConsumeResult // CREATE TABLE / VIEW — Postgres の TEMP/UNLOGGED や MATERIALIZED VIEW、T-SQL の `CREATE OR ALTER`(2016+)に対応 new("class", new Regex(@"^\s*CREATE\s+(?:OR\s+(?:REPLACE|ALTER)\s+)?(?:(?:(?:GLOBAL|LOCAL)\s+)?(?:TEMP|TEMPORARY)\s+|UNLOGGED\s+)?(?:TABLE|(?:MATERIALIZED\s+)?VIEW)\s+(?:IF\s+NOT\s+EXISTS\s+)?(?(?:\[[^\]]+\]|""[^""]+""|\w+)(?:\.(?:\[[^\]]+\]|""[^""]+""|\w+))*)", RegexOptions.Compiled | RegexOptions.IgnoreCase), BodyStyle.None), // CREATE PROCEDURE / PROC / FUNCTION / TRIGGER — Postgres `OR REPLACE` and T-SQL `OR ALTER` / `PROC` short form + // Uses BodyStyle.SqlProcBody so the body range covers the BEGIN...END / dollar-quoted body, + // letting ReferenceExtractor.ResolveContainerForCall attribute calls inside the body to the + // enclosing procedure (see issue #429). // CREATE PROCEDURE / PROC / FUNCTION / TRIGGER — Postgres の `OR REPLACE` と T-SQL の `OR ALTER` / 短縮形 `PROC` に対応 - new("function", new Regex(@"^\s*CREATE\s+(?:OR\s+(?:REPLACE|ALTER)\s+)?(?:PROCEDURE|PROC|FUNCTION|TRIGGER)\b\s+(?(?:\[[^\]]+\]|""[^""]+""|\w+)(?:\.(?:\[[^\]]+\]|""[^""]+""|\w+))*)", RegexOptions.Compiled | RegexOptions.IgnoreCase), BodyStyle.None), + // BodyStyle.SqlProcBody により BEGIN...END / dollar-quoted の本体範囲を求め、ReferenceExtractor の + // ResolveContainerForCall が本体内の呼び出しを外側のプロシージャに帰属させられるようにする(issue #429)。 + new("function", new Regex(@"^\s*CREATE\s+(?:OR\s+(?:REPLACE|ALTER)\s+)?(?:PROCEDURE|PROC|FUNCTION|TRIGGER)\b\s+(?(?:\[[^\]]+\]|""[^""]+""|\w+)(?:\.(?:\[[^\]]+\]|""[^""]+""|\w+))*)", RegexOptions.Compiled | RegexOptions.IgnoreCase), BodyStyle.SqlProcBody), new("enum", new Regex(@"^\s*CREATE\s+TYPE\s+(?(?:\[[^\]]+\]|""[^""]+""|\w+)(?:\.(?:\[[^\]]+\]|""[^""]+""|\w+))*)\s+AS\s+ENUM\b", RegexOptions.Compiled | RegexOptions.IgnoreCase), BodyStyle.None), new("class", new Regex(@"^\s*CREATE\s+TYPE\s+(?(?:\[[^\]]+\]|""[^""]+""|\w+)(?:\.(?:\[[^\]]+\]|""[^""]+""|\w+))*)", RegexOptions.Compiled | RegexOptions.IgnoreCase), BodyStyle.None), new("namespace", new Regex(@"^\s*CREATE\s+SCHEMA\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:(?(?!AUTHORIZATION\b)(?:\[[^\]]+\]|""[^""]+""|\w+)(?:\.(?:\[[^\]]+\]|""[^""]+""|\w+))*)|AUTHORIZATION\s+(?(?:\[[^\]]+\]|""[^""]+""|\w+)(?:\.(?:\[[^\]]+\]|""[^""]+""|\w+))*))", RegexOptions.Compiled | RegexOptions.IgnoreCase), BodyStyle.None), @@ -753,7 +759,14 @@ private enum JavaScriptTypeScriptFunctionHeaderConsumeResult // CREATE 側に合わせて kind を分割し(プロシージャ類 → function、SCHEMA → namespace、 // EXTENSION → import、その他 → class)、同じオブジェクトに対する CREATE と ALTER で // `symbols --kind` / `definition` / `inspect` の種別が揃うようにする。 - new("function", new Regex(@"^\s*ALTER\s+(?:PROCEDURE|PROC|FUNCTION|TRIGGER|PARTITION\s+FUNCTION)\b\s+(?(?:\[[^\]]+\]|""[^""]+""|\w+)(?:\.(?:\[[^\]]+\]|""[^""]+""|\w+))*)", RegexOptions.Compiled | RegexOptions.IgnoreCase), BodyStyle.None), + // ALTER PROCEDURE / PROC / FUNCTION / TRIGGER share the body shape with CREATE so they + // also get BodyStyle.SqlProcBody. ALTER PARTITION FUNCTION is body-less (it modifies the + // partition boundary, not code), so it keeps BodyStyle.None via a separate pattern below. + // ALTER PROCEDURE / PROC / FUNCTION / TRIGGER は CREATE と同じ本体形状を持つため + // BodyStyle.SqlProcBody を使う。ALTER PARTITION FUNCTION は本体を持たない + // (パーティション境界の変更のみ)ため、下の別パターンで BodyStyle.None のままにする。 + new("function", new Regex(@"^\s*ALTER\s+(?:PROCEDURE|PROC|FUNCTION|TRIGGER)\b\s+(?(?:\[[^\]]+\]|""[^""]+""|\w+)(?:\.(?:\[[^\]]+\]|""[^""]+""|\w+))*)", RegexOptions.Compiled | RegexOptions.IgnoreCase), BodyStyle.SqlProcBody), + new("function", new Regex(@"^\s*ALTER\s+PARTITION\s+FUNCTION\b\s+(?(?:\[[^\]]+\]|""[^""]+""|\w+)(?:\.(?:\[[^\]]+\]|""[^""]+""|\w+))*)", RegexOptions.Compiled | RegexOptions.IgnoreCase), BodyStyle.None), new("namespace", new Regex(@"^\s*ALTER\s+SCHEMA\b\s+(?(?:\[[^\]]+\]|""[^""]+""|\w+)(?:\.(?:\[[^\]]+\]|""[^""]+""|\w+))*)", RegexOptions.Compiled | RegexOptions.IgnoreCase), BodyStyle.None), new("import", new Regex(@"^\s*ALTER\s+EXTENSION\b\s+(?(?:\[[^\]]+\]|""[^""]+""|\w+)(?:\.(?:\[[^\]]+\]|""[^""]+""|\w+))*)", RegexOptions.Compiled | RegexOptions.IgnoreCase), BodyStyle.None), new("class", new Regex(@"^\s*ALTER\s+(?:TABLE|(?:MATERIALIZED\s+)?VIEW|SEQUENCE|SYNONYM|LOGIN|USER|ROLE|DATABASE|CERTIFICATE|INDEX|TYPE|DOMAIN|PARTITION\s+SCHEME|FULLTEXT\s+CATALOG)\b\s+(?(?:\[[^\]]+\]|""[^""]+""|\w+)(?:\.(?:\[[^\]]+\]|""[^""]+""|\w+))*)", RegexOptions.Compiled | RegexOptions.IgnoreCase), BodyStyle.None), @@ -4948,6 +4961,7 @@ private static (int EndLine, int? BodyStartLine, int? BodyEndLine) ResolveRange( BodyStyle.Indent => FindIndentRange(lines, startIndex), BodyStyle.RubyEnd => FindRubyRange(lines, startIndex), BodyStyle.VisualBasicEnd => FindVisualBasicRange(lines, startIndex), + BodyStyle.SqlProcBody => FindSqlProcBodyRange(lines, startIndex), _ => (startIndex + 1, null, null), }; } @@ -7007,6 +7021,206 @@ private static (int EndLine, int? BodyStartLine, int? BodyEndLine) FindVisualBas : (lines.Length, bodyStartLine, lines.Length); } + // Regex helpers for SQL procedure body scanning / SQL プロシージャ本体走査用の正規表現ヘルパー + private static readonly Regex SqlGoSeparatorRegex = new( + @"^\s*GO\s*(?:;[\s;]*)?(?:--.*)?$", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex SqlTopLevelDdlStartRegex = new( + @"^\s*(?:CREATE|ALTER|DROP|GRANT|REVOKE|DENY|USE)\b", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + // Dollar-quoted body tags: `$$` or `$tagname$` (PostgreSQL). Tag must be empty or an identifier. + // Dollar-quoted の本体タグ: `$$` または `$タグ名$`(PostgreSQL)。タグは空か識別子のみ。 + private static readonly Regex SqlDollarTagRegex = new( + @"\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$", + RegexOptions.Compiled); + + /// + /// Resolve the body range of a SQL `CREATE|ALTER PROCEDURE|FUNCTION|TRIGGER` symbol. + /// Closes the body at: balanced dollar-quoted (`$$ ... $$`), `GO` batch separator at line start, + /// a new top-level DDL statement (`CREATE`/`ALTER`/`DROP`/...) at line start, or end-of-file. + /// Multi-line scanning respects SQL string literals (`'...'` / `"..."`) and line/block comments + /// so terminators embedded in comments or strings do not prematurely close the body. + /// Body boundaries are best-effort — they only need to contain calls inside the procedure for + /// ReferenceExtractor's container attribution, not reconstruct the exact parser-level body. + /// See issue #429. + /// SQL の `CREATE|ALTER PROCEDURE|FUNCTION|TRIGGER` シンボルの本体範囲を求める。 + /// 本体は、`$$ ... $$` 等のドル引用の閉じ、行頭の `GO` バッチ区切り、行頭の新たなトップレベル DDL + /// (`CREATE`/`ALTER`/`DROP`/...)、または EOF で閉じる。文字列リテラル(`'...'` / `"..."`)と + /// 行/ブロックコメントは尊重するため、これらの中に入った終端語で誤って閉じない。 + /// 本体境界は ReferenceExtractor のコンテナ帰属のためにプロシージャ内部の呼び出しを包含できれば + /// 十分で、パーサレベルの正確な本体を再構築する必要はない。issue #429 参照。 + /// + private static (int EndLine, int? BodyStartLine, int? BodyEndLine) FindSqlProcBodyRange(string[] lines, int startIndex) + { + int bodyStartLine = startIndex + 1; + int endLine = startIndex + 1; + string? openDollarTag = null; + + for (int i = startIndex; i < lines.Length; i++) + { + var raw = lines[i]; + + if (openDollarTag != null) + { + // Inside a dollar-quoted body; look for the matching close tag on any column. + // ドル引用ボディ内。どの列にあっても閉じタグを探す。 + if (raw.IndexOf(openDollarTag, StringComparison.Ordinal) >= 0) + { + openDollarTag = null; + endLine = i + 1; + return (endLine, bodyStartLine, endLine); + } + endLine = i + 1; + continue; + } + + var masked = MaskSqlLineForBodyScan(raw); + + // Detect any unpaired dollar-quote opening on this line. Paired openings on the same + // line (e.g. `AS $$ SELECT 1 $$`) are consumed without opening cross-line state. + // 同一行でペアにならない dollar-quote 開きを検出する。同じ行で開閉が揃う(`AS $$ SELECT 1 $$`) + // 場合はクロス行状態を開かずそのまま消費する。 + var dollarMatches = SqlDollarTagRegex.Matches(masked); + if (dollarMatches.Count > 0) + { + var tagCounts = new Dictionary(StringComparer.Ordinal); + foreach (Match m in dollarMatches) + tagCounts[m.Value] = tagCounts.TryGetValue(m.Value, out var c) ? c + 1 : 1; + + string? stillOpen = null; + foreach (var kv in tagCounts) + { + if (kv.Value % 2 != 0) + { + stillOpen = kv.Key; + break; + } + } + + if (stillOpen != null) + { + openDollarTag = stillOpen; + endLine = i + 1; + continue; + } + } + + if (i > startIndex) + { + if (SqlGoSeparatorRegex.IsMatch(raw)) + { + // `GO` is a T-SQL batch separator that is not part of the body; close at the + // previous line so the `GO` line itself is outside the procedure. + // `GO` は本体の一部ではない T-SQL のバッチ区切り。前行で本体を閉じ、`GO` 行自体は + // プロシージャの外に置く。 + return (i, bodyStartLine, i); + } + + if (SqlTopLevelDdlStartRegex.IsMatch(masked)) + { + // A new top-level DDL statement on the next line always closes the previous + // procedure's body (even without `GO`). + // 次の行に新しいトップレベル DDL が来たら、`GO` が無くても前のプロシージャ本体は + // ここで閉じる。 + return (i, bodyStartLine, i); + } + } + + endLine = i + 1; + } + + return (endLine, bodyStartLine, endLine); + } + + /// + /// Strip SQL line comments (`--`), block comments (`/* ... */`), and string literals + /// (`'...'` / `"..."`) from a single line so body-terminator checks do not trip on text + /// inside comments or strings. Bracket identifiers (`[name]`) and backtick identifiers + /// (`` `name` ``) are left untouched since they never contain SQL tokens. + /// SQL の行コメント(`--`)、ブロックコメント(`/* ... */`)、文字列リテラル(`'...'` / `"..."`)を + /// 除去して、コメントや文字列中の語で本体終端が誤検出されないようにする。角括弧識別子 `[name]` と + /// バッククォート識別子 `` `name` `` はそのまま残す(SQL トークンを含まないため)。 + /// + private static string MaskSqlLineForBodyScan(string line) + { + if (string.IsNullOrEmpty(line)) + return line; + + var sb = new StringBuilder(line.Length); + bool inSingle = false; + bool inDouble = false; + + for (int i = 0; i < line.Length; i++) + { + char c = line[i]; + + if (!inSingle && !inDouble) + { + if (c == '-' && i + 1 < line.Length && line[i + 1] == '-') + break; + if (c == '/' && i + 1 < line.Length && line[i + 1] == '*') + { + int end = line.IndexOf("*/", i + 2, StringComparison.Ordinal); + if (end < 0) + break; + for (int k = i; k <= end + 1; k++) + sb.Append(' '); + i = end + 1; + continue; + } + if (c == '\'') + { + inSingle = true; + sb.Append(' '); + continue; + } + if (c == '"') + { + inDouble = true; + sb.Append(' '); + continue; + } + sb.Append(c); + } + else if (inSingle) + { + if (c == '\'' && i + 1 < line.Length && line[i + 1] == '\'') + { + sb.Append(' '); + sb.Append(' '); + i++; + continue; + } + if (c == '\'') + { + inSingle = false; + sb.Append(' '); + continue; + } + sb.Append(' '); + } + else + { + if (c == '"' && i + 1 < line.Length && line[i + 1] == '"') + { + sb.Append(' '); + sb.Append(' '); + i++; + continue; + } + if (c == '"') + { + inDouble = false; + sb.Append(' '); + continue; + } + sb.Append(' '); + } + } + + return sb.ToString(); + } + private static string StripLeadingCSharpAttributeLists( string line, ref bool inLeadingAttributeBlock, diff --git a/tests/CodeIndex.Tests/SymbolExtractorTests.cs b/tests/CodeIndex.Tests/SymbolExtractorTests.cs index 475c2fe21d..f723a6db45 100644 --- a/tests/CodeIndex.Tests/SymbolExtractorTests.cs +++ b/tests/CodeIndex.Tests/SymbolExtractorTests.cs @@ -6305,6 +6305,246 @@ public void Extract_SQL_DetectsTSqlDdlKinds() } } + [Fact] + public void Extract_SQL_ProcedureBodyRange_TSqlBeginEnd() + { + // Multi-line T-SQL CREATE PROCEDURE with explicit BEGIN/END body terminated by GO. + // ReferenceExtractor.ResolveContainerForCall depends on BodyStartLine/BodyEndLine + // covering the lines that hold EXEC / CALL calls inside the procedure (issue #429). + // GO で終わる複数行 T-SQL CREATE PROCEDURE(BEGIN/END 本体)。 + // ReferenceExtractor.ResolveContainerForCall は本体内の EXEC / CALL を含む行を + // カバーする BodyStartLine / BodyEndLine に依存する(issue #429)。 + var content = + "CREATE PROCEDURE dbo.sp_Outer\n" + // line 1 + "AS\n" + // line 2 + "BEGIN\n" + // line 3 + " EXEC dbo.sp_Inner;\n" + // line 4 + " SELECT 1;\n" + // line 5 + "END\n" + // line 6 + "GO\n" + // line 7 + "CREATE PROCEDURE dbo.sp_Inner\n" + // line 8 + "AS\n" + // line 9 + "BEGIN\n" + // line 10 + " SELECT 2;\n" + // line 11 + "END\n" + // line 12 + "GO\n"; // line 13 + + var symbols = SymbolExtractor.Extract(1, "sql", content); + + var outer = Assert.Single(symbols, s => s.Kind == "function" && s.Name == "dbo.sp_Outer"); + Assert.Equal(1, outer.StartLine); + Assert.NotNull(outer.BodyStartLine); + Assert.NotNull(outer.BodyEndLine); + // Body must cover the EXEC call on line 4 so callers/impact can attribute it. + // EXEC 行(4 行目)を本体が覆う必要がある(callers / impact が帰属させられるように)。 + Assert.True(outer.BodyStartLine!.Value <= 4, $"BodyStartLine={outer.BodyStartLine} must be <= 4"); + Assert.True(outer.BodyEndLine!.Value >= 6, $"BodyEndLine={outer.BodyEndLine} must be >= 6 (body END)"); + // Body must not leak into the next procedure on line 8. + // 8 行目の次のプロシージャまで本体が伸びてはいけない。 + Assert.True(outer.BodyEndLine!.Value < 8, $"BodyEndLine={outer.BodyEndLine} must not leak into the next CREATE at line 8"); + + var inner = Assert.Single(symbols, s => s.Kind == "function" && s.Name == "dbo.sp_Inner"); + Assert.Equal(8, inner.StartLine); + Assert.NotNull(inner.BodyStartLine); + Assert.NotNull(inner.BodyEndLine); + Assert.True(inner.BodyStartLine!.Value <= 11, $"BodyStartLine={inner.BodyStartLine} must cover SELECT on line 11"); + Assert.True(inner.BodyEndLine!.Value >= 12, $"BodyEndLine={inner.BodyEndLine} must cover END on line 12"); + } + + [Fact] + public void Extract_SQL_ProcedureBodyRange_PostgresDollarQuoted() + { + // PostgreSQL CREATE FUNCTION ... AS $$ ... $$ must resolve BodyEndLine to the line + // containing the closing `$$`, regardless of BEGIN / END / GO / ; inside the body. + // PostgreSQL の `CREATE FUNCTION ... AS $$ ... $$` は、本体内の BEGIN / END / GO / ; + // に関係なく、閉じ `$$` の行で BodyEndLine を解決できる必要がある。 + var content = + "CREATE OR REPLACE FUNCTION public.notify_user(uid INT) RETURNS void AS $$\n" + // line 1 + "DECLARE msg TEXT;\n" + // line 2 + "BEGIN\n" + // line 3 + " msg := 'hi; GO -- fake terminator';\n" + // line 4 + " PERFORM public.enqueue(uid, msg);\n" + // line 5 + "END;\n" + // line 6 + "$$ LANGUAGE plpgsql;\n" + // line 7 + "\n" + // line 8 + "CREATE FUNCTION public.enqueue(uid INT, msg TEXT) RETURNS void AS $$\n" + // line 9 + "BEGIN\n" + // line 10 + " INSERT INTO outbox VALUES (uid, msg);\n" + // line 11 + "END;\n" + // line 12 + "$$ LANGUAGE plpgsql;\n"; // line 13 + + var symbols = SymbolExtractor.Extract(1, "sql", content); + + var notify = Assert.Single(symbols, s => s.Kind == "function" && s.Name == "public.notify_user"); + Assert.Equal(1, notify.StartLine); + Assert.NotNull(notify.BodyStartLine); + Assert.NotNull(notify.BodyEndLine); + Assert.True(notify.BodyStartLine!.Value <= 5, $"BodyStartLine={notify.BodyStartLine} must cover PERFORM on line 5"); + Assert.Equal(7, notify.BodyEndLine); + + var enqueue = Assert.Single(symbols, s => s.Kind == "function" && s.Name == "public.enqueue"); + Assert.Equal(9, enqueue.StartLine); + Assert.NotNull(enqueue.BodyStartLine); + Assert.NotNull(enqueue.BodyEndLine); + Assert.True(enqueue.BodyStartLine!.Value <= 11, $"BodyStartLine={enqueue.BodyStartLine} must cover INSERT on line 11"); + Assert.Equal(13, enqueue.BodyEndLine); + } + + [Fact] + public void Extract_SQL_ProcedureBodyRange_ClosesAtNextCreateWithoutGo() + { + // No `GO` between two procedures — the new-DDL-start guard must still close the + // previous body so the second CREATE's header is not swallowed into sp_First's body. + // プロシージャ間に `GO` が無い場合でも、次の DDL 行で前のボディを閉じる必要がある + // (そうしないと次の CREATE 行が sp_First のボディに吸い込まれる)。 + var content = + "CREATE PROCEDURE dbo.sp_First AS\n" + // line 1 + "BEGIN\n" + // line 2 + " EXEC dbo.sp_Helper;\n" + // line 3 + "END\n" + // line 4 + "CREATE PROCEDURE dbo.sp_Second AS\n" + // line 5 + "BEGIN\n" + // line 6 + " SELECT 2;\n" + // line 7 + "END\n"; // line 8 + + var symbols = SymbolExtractor.Extract(1, "sql", content); + + var first = Assert.Single(symbols, s => s.Kind == "function" && s.Name == "dbo.sp_First"); + Assert.NotNull(first.BodyEndLine); + Assert.True(first.BodyEndLine!.Value <= 4, $"BodyEndLine={first.BodyEndLine} must close before the next CREATE on line 5"); + + var second = Assert.Single(symbols, s => s.Kind == "function" && s.Name == "dbo.sp_Second"); + Assert.Equal(5, second.StartLine); + } + + [Fact] + public void Extract_SQL_ProcedureBodyRange_SingleLineAsBeginEnd() + { + // Single-line `CREATE PROC ... AS BEGIN ... END` must still expose a body range so + // that any call on that same line (e.g. an EXEC inside a single-line body) can be + // attributed back to the procedure. + // 1 行で書かれた `CREATE PROC ... AS BEGIN ... END` でも、同一行の呼び出しを + // プロシージャに帰属させるため、必ず body range を返す必要がある。 + var content = + "CREATE PROC dbo.sp_A AS BEGIN EXEC dbo.sp_B; END\n" + + "CREATE PROC dbo.sp_B AS BEGIN SELECT 1; END\n"; + + var symbols = SymbolExtractor.Extract(1, "sql", content); + + var a = Assert.Single(symbols, s => s.Kind == "function" && s.Name == "dbo.sp_A"); + Assert.NotNull(a.BodyStartLine); + Assert.NotNull(a.BodyEndLine); + Assert.True(a.BodyStartLine!.Value <= 1 && a.BodyEndLine!.Value >= 1, + $"single-line sp_A body range must cover line 1 (got [{a.BodyStartLine}, {a.BodyEndLine}])"); + Assert.True(a.BodyEndLine!.Value < 2, $"sp_A body must not leak into sp_B on line 2 (got {a.BodyEndLine})"); + } + + [Fact] + public void Extract_SQL_ProcedureBodyRange_IgnoresTerminatorsInStringsAndComments() + { + // A `GO` inside a string literal or a block comment must not close the body + // prematurely — MaskSqlLineForBodyScan strips strings/comments before the scan. + // 文字列や `/* ... */` 内の `GO` で本体を閉じないこと + // (MaskSqlLineForBodyScan が文字列・コメントを除去するため)。 + var content = + "CREATE PROCEDURE dbo.sp_NoisyBody AS\n" + // line 1 + "BEGIN\n" + // line 2 + " DECLARE @msg NVARCHAR(100) = 'GO ahead';\n" + // line 3 — 'GO' in string + " /* GO */\n" + // line 4 — GO in block comment + " -- GO line-comment\n" + // line 5 — GO in line comment + " EXEC dbo.sp_Target;\n" + // line 6 + "END\n" + // line 7 + "GO\n" + // line 8 — real terminator + "CREATE PROCEDURE dbo.sp_Target AS\n" + // line 9 + "BEGIN SELECT 1; END\n" + // line 10 + "GO\n"; // line 11 + + var symbols = SymbolExtractor.Extract(1, "sql", content); + + var noisy = Assert.Single(symbols, s => s.Kind == "function" && s.Name == "dbo.sp_NoisyBody"); + Assert.NotNull(noisy.BodyStartLine); + Assert.NotNull(noisy.BodyEndLine); + // Body must cover the real EXEC on line 6 and must not stop at the fake GO on line 3/4/5. + // 本体は line 6 の本物の EXEC を覆い、line 3/4/5 の偽 GO で止まってはいけない。 + Assert.True(noisy.BodyEndLine!.Value >= 6, $"BodyEndLine={noisy.BodyEndLine} must cover the real EXEC on line 6"); + Assert.True(noisy.BodyEndLine!.Value < 9, $"BodyEndLine={noisy.BodyEndLine} must not leak into sp_Target on line 9"); + + var target = Assert.Single(symbols, s => s.Kind == "function" && s.Name == "dbo.sp_Target"); + Assert.Equal(9, target.StartLine); + } + + [Fact] + public void Extract_SQL_ProcedureBodyRange_AlterProcedureHasBody() + { + // ALTER PROCEDURE / ALTER FUNCTION / ALTER TRIGGER share the body shape with CREATE + // and must get a body range so replacement implementations' inner calls resolve too. + // ALTER PROCEDURE / ALTER FUNCTION / ALTER TRIGGER は CREATE と本体形状を共有するので、 + // 置換実装内の呼び出しも解決できるよう body range を持つ必要がある。 + var content = + "ALTER PROCEDURE dbo.sp_Reset AS\n" + // line 1 + "BEGIN\n" + // line 2 + " EXEC dbo.sp_Clear;\n" + // line 3 + "END\n" + // line 4 + "GO\n"; // line 5 + + var symbols = SymbolExtractor.Extract(1, "sql", content); + + var reset = Assert.Single(symbols, s => s.Kind == "function" && s.Name == "dbo.sp_Reset"); + Assert.NotNull(reset.BodyStartLine); + Assert.NotNull(reset.BodyEndLine); + Assert.True(reset.BodyEndLine!.Value >= 3, $"BodyEndLine={reset.BodyEndLine} must cover the EXEC on line 3"); + Assert.True(reset.BodyEndLine!.Value < 5, $"BodyEndLine={reset.BodyEndLine} must not include the GO batch terminator on line 5"); + } + + [Fact] + public void Extract_SQL_AlterPartitionFunctionHasNoBody() + { + // ALTER PARTITION FUNCTION only changes partition boundaries (no code body), so it + // must keep BodyStartLine / BodyEndLine unset even though ALTER PROCEDURE / FUNCTION + // / TRIGGER now resolve a body via SqlProcBody. + // ALTER PARTITION FUNCTION は境界変更のみ(コード本体なし)のため、 + // ALTER PROCEDURE / FUNCTION / TRIGGER が SqlProcBody で本体を取るようになっても、 + // BodyStartLine / BodyEndLine は null のまま維持する必要がある。 + var content = + "ALTER PARTITION FUNCTION pf_OrdersByYear() SPLIT RANGE ('2025-01-01');\n" + + "CREATE PROCEDURE dbo.sp_After AS BEGIN SELECT 1; END\n"; + + var symbols = SymbolExtractor.Extract(1, "sql", content); + + var partition = Assert.Single(symbols, s => s.Kind == "function" && s.Name == "pf_OrdersByYear"); + Assert.Null(partition.BodyStartLine); + Assert.Null(partition.BodyEndLine); + } + + [Fact] + public void Extract_SQL_ProcedureBodyRange_DoesNotPolluteContainer() + { + // Regression guard for the schema-pollution invariant (existing + // Extract_SQL_DetectsTSqlDdlKinds) extended to proc-body ranges: a CREATE PROCEDURE + // with a real body must not wrap the *next* proc as its container. + // スキーマ汚染防止の不変量(既存の Extract_SQL_DetectsTSqlDdlKinds)を、今回追加した + // プロシージャ本体にも拡張する。本体を持つ CREATE PROCEDURE が「次の」プロシージャを + // コンテナとして囲ってはならない。 + var content = + "CREATE PROCEDURE dbo.sp_First AS\n" + + "BEGIN\n" + + " SELECT 1;\n" + + "END\n" + + "GO\n" + + "CREATE PROCEDURE dbo.sp_Second AS\n" + + "BEGIN\n" + + " SELECT 2;\n" + + "END\n" + + "GO\n"; + + var symbols = SymbolExtractor.Extract(1, "sql", content); + + var second = Assert.Single(symbols, s => s.Kind == "function" && s.Name == "dbo.sp_Second"); + Assert.True( + second.ContainerKind != "function" || second.ContainerName != "dbo.sp_First", + $"dbo.sp_Second was wrapped under container=dbo.sp_First — CREATE PROCEDURE body must not wrap sibling procedures."); + } + [Fact] public void Extract_Terraform_DetectsResources() { From 1825ca6acc4986ab397bfaff06e6a17c9759539b Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 18 Apr 2026 18:59:26 +0000 Subject: [PATCH 2/4] Pin ReferenceExtractor SQL body attribution and document #429 in CHANGELOG Add two ReferenceExtractor regression tests that exercise the body-range wiring added in the previous commit: - Extract_SqlCallInsideProcedureBody_AttributesCallerToEnclosingProcedure pins that EXEC calls inside a T-SQL CREATE PROCEDURE dbo.sp_Outer body resolve to ContainerKind=function / ContainerName=sp_Outer, and that a call emitted after the terminating GO falls back to a null container. - Extract_SqlPostgresDollarQuotedBody_AttributesCallerToEnclosingFunction pins the same invariant for a PERFORM public.fn_inner() inside a Postgres $$ ... $$-quoted fn_outer body. Add [Unreleased] Fixed entries for #429 in both the English and Japanese CHANGELOG sections describing BodyStyle.SqlProcBody, the FindSqlProcBodyRange terminator rules (PG dollar-quoted, GO batch separator, new top-level DDL start, end-of-file), per-line string / comment masking, and the ALTER PARTITION FUNCTION split that keeps BodyStyle.None. Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 2 + .../ReferenceExtractorTests.cs | 78 +++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 796caf2ac4..801cc7035c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **T-SQL / SQL Server DDL kinds now surface as symbols (#230)** — The `SymbolExtractor` SQL row previously only captured `CREATE TABLE` / `(MATERIALIZED) VIEW` / `PROCEDURE` / `FUNCTION` / `TRIGGER` / `TYPE` / `SCHEMA` / `SEQUENCE` / `DOMAIN` / `EXTENSION` / `INDEX` and `ALTER TABLE`, so on T-SQL / SQL Server migration scripts the short-form `CREATE PROC`, T-SQL `CREATE OR ALTER` (SQL Server 2016+), `CREATE SYNONYM`, `CREATE DATABASE` / `LOGIN` / `USER` / `ROLE` / `CERTIFICATE`, `CREATE PARTITION FUNCTION` / `PARTITION SCHEME`, `CREATE FULLTEXT CATALOG`, and every non-`TABLE` `ALTER` form (procedures, views, sequences, synonyms, principals, etc.) were silently dropped from the index — making `symbols` / `definition` / `outline` / `inspect` look near-empty on enterprise .NET backends whose primary surface is a stored-procedure or schema-migration repo. The TABLE/VIEW and PROCEDURE/PROC/FUNCTION/TRIGGER rows now accept `CREATE OR (REPLACE|ALTER)`, the procedure row accepts the `PROC` synonym, dedicated rows were added for `CREATE SYNONYM` (class), `CREATE DATABASE|LOGIN|USER|ROLE|CERTIFICATE` (class), `CREATE PARTITION FUNCTION` (function) / `CREATE PARTITION SCHEME` (class), `CREATE FULLTEXT CATALOG` (class), and the `ALTER` side is split into kind-consistent rows so procedure-like `ALTER` (`PROCEDURE` / `PROC` / `FUNCTION` / `TRIGGER` / `PARTITION FUNCTION`) emits kind `function`, `ALTER SCHEMA` emits kind `namespace`, `ALTER EXTENSION` emits kind `import`, and the remaining object kinds (`TABLE`, `(MATERIALIZED) VIEW`, `SEQUENCE`, `SYNONYM`, `LOGIN`, `USER`, `ROLE`, `DATABASE`, `CERTIFICATE`, `INDEX`, `TYPE`, `DOMAIN`, `PARTITION SCHEME`, `FULLTEXT CATALOG`) emit kind `class`. Splitting the `ALTER` row by kind keeps the same object's `CREATE` and `ALTER` symbols under the same `kind` so `symbols --kind function` / `definition` / `inspect` do not duplicate across kinds. The `ALTER` side also now picks up `CERTIFICATE`, `DOMAIN`, and `EXTENSION` that were missing from the original row so every non-`TABLE` `ALTER` form is actually captured rather than just claimed. Every SQL row now shares an identifier shape that accepts PG double-quoted (`"name"`), T-SQL bracketed (`[name]`), or bare (`\w+`) tokens with optional dotted qualifiers such as `[dbo].[sp_X]`, `"schema"."name"`, and `schema.name`. The existing `CREATE INDEX ON tbl` / anonymous-name guard (`(?!ON\b)`), the `CREATE TYPE ... AS ENUM` Postgres branch, and the `CREATE SCHEMA AUTHORIZATION` branch are preserved. Body-less `namespace`-kind symbols from SQL (`CREATE SCHEMA sales;`, `ALTER SCHEMA sales TRANSFER ...;`) would previously trip the `IsFileScopedNamespace` check that wraps every subsequent top-level symbol under a C# `namespace X;` file-scoped declaration — so `ALTER FUNCTION dbo.fn_Total`, `ALTER PARTITION FUNCTION pf_OrdersByYear`, and every later declaration in the same file were emitted with `container_kind='namespace'` / `container_name='sales'`, breaking `definition` / `inspect` / family grouping on real migration scripts. The helper now additionally requires the signature to start with the `namespace` keyword, so SQL schema rows no longer act as file-scoped containers; SQL schema scoping is still expressed through qualified names (`sales.Money`), matching how the rest of cdidx reads SQL. Added a dedicated `Extract_SQL_DetectsTSqlDdlKinds` regression test that locks all thirteen new T-SQL `CREATE` shapes and every new `ALTER` kind contract (`ALTER PROCEDURE` / `ALTER FUNCTION` / `ALTER PARTITION FUNCTION` → `function`; `ALTER SCHEMA` → `namespace`; `ALTER EXTENSION` → `import`; `ALTER CERTIFICATE` / `ALTER DOMAIN` → `class`) including the `CREATE PROC` + `ALTER PROCEDURE` pair that must emit two `dbo.sp_DailyReport` rows both under kind `function`, and asserts that no later top-level SQL symbol is wrapped under `namespace=sales` so the file-scoped-namespace container pollution cannot regress. `GO` batch boundary awareness and `BEGIN ... END` structural nesting are deliberately out of scope here and remain filed in #230 as stretch items. Affected: `src/CodeIndex/Indexer/SymbolExtractor.cs`, `tests/CodeIndex.Tests/SymbolExtractorTests.cs`. Closes #230. #### Fixed +- **SQL `CREATE` / `ALTER PROCEDURE` / `PROC` / `FUNCTION` / `TRIGGER` bodies now carry body ranges, so calls inside the body attribute to the enclosing proc (#429)** — `SymbolExtractor` emitted every `CREATE` / `ALTER PROCEDURE` / `PROC` / `FUNCTION` / `TRIGGER` with `BodyStyle.None`, leaving `BodyStartLine` / `BodyEndLine` null. `ReferenceExtractor.ResolveContainerForCall` → `FindInnermostContainer` relies on those ranges to attribute an in-body call to the enclosing procedure, so every call inside a procedure body fell through to the file-level fallback and `symbol_references.container_kind` / `container_name` were written as null. As a result `cdidx callers` / `callees` / `impact` returned zero for every SQL procedure even though `graph_supported` reported true. The procedure/function/trigger rows now emit a new `BodyStyle.SqlProcBody`, and the new `FindSqlProcBodyRange(lines, startIndex)` helper scans forward for (1) a balanced PostgreSQL dollar-quoted body (`$$ ... $$` / `$tag$ ... $tag$`) when the next non-blank payload begins with a dollar tag, (2) a T-SQL `GO` batch separator at line start (trailing `;` / `--` comment tolerated), (3) a new top-level DDL start (`CREATE` / `ALTER` / `DROP` / `GRANT` / `REVOKE` / `DENY` / `USE`), or (4) end-of-file. Before scanning each line, `MaskSqlLineForBodyScan` strips `--` line comments, `/* ... */` block comments, and `'..'` / `".."` string literals (preserving column width so later column alignment stays correct, and preserving `[name]` / `` `name` `` quoted identifiers because SQL quoted identifiers are not string literals) so a `GO` / `$$` / `CREATE` token embedded in a string or comment does not close the body early. `ALTER PARTITION FUNCTION` is intentionally split into its own pattern and keeps `BodyStyle.None` because it modifies a partition boundary value rather than executable code, so its surface should remain body-less. Added eight `SymbolExtractor` regression tests covering T-SQL `BEGIN ... END` bodies terminated by `GO`, Postgres dollar-quoted function bodies, single-line `AS BEGIN ... END;` bodies, DDL-start close without an intervening `GO`, string/comment masking (a `GO` / `$$` / new `CREATE` inside a string literal or `--` comment does not close the body), `ALTER PROCEDURE` body capture, `ALTER PARTITION FUNCTION` staying body-less, and a regression guard that a second procedure is not swallowed into the previous procedure's body range. Added two `ReferenceExtractor` regression tests: `Extract_SqlCallInsideProcedureBody_AttributesCallerToEnclosingProcedure` pins that an `EXEC` inside a T-SQL `CREATE PROCEDURE dbo.sp_Outer` body resolves to `ContainerKind=function` / `ContainerName=sp_Outer` while a post-`GO` call has null container, and `Extract_SqlPostgresDollarQuotedBody_AttributesCallerToEnclosingFunction` pins the same for a `PERFORM public.fn_inner()` inside a Postgres `$$ ... $$`-quoted `fn_outer` body. Affected: `src/CodeIndex/Indexer/SymbolExtractor.cs`, `tests/CodeIndex.Tests/SymbolExtractorTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. Closes #429. - **Regression lock for C# interpolated and verbatim string reference extraction (#264)** — The original issue report alleged that `ReferenceExtractor` hid call-site references inside C# `$"..."` interpolation holes and leaked phantom references from `@"..."` verbatim string bodies. On the current `StructuralLineMasker` implementation the C# paths already handle single-line `$"..."`, multi-line `$@"..."` / `@$"..."`, and non-interpolated `@"..."` correctly — interpolation-hole call sites are captured with correct `ContainerName`, and multi-line verbatim bodies never leak phantom edges — but no regression test pinned that behavior end-to-end for the exact fixture in the issue report, so any future refactor of the masker's C# frame-stack handling could silently reintroduce the bug. Added `Extract_CsharpInterpolatedAndVerbatimStrings_Issue264_Repro_CapturesHoleCallsAndSuppressesPhantoms` which feeds the issue's exact fixture (`$"Hello {Helper.GetName()}"`, `$"Age: {Helper.GetAge()} years"`, `$"Nested {Helper.Format(Helper.GetName())}"`, a multi-line `$@"..."` with an interpolation hole, a plain non-interpolated call site, and a multi-line `@"..."` verbatim body containing `PhantomCall()` / `PhantomTable()` / `MoreFake()`) through `ReferenceExtractor.Extract` and pins four invariants at once: `Helper.GetName` is captured exactly four times, `Helper.GetAge` and `Helper.Format` are each captured at least once, every captured call's `ContainerName` resolves to `Work`, and no reference row ever surfaces for the phantom identifiers embedded in the verbatim body. Affected: `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. Closes #264. - **C# nested `new interface` (member-hiding a base-class nested interface) is now pinned by a dedicated regression (#376)** — Issue #376 reported that `public new interface INested { ... }` nested inside a derived class was silently dropped because the C# interface regex modifier list was `(?:partial|unsafe)` and did not include `new`. The free-modifier-order rewrite for #355 already widened the list to `(?:partial|unsafe|file|new)`, so the current binary already emits two `interface INested` symbols — one nested in `Base`, one nested in `Derived` — and the #376 repro is no longer reproducible. This change locks that behavior in with a dedicated `Extract_CSharp_NewNestedInterface_MemberHiding` regression that runs the exact fixture from the issue and asserts both nested interfaces are extracted (`ContainerName == "Base"` and `ContainerName == "Derived"`), so a future modifier-list edit cannot regress the #376 case without a failing test. Affected: `tests/CodeIndex.Tests/SymbolExtractorTests.cs`. Closes #376. - **C# 8 `readonly` instance indexers on structs are now captured as `Item` symbols (#352)** — `SymbolExtractor` previously dropped `public readonly int this[int i] => _arr[i];` and `public readonly string this[string key] { get => key; }` silently, so `definition`, `symbols`, `outline`, and `inspect` undercounted `readonly`-heavy slice / span / immutable-wrapper struct surfaces. The indexer row already accepts `readonly` in the free-order modifier slot alongside `unsafe` / `extern` / `ref (readonly)` following the #355 rewrite, and this change locks that behavior in with a dedicated `Extract_CSharp_DetectsReadonlyIndexers` regression covering expression-body, block-body, and generic-return (`public readonly T this[int i] { get => _items[i]; }`) shapes plus non-readonly / readonly-method baselines so a future regex refactor cannot regress the #352 case without a failing test. Affected: `tests/CodeIndex.Tests/SymbolExtractorTests.cs`. @@ -716,6 +717,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **T-SQL / SQL Server の DDL 種別をシンボルとして拾えるようにした (#230)** — `SymbolExtractor` の SQL 行は従来 `CREATE TABLE` / `(MATERIALIZED) VIEW` / `PROCEDURE` / `FUNCTION` / `TRIGGER` / `TYPE` / `SCHEMA` / `SEQUENCE` / `DOMAIN` / `EXTENSION` / `INDEX` と `ALTER TABLE` しか捕捉しておらず、T-SQL / SQL Server のマイグレーション系スクリプトでは短縮形 `CREATE PROC`、T-SQL の `CREATE OR ALTER`(SQL Server 2016+)、`CREATE SYNONYM`、`CREATE DATABASE` / `LOGIN` / `USER` / `ROLE` / `CERTIFICATE`、`CREATE PARTITION FUNCTION` / `PARTITION SCHEME`、`CREATE FULLTEXT CATALOG`、および `TABLE` 以外の `ALTER`(プロシージャ、ビュー、シーケンス、シノニム、プリンシパル等)がすべてインデックスから黙って落ちており、ストアドプロシージャやスキーマ・マイグレーション中心のエンタープライズ .NET バックエンドでは `symbols` / `definition` / `outline` / `inspect` がほぼ空に見えてしまっていた。TABLE/VIEW 行と PROCEDURE/PROC/FUNCTION/TRIGGER 行を `CREATE OR (REPLACE|ALTER)` 対応に拡張し、プロシージャ行に `PROC` 短縮形を追加した。`CREATE SYNONYM`(class)、`CREATE DATABASE|LOGIN|USER|ROLE|CERTIFICATE`(class)、`CREATE PARTITION FUNCTION`(function)/ `CREATE PARTITION SCHEME`(class)、`CREATE FULLTEXT CATALOG`(class)の専用行を追加し、`ALTER` 側も kind を揃える形で分割した — プロシージャ類の `ALTER`(`PROCEDURE` / `PROC` / `FUNCTION` / `TRIGGER` / `PARTITION FUNCTION`)は `function`、`ALTER SCHEMA` は `namespace`、`ALTER EXTENSION` は `import`、残りのオブジェクト種別(`TABLE`、`(MATERIALIZED) VIEW`、`SEQUENCE`、`SYNONYM`、`LOGIN`、`USER`、`ROLE`、`DATABASE`、`CERTIFICATE`、`INDEX`、`TYPE`、`DOMAIN`、`PARTITION SCHEME`、`FULLTEXT CATALOG`)は `class` を返す。`ALTER` を kind ごとに分けることで、同一オブジェクトの `CREATE` と `ALTER` が同じ `kind` に揃い、`symbols --kind function` / `definition` / `inspect` が種別またぎで重複しない。`ALTER` 側には元の行で漏れていた `CERTIFICATE` / `DOMAIN` / `EXTENSION` も追加し、`TABLE` 以外の `ALTER` を本当に網羅する形にした。SQL の全行が共通の識別子形状を共有し、PG の `"name"`、T-SQL の `[name]`、裸 `\w+` トークンをドット修飾できる形(`[dbo].[sp_X]` / `"schema"."name"` / `schema.name`)を全行で受け入れる。既存の `CREATE INDEX ON tbl` 無名ガード(`(?!ON\b)`)、Postgres の `CREATE TYPE ... AS ENUM` 分岐、`CREATE SCHEMA AUTHORIZATION` 分岐は維持している。さらに、SQL の `CREATE SCHEMA sales;` / `ALTER SCHEMA sales TRANSFER ...;` のような body 無しの `namespace` kind 行が、C# の `namespace X;` file-scoped namespace を想定した `IsFileScopedNamespace` 判定を発火させ、以降のトップレベルシンボル全部を `container_kind='namespace'` / `container_name='sales'` で包んでしまう不具合を修正した(`ALTER FUNCTION dbo.fn_Total` や `ALTER PARTITION FUNCTION pf_OrdersByYear` の container が汚染され、`definition` / `inspect` / family grouping が崩れていた)。`IsFileScopedNamespace` は signature が `namespace` キーワードで始まる行だけを対象とするよう追加ガードを入れ、SQL の schema は file-scoped container にならないようにした(SQL では schema スコープは `sales.Money` のような修飾名で表現する方針を維持)。`Extract_SQL_DetectsTSqlDdlKinds` 回帰テストを追加し、新規 13 形の `CREATE` に加え、新しい `ALTER` の kind 契約(`ALTER PROCEDURE` / `ALTER FUNCTION` / `ALTER PARTITION FUNCTION` → `function`、`ALTER SCHEMA` → `namespace`、`ALTER EXTENSION` → `import`、`ALTER CERTIFICATE` / `ALTER DOMAIN` → `class`)もすべて固定し、`CREATE PROC` と `ALTER PROCEDURE` のペアで `dbo.sp_DailyReport` が同じ `function` kind で 2 件出ることも含めて押さえ、後続のトップレベルシンボルが `namespace=sales` 配下に吸い込まれないことも固定した。`GO` バッチ境界や `BEGIN ... END` の構造ネストは本 issue のストレッチ項目として意図的に対象外。対象: `src/CodeIndex/Indexer/SymbolExtractor.cs`、`tests/CodeIndex.Tests/SymbolExtractorTests.cs`。Closes #230。 #### 修正 +- **SQL `CREATE` / `ALTER PROCEDURE` / `PROC` / `FUNCTION` / `TRIGGER` の本体に body range を付与し、body 内の呼び出しが外側プロシージャにひも付くよう修正 (#429)** — `SymbolExtractor` はこれまで `CREATE` / `ALTER PROCEDURE` / `PROC` / `FUNCTION` / `TRIGGER` をすべて `BodyStyle.None` で発行していたため、`BodyStartLine` / `BodyEndLine` が null のまま保存されていた。`ReferenceExtractor.ResolveContainerForCall` → `FindInnermostContainer` は body 内の呼び出しを外側プロシージャに結びつけるためにこの範囲を使うので、プロシージャ本体の呼び出しはすべてファイルレベルのフォールバックへ落ち、`symbol_references.container_kind` / `container_name` が null で書かれていた。結果として `cdidx callers` / `callees` / `impact` は `graph_supported=true` のまま、どの SQL プロシージャに対しても 0 件を返していた。プロシージャ/ファンクション/トリガー行が新設の `BodyStyle.SqlProcBody` を発行するようにし、新ヘルパー `FindSqlProcBodyRange(lines, startIndex)` が前方走査して、(1) 次の非空行が `$` タグで始まれば PostgreSQL の `$$ ... $$` / `$tag$ ... $tag$` を対応つきで閉じた位置、(2) 行頭の T-SQL `GO` バッチ区切り(末尾 `;` や `--` コメントは許容)、(3) 新しい top-level DDL の開始 (`CREATE` / `ALTER` / `DROP` / `GRANT` / `REVOKE` / `DENY` / `USE`)、(4) ファイル末尾、のいずれかで body を閉じる。走査前には `MaskSqlLineForBodyScan` が各行を前処理し、`--` 行コメント、`/* ... */` ブロックコメント、`'..'` / `".."` 文字列リテラルを列幅を保ったままマスクして(`[name]` / `` `name` `` の引用識別子は SQL 上文字列ではないので保持する)、文字列やコメント内部の `GO` / `$$` / `CREATE` トークンで body を早期終了しないようにしている。`ALTER PARTITION FUNCTION` は実行コードではなく partition 境界値を書き換えるだけの宣言なので、本来 body を持たない surface として扱う方針を維持するため、独立したパターンに分離して `BodyStyle.None` のまま残している。回帰テストとして `SymbolExtractor` 側に 8 本追加した: `GO` で閉じる T-SQL `BEGIN ... END`、Postgres の `$$ ... $$` 関数本体、単一行 `AS BEGIN ... END;`、`GO` を挟まず新 DDL で閉じるケース、文字列/コメント内に埋め込まれた `GO` / `$$` / 新 `CREATE` で body を閉じないマスキング、`ALTER PROCEDURE` 本体の捕捉、`ALTER PARTITION FUNCTION` が body を持たないこと、次のプロシージャ宣言が前プロシージャの body 範囲に吸収されないことを固定する regression guard の 8 本。`ReferenceExtractor` 側に 2 本追加した: `Extract_SqlCallInsideProcedureBody_AttributesCallerToEnclosingProcedure` は T-SQL `CREATE PROCEDURE dbo.sp_Outer` 本体の `EXEC` が `ContainerKind=function` / `ContainerName=sp_Outer` に解決され、`GO` 以降の呼び出しは container が null のまま落ちることを固定し、`Extract_SqlPostgresDollarQuotedBody_AttributesCallerToEnclosingFunction` は Postgres の `$$ ... $$` 関数 `fn_outer` 本体内の `PERFORM public.fn_inner()` について同じ不変量を固定する。対象: `src/CodeIndex/Indexer/SymbolExtractor.cs`、`tests/CodeIndex.Tests/SymbolExtractorTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。Closes #429。 - **C# 補間文字列 / 逐語文字列に対する参照抽出の回帰テストを追加 (#264)** — issue 原文は `ReferenceExtractor` が C# の `$"..."` 補間ホール内の呼び出しを落とし、`@"..."` 逐語文字列本体から phantom 参照を漏らしていると報告していた。現時点の `StructuralLineMasker` 実装では、単行 `$"..."`、複数行 `$@"..."` / `@$"..."`、非補間 `@"..."` のいずれも C# 経路で正しく処理されており、補間ホール内の呼び出しは正しい `ContainerName` で捕捉され、複数行逐語本体から phantom 参照は漏れない状態になっているが、issue 原文の再現コードそのものを end-to-end で固定する回帰テストが無く、将来 masker の C# フレームスタック処理を refactor した際に静かに不具合が再発しうる状況だった。`Extract_CsharpInterpolatedAndVerbatimStrings_Issue264_Repro_CapturesHoleCallsAndSuppressesPhantoms` を追加し、issue 原文の fixture そのもの(`$"Hello {Helper.GetName()}"`、`$"Age: {Helper.GetAge()} years"`、`$"Nested {Helper.Format(Helper.GetName())}"`、補間ホール付き複数行 `$@"..."`、素の非補間呼び出し、`PhantomCall()` / `PhantomTable()` / `MoreFake()` を含む複数行 `@"..."` 逐語本体)を `ReferenceExtractor.Extract` に通し、(1) `Helper.GetName` がちょうど 4 回捕捉されること、(2) `Helper.GetAge` と `Helper.Format` がそれぞれ 1 回以上捕捉されること、(3) 捕捉された呼び出しの `ContainerName` がいずれも `Work` であること、(4) 逐語本体に埋め込まれた phantom 識別子がいずれも参照行として現れないこと、の 4 点を同時に固定する。対象: `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。Closes #264。 - **派生クラスにネストした `new interface`(ベースクラスの同名 nested interface を隠蔽する形)の抽出を専用回帰テストで固定化 (#376)** — Issue #376 は、派生クラスにネストした `public new interface INested { ... }` が無言で落ちる、`SymbolExtractor` の interface 正規表現の修飾子リストが `(?:partial|unsafe)` どまりで `new` が含まれていないのが根本原因、という報告だった。#355 の自由順修飾子書き換えでこのリストは既に `(?:partial|unsafe|file|new)` に拡張されており、現在のバイナリでは `Base` と `Derived` の双方で `interface INested` が 2 件抽出されるため、#376 の再現手順ではもう症状を再現できない。本変更はその挙動を専用回帰テスト `Extract_CSharp_NewNestedInterface_MemberHiding` として固定する。Issue 記載の fixture をそのまま流し、両 nested interface(`ContainerName == "Base"` と `ContainerName == "Derived"`)が抽出されることをアサートするので、今後の修飾子リスト編集で #376 のケースが失敗テスト無しに静かに再回帰しないようになる。対象: `tests/CodeIndex.Tests/SymbolExtractorTests.cs`。Closes #376。 - **C# 8 の struct 上 `readonly` インスタンスインデクサが `Item` シンボルとして抽出されることを回帰テストで固定化 (#352)** — `SymbolExtractor` は以前、`public readonly int this[int i] => _arr[i];` や `public readonly string this[string key] { get => key; }` を silent drop していたため、`readonly` インデクサを多用する slice / span / イミュータブル wrapper struct の表面積を `definition` / `symbols` / `outline` / `inspect` が過小にしていた。#355 の書き換えに伴って C# インデクサ行の自由順 modifier スロットが `readonly` に加えて `unsafe` / `extern` / `ref (readonly)` も受理するようになっているが、今回はその挙動を専用回帰テスト `Extract_CSharp_DetectsReadonlyIndexers` で固定する。式本体、ブロック本体、ジェネリック戻り値 (`public readonly T this[int i] { get => _items[i]; }`) の三形態と、非 readonly / readonly メソッドのベースラインまで含めてカバーし、今後の regex リファクタで #352 のケースが再回帰したときに失敗テストを伴わずに壊れないようにする。対象: `tests/CodeIndex.Tests/SymbolExtractorTests.cs`。 diff --git a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs index 878634b201..e55ba6609b 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs @@ -4844,4 +4844,82 @@ public void Extract_SqlCallBacktickIdentifierContainingHash_IsCaptured() Assert.Contains(references, r => r.SymbolName == "proc#1" && r.ReferenceKind == "call" && r.Line == 1); Assert.Contains(references, r => r.SymbolName == "proc#sqlserver" && r.ReferenceKind == "call" && r.Line == 2); } + + [Fact] + public void Extract_SqlCallInsideProcedureBody_AttributesCallerToEnclosingProcedure() + { + // issue #429: CREATE / ALTER PROCEDURE | FUNCTION | TRIGGER symbols used to have null body ranges, + // so FindInnermostContainer could not attribute EXEC / CALL sites inside the body to the enclosing + // procedure. callers / callees / impact consequently returned zero for every SQL procedure even + // though graph_supported reported true. With BodyStyle.SqlProcBody, calls inside the proc body now + // surface with ContainerKind = "function" and ContainerName equal to the owning proc. + // issue #429: CREATE/ALTER PROCEDURE 等は本体範囲が null だったため本体内の EXEC/CALL を外側プロシージャに帰属できず、 + // SQL プロシージャの callers/callees/impact が常に 0 件になっていた。BodyStyle.SqlProcBody により、 + // 本体内の呼び出しが ContainerKind="function"/ContainerName=外側プロシージャ名で参照に載ることを固定する。 + const string content = """ + CREATE PROCEDURE dbo.sp_Outer + AS + BEGIN + EXEC dbo.sp_Inner; + EXEC sp_Inner; + END + GO + + EXEC dbo.sp_Inner; + """; + + var symbols = SymbolExtractor.Extract(1, "sql", content); + var references = ReferenceExtractor.Extract(1, "sql", content, symbols); + + var innerCalls = references.Where(r => r.SymbolName == "sp_Inner" && r.ReferenceKind == "call").ToList(); + Assert.Equal(3, innerCalls.Count); + + // Calls inside the proc body (lines 4 and 5) must attribute to sp_Outer. + // プロシージャ本体内 (4 行目・5 行目) の呼び出しは sp_Outer に帰属する。 + var bodyCalls = innerCalls.Where(r => r.Line == 4 || r.Line == 5).ToList(); + Assert.Equal(2, bodyCalls.Count); + Assert.All(bodyCalls, r => + { + Assert.Equal("function", r.ContainerKind); + Assert.Equal("sp_Outer", r.ContainerName); + }); + + // The post-GO call (line 9) is outside any procedure body and must carry no container. + // GO 以降の呼び出し (9 行目) はどのプロシージャ本体にも属さず、コンテナは null でなければならない。 + var topLevelCall = Assert.Single(innerCalls.Where(r => r.Line == 9)); + Assert.Null(topLevelCall.ContainerKind); + Assert.Null(topLevelCall.ContainerName); + } + + [Fact] + public void Extract_SqlPostgresDollarQuotedBody_AttributesCallerToEnclosingFunction() + { + // PostgreSQL dollar-quoted function bodies also rely on FindSqlProcBodyRange to balance `$$ ... $$` + // so PERFORM / call-like statements inside attach to the enclosing function rather than leaking out. + // PostgreSQL の dollar-quoted 本体でも `$$ ... $$` を FindSqlProcBodyRange が閉じるため、本体内の + // PERFORM / 呼び出しが外側関数に帰属することを固定する。 + const string content = """ + CREATE OR REPLACE FUNCTION public.fn_outer() RETURNS void AS $$ + BEGIN + PERFORM public.fn_inner(); + END; + $$ LANGUAGE plpgsql; + + SELECT public.fn_inner(); + """; + + var symbols = SymbolExtractor.Extract(1, "sql", content); + var references = ReferenceExtractor.Extract(1, "sql", content, symbols); + + var innerCalls = references.Where(r => r.SymbolName == "fn_inner" && r.ReferenceKind == "call").ToList(); + Assert.Equal(2, innerCalls.Count); + + var bodyCall = Assert.Single(innerCalls.Where(r => r.Line == 3)); + Assert.Equal("function", bodyCall.ContainerKind); + Assert.Equal("fn_outer", bodyCall.ContainerName); + + var outsideCall = Assert.Single(innerCalls.Where(r => r.Line == 7)); + Assert.Null(outsideCall.ContainerKind); + Assert.Null(outsideCall.ContainerName); + } } From a6d0c582cb8b30cf6100a67fcf89a7a3a1ba854f Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sun, 19 Apr 2026 05:14:36 +0000 Subject: [PATCH 3/4] Address codex review iteration 1 findings for #429 - Narrow SqlTopLevelDdlStartRegex to proc-like DDL (CREATE/ALTER/DROP PROCEDURE|PROC|FUNCTION|TRIGGER, optional OR REPLACE) so body-internal CREATE TABLE / ALTER TABLE / GRANT / USE no longer prematurely close the proc body. - Thread inBlockComment state across lines in MaskSqlLineForBodyScan via ref bool, and evaluate GO / sibling-DDL terminators against the masked text, so a multi-line /* ... */ comment containing a bare GO or fake CREATE PROCEDURE header does not close the body. - Qualify SQL ReferenceExtractor regression assertions to dbo.sp_Outer / public.fn_outer to match the persisted SymbolRecord.Name shape. - Add SymbolExtractor regression tests for body-internal CREATE TABLE and multi-line /* ... */ block comment containing GO / fake CREATE PROCEDURE so the proc body range stays correct. - Update CHANGELOG (English and Japanese) #429 entry. Closes #429 Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 4 +- src/CodeIndex/Indexer/SymbolExtractor.cs | 71 +++++++++++++++---- .../ReferenceExtractorTests.cs | 12 +++- tests/CodeIndex.Tests/SymbolExtractorTests.cs | 67 +++++++++++++++++ 4 files changed, 138 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 801cc7035c..821583eddb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **T-SQL / SQL Server DDL kinds now surface as symbols (#230)** — The `SymbolExtractor` SQL row previously only captured `CREATE TABLE` / `(MATERIALIZED) VIEW` / `PROCEDURE` / `FUNCTION` / `TRIGGER` / `TYPE` / `SCHEMA` / `SEQUENCE` / `DOMAIN` / `EXTENSION` / `INDEX` and `ALTER TABLE`, so on T-SQL / SQL Server migration scripts the short-form `CREATE PROC`, T-SQL `CREATE OR ALTER` (SQL Server 2016+), `CREATE SYNONYM`, `CREATE DATABASE` / `LOGIN` / `USER` / `ROLE` / `CERTIFICATE`, `CREATE PARTITION FUNCTION` / `PARTITION SCHEME`, `CREATE FULLTEXT CATALOG`, and every non-`TABLE` `ALTER` form (procedures, views, sequences, synonyms, principals, etc.) were silently dropped from the index — making `symbols` / `definition` / `outline` / `inspect` look near-empty on enterprise .NET backends whose primary surface is a stored-procedure or schema-migration repo. The TABLE/VIEW and PROCEDURE/PROC/FUNCTION/TRIGGER rows now accept `CREATE OR (REPLACE|ALTER)`, the procedure row accepts the `PROC` synonym, dedicated rows were added for `CREATE SYNONYM` (class), `CREATE DATABASE|LOGIN|USER|ROLE|CERTIFICATE` (class), `CREATE PARTITION FUNCTION` (function) / `CREATE PARTITION SCHEME` (class), `CREATE FULLTEXT CATALOG` (class), and the `ALTER` side is split into kind-consistent rows so procedure-like `ALTER` (`PROCEDURE` / `PROC` / `FUNCTION` / `TRIGGER` / `PARTITION FUNCTION`) emits kind `function`, `ALTER SCHEMA` emits kind `namespace`, `ALTER EXTENSION` emits kind `import`, and the remaining object kinds (`TABLE`, `(MATERIALIZED) VIEW`, `SEQUENCE`, `SYNONYM`, `LOGIN`, `USER`, `ROLE`, `DATABASE`, `CERTIFICATE`, `INDEX`, `TYPE`, `DOMAIN`, `PARTITION SCHEME`, `FULLTEXT CATALOG`) emit kind `class`. Splitting the `ALTER` row by kind keeps the same object's `CREATE` and `ALTER` symbols under the same `kind` so `symbols --kind function` / `definition` / `inspect` do not duplicate across kinds. The `ALTER` side also now picks up `CERTIFICATE`, `DOMAIN`, and `EXTENSION` that were missing from the original row so every non-`TABLE` `ALTER` form is actually captured rather than just claimed. Every SQL row now shares an identifier shape that accepts PG double-quoted (`"name"`), T-SQL bracketed (`[name]`), or bare (`\w+`) tokens with optional dotted qualifiers such as `[dbo].[sp_X]`, `"schema"."name"`, and `schema.name`. The existing `CREATE INDEX ON tbl` / anonymous-name guard (`(?!ON\b)`), the `CREATE TYPE ... AS ENUM` Postgres branch, and the `CREATE SCHEMA AUTHORIZATION` branch are preserved. Body-less `namespace`-kind symbols from SQL (`CREATE SCHEMA sales;`, `ALTER SCHEMA sales TRANSFER ...;`) would previously trip the `IsFileScopedNamespace` check that wraps every subsequent top-level symbol under a C# `namespace X;` file-scoped declaration — so `ALTER FUNCTION dbo.fn_Total`, `ALTER PARTITION FUNCTION pf_OrdersByYear`, and every later declaration in the same file were emitted with `container_kind='namespace'` / `container_name='sales'`, breaking `definition` / `inspect` / family grouping on real migration scripts. The helper now additionally requires the signature to start with the `namespace` keyword, so SQL schema rows no longer act as file-scoped containers; SQL schema scoping is still expressed through qualified names (`sales.Money`), matching how the rest of cdidx reads SQL. Added a dedicated `Extract_SQL_DetectsTSqlDdlKinds` regression test that locks all thirteen new T-SQL `CREATE` shapes and every new `ALTER` kind contract (`ALTER PROCEDURE` / `ALTER FUNCTION` / `ALTER PARTITION FUNCTION` → `function`; `ALTER SCHEMA` → `namespace`; `ALTER EXTENSION` → `import`; `ALTER CERTIFICATE` / `ALTER DOMAIN` → `class`) including the `CREATE PROC` + `ALTER PROCEDURE` pair that must emit two `dbo.sp_DailyReport` rows both under kind `function`, and asserts that no later top-level SQL symbol is wrapped under `namespace=sales` so the file-scoped-namespace container pollution cannot regress. `GO` batch boundary awareness and `BEGIN ... END` structural nesting are deliberately out of scope here and remain filed in #230 as stretch items. Affected: `src/CodeIndex/Indexer/SymbolExtractor.cs`, `tests/CodeIndex.Tests/SymbolExtractorTests.cs`. Closes #230. #### Fixed -- **SQL `CREATE` / `ALTER PROCEDURE` / `PROC` / `FUNCTION` / `TRIGGER` bodies now carry body ranges, so calls inside the body attribute to the enclosing proc (#429)** — `SymbolExtractor` emitted every `CREATE` / `ALTER PROCEDURE` / `PROC` / `FUNCTION` / `TRIGGER` with `BodyStyle.None`, leaving `BodyStartLine` / `BodyEndLine` null. `ReferenceExtractor.ResolveContainerForCall` → `FindInnermostContainer` relies on those ranges to attribute an in-body call to the enclosing procedure, so every call inside a procedure body fell through to the file-level fallback and `symbol_references.container_kind` / `container_name` were written as null. As a result `cdidx callers` / `callees` / `impact` returned zero for every SQL procedure even though `graph_supported` reported true. The procedure/function/trigger rows now emit a new `BodyStyle.SqlProcBody`, and the new `FindSqlProcBodyRange(lines, startIndex)` helper scans forward for (1) a balanced PostgreSQL dollar-quoted body (`$$ ... $$` / `$tag$ ... $tag$`) when the next non-blank payload begins with a dollar tag, (2) a T-SQL `GO` batch separator at line start (trailing `;` / `--` comment tolerated), (3) a new top-level DDL start (`CREATE` / `ALTER` / `DROP` / `GRANT` / `REVOKE` / `DENY` / `USE`), or (4) end-of-file. Before scanning each line, `MaskSqlLineForBodyScan` strips `--` line comments, `/* ... */` block comments, and `'..'` / `".."` string literals (preserving column width so later column alignment stays correct, and preserving `[name]` / `` `name` `` quoted identifiers because SQL quoted identifiers are not string literals) so a `GO` / `$$` / `CREATE` token embedded in a string or comment does not close the body early. `ALTER PARTITION FUNCTION` is intentionally split into its own pattern and keeps `BodyStyle.None` because it modifies a partition boundary value rather than executable code, so its surface should remain body-less. Added eight `SymbolExtractor` regression tests covering T-SQL `BEGIN ... END` bodies terminated by `GO`, Postgres dollar-quoted function bodies, single-line `AS BEGIN ... END;` bodies, DDL-start close without an intervening `GO`, string/comment masking (a `GO` / `$$` / new `CREATE` inside a string literal or `--` comment does not close the body), `ALTER PROCEDURE` body capture, `ALTER PARTITION FUNCTION` staying body-less, and a regression guard that a second procedure is not swallowed into the previous procedure's body range. Added two `ReferenceExtractor` regression tests: `Extract_SqlCallInsideProcedureBody_AttributesCallerToEnclosingProcedure` pins that an `EXEC` inside a T-SQL `CREATE PROCEDURE dbo.sp_Outer` body resolves to `ContainerKind=function` / `ContainerName=sp_Outer` while a post-`GO` call has null container, and `Extract_SqlPostgresDollarQuotedBody_AttributesCallerToEnclosingFunction` pins the same for a `PERFORM public.fn_inner()` inside a Postgres `$$ ... $$`-quoted `fn_outer` body. Affected: `src/CodeIndex/Indexer/SymbolExtractor.cs`, `tests/CodeIndex.Tests/SymbolExtractorTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. Closes #429. +- **SQL `CREATE` / `ALTER PROCEDURE` / `PROC` / `FUNCTION` / `TRIGGER` bodies now carry body ranges, so calls inside the body attribute to the enclosing proc (#429)** — `SymbolExtractor` emitted every `CREATE` / `ALTER PROCEDURE` / `PROC` / `FUNCTION` / `TRIGGER` with `BodyStyle.None`, leaving `BodyStartLine` / `BodyEndLine` null. `ReferenceExtractor.ResolveContainerForCall` → `FindInnermostContainer` relies on those ranges to attribute an in-body call to the enclosing procedure, so every call inside a procedure body fell through to the file-level fallback and `symbol_references.container_kind` / `container_name` were written as null. As a result `cdidx callers` / `callees` / `impact` returned zero for every SQL procedure even though `graph_supported` reported true. The procedure/function/trigger rows now emit a new `BodyStyle.SqlProcBody`, and the new `FindSqlProcBodyRange(lines, startIndex)` helper scans forward for (1) a balanced PostgreSQL dollar-quoted body (`$$ ... $$` / `$tag$ ... $tag$`) when the next non-blank payload begins with a dollar tag, (2) a T-SQL `GO` batch separator at line start (trailing `;` / `--` comment tolerated), (3) a sibling proc-like DDL start (`CREATE` / `ALTER` / `DROP` + optional `OR REPLACE` + `PROCEDURE` / `PROC` / `FUNCTION` / `TRIGGER`) — narrowed from the original `CREATE|ALTER|DROP|GRANT|REVOKE|DENY|USE` catch-all so body-internal `CREATE TABLE #tmp` / `ALTER TABLE` / `GRANT` / `USE` do not prematurely close the proc body and re-introduce issue #429, or (4) end-of-file. Before scanning each line, `MaskSqlLineForBodyScan` strips `--` line comments, `/* ... */` block comments, and `'..'` / `".."` string literals (preserving column width so later column alignment stays correct, and preserving `[name]` / `` `name` `` quoted identifiers because SQL quoted identifiers are not string literals) so a `GO` / `$$` / `CREATE PROCEDURE` token embedded in a string or comment does not close the body early. The mask also threads an `inBlockComment` state across lines via `ref bool`, so a multi-line `/* ... */` block — even one holding a bare `GO` or a `CREATE PROCEDURE` header on a comment-interior line — stays fully masked until `*/` is reached instead of bailing out on the first following line. Both the `GO` terminator and the sibling-proc-DDL terminator are now evaluated against the masked text for the same reason. `ALTER PARTITION FUNCTION` is intentionally split into its own pattern and keeps `BodyStyle.None` because it modifies a partition boundary value rather than executable code, so its surface should remain body-less. Added ten `SymbolExtractor` regression tests covering T-SQL `BEGIN ... END` bodies terminated by `GO`, Postgres dollar-quoted function bodies, single-line `AS BEGIN ... END;` bodies, DDL-start close without an intervening `GO`, string / comment masking (a `GO` / `$$` / new proc-like `CREATE` inside a string literal or `--` comment does not close the body), `ALTER PROCEDURE` body capture, `ALTER PARTITION FUNCTION` staying body-less, a regression guard that a second procedure is not swallowed into the previous procedure's body range, body-internal `CREATE TABLE #tmp` / `ALTER TABLE` not closing the body, and a multi-line `/* ... */` block containing bare `GO` / fake `CREATE PROCEDURE` headers not closing the body. Added two `ReferenceExtractor` regression tests: `Extract_SqlCallInsideProcedureBody_AttributesCallerToEnclosingProcedure` pins that an `EXEC` inside a T-SQL `CREATE PROCEDURE dbo.sp_Outer` body resolves to `ContainerKind=function` / `ContainerName=dbo.sp_Outer` (schema-qualified, matching the persisted `SymbolRecord.Name`) while a post-`GO` call has null container, and `Extract_SqlPostgresDollarQuotedBody_AttributesCallerToEnclosingFunction` pins the same for a `PERFORM public.fn_inner()` inside a Postgres `$$ ... $$`-quoted `public.fn_outer` body. Affected: `src/CodeIndex/Indexer/SymbolExtractor.cs`, `tests/CodeIndex.Tests/SymbolExtractorTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. Closes #429. - **Regression lock for C# interpolated and verbatim string reference extraction (#264)** — The original issue report alleged that `ReferenceExtractor` hid call-site references inside C# `$"..."` interpolation holes and leaked phantom references from `@"..."` verbatim string bodies. On the current `StructuralLineMasker` implementation the C# paths already handle single-line `$"..."`, multi-line `$@"..."` / `@$"..."`, and non-interpolated `@"..."` correctly — interpolation-hole call sites are captured with correct `ContainerName`, and multi-line verbatim bodies never leak phantom edges — but no regression test pinned that behavior end-to-end for the exact fixture in the issue report, so any future refactor of the masker's C# frame-stack handling could silently reintroduce the bug. Added `Extract_CsharpInterpolatedAndVerbatimStrings_Issue264_Repro_CapturesHoleCallsAndSuppressesPhantoms` which feeds the issue's exact fixture (`$"Hello {Helper.GetName()}"`, `$"Age: {Helper.GetAge()} years"`, `$"Nested {Helper.Format(Helper.GetName())}"`, a multi-line `$@"..."` with an interpolation hole, a plain non-interpolated call site, and a multi-line `@"..."` verbatim body containing `PhantomCall()` / `PhantomTable()` / `MoreFake()`) through `ReferenceExtractor.Extract` and pins four invariants at once: `Helper.GetName` is captured exactly four times, `Helper.GetAge` and `Helper.Format` are each captured at least once, every captured call's `ContainerName` resolves to `Work`, and no reference row ever surfaces for the phantom identifiers embedded in the verbatim body. Affected: `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. Closes #264. - **C# nested `new interface` (member-hiding a base-class nested interface) is now pinned by a dedicated regression (#376)** — Issue #376 reported that `public new interface INested { ... }` nested inside a derived class was silently dropped because the C# interface regex modifier list was `(?:partial|unsafe)` and did not include `new`. The free-modifier-order rewrite for #355 already widened the list to `(?:partial|unsafe|file|new)`, so the current binary already emits two `interface INested` symbols — one nested in `Base`, one nested in `Derived` — and the #376 repro is no longer reproducible. This change locks that behavior in with a dedicated `Extract_CSharp_NewNestedInterface_MemberHiding` regression that runs the exact fixture from the issue and asserts both nested interfaces are extracted (`ContainerName == "Base"` and `ContainerName == "Derived"`), so a future modifier-list edit cannot regress the #376 case without a failing test. Affected: `tests/CodeIndex.Tests/SymbolExtractorTests.cs`. Closes #376. - **C# 8 `readonly` instance indexers on structs are now captured as `Item` symbols (#352)** — `SymbolExtractor` previously dropped `public readonly int this[int i] => _arr[i];` and `public readonly string this[string key] { get => key; }` silently, so `definition`, `symbols`, `outline`, and `inspect` undercounted `readonly`-heavy slice / span / immutable-wrapper struct surfaces. The indexer row already accepts `readonly` in the free-order modifier slot alongside `unsafe` / `extern` / `ref (readonly)` following the #355 rewrite, and this change locks that behavior in with a dedicated `Extract_CSharp_DetectsReadonlyIndexers` regression covering expression-body, block-body, and generic-return (`public readonly T this[int i] { get => _items[i]; }`) shapes plus non-readonly / readonly-method baselines so a future regex refactor cannot regress the #352 case without a failing test. Affected: `tests/CodeIndex.Tests/SymbolExtractorTests.cs`. @@ -717,7 +717,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **T-SQL / SQL Server の DDL 種別をシンボルとして拾えるようにした (#230)** — `SymbolExtractor` の SQL 行は従来 `CREATE TABLE` / `(MATERIALIZED) VIEW` / `PROCEDURE` / `FUNCTION` / `TRIGGER` / `TYPE` / `SCHEMA` / `SEQUENCE` / `DOMAIN` / `EXTENSION` / `INDEX` と `ALTER TABLE` しか捕捉しておらず、T-SQL / SQL Server のマイグレーション系スクリプトでは短縮形 `CREATE PROC`、T-SQL の `CREATE OR ALTER`(SQL Server 2016+)、`CREATE SYNONYM`、`CREATE DATABASE` / `LOGIN` / `USER` / `ROLE` / `CERTIFICATE`、`CREATE PARTITION FUNCTION` / `PARTITION SCHEME`、`CREATE FULLTEXT CATALOG`、および `TABLE` 以外の `ALTER`(プロシージャ、ビュー、シーケンス、シノニム、プリンシパル等)がすべてインデックスから黙って落ちており、ストアドプロシージャやスキーマ・マイグレーション中心のエンタープライズ .NET バックエンドでは `symbols` / `definition` / `outline` / `inspect` がほぼ空に見えてしまっていた。TABLE/VIEW 行と PROCEDURE/PROC/FUNCTION/TRIGGER 行を `CREATE OR (REPLACE|ALTER)` 対応に拡張し、プロシージャ行に `PROC` 短縮形を追加した。`CREATE SYNONYM`(class)、`CREATE DATABASE|LOGIN|USER|ROLE|CERTIFICATE`(class)、`CREATE PARTITION FUNCTION`(function)/ `CREATE PARTITION SCHEME`(class)、`CREATE FULLTEXT CATALOG`(class)の専用行を追加し、`ALTER` 側も kind を揃える形で分割した — プロシージャ類の `ALTER`(`PROCEDURE` / `PROC` / `FUNCTION` / `TRIGGER` / `PARTITION FUNCTION`)は `function`、`ALTER SCHEMA` は `namespace`、`ALTER EXTENSION` は `import`、残りのオブジェクト種別(`TABLE`、`(MATERIALIZED) VIEW`、`SEQUENCE`、`SYNONYM`、`LOGIN`、`USER`、`ROLE`、`DATABASE`、`CERTIFICATE`、`INDEX`、`TYPE`、`DOMAIN`、`PARTITION SCHEME`、`FULLTEXT CATALOG`)は `class` を返す。`ALTER` を kind ごとに分けることで、同一オブジェクトの `CREATE` と `ALTER` が同じ `kind` に揃い、`symbols --kind function` / `definition` / `inspect` が種別またぎで重複しない。`ALTER` 側には元の行で漏れていた `CERTIFICATE` / `DOMAIN` / `EXTENSION` も追加し、`TABLE` 以外の `ALTER` を本当に網羅する形にした。SQL の全行が共通の識別子形状を共有し、PG の `"name"`、T-SQL の `[name]`、裸 `\w+` トークンをドット修飾できる形(`[dbo].[sp_X]` / `"schema"."name"` / `schema.name`)を全行で受け入れる。既存の `CREATE INDEX ON tbl` 無名ガード(`(?!ON\b)`)、Postgres の `CREATE TYPE ... AS ENUM` 分岐、`CREATE SCHEMA AUTHORIZATION` 分岐は維持している。さらに、SQL の `CREATE SCHEMA sales;` / `ALTER SCHEMA sales TRANSFER ...;` のような body 無しの `namespace` kind 行が、C# の `namespace X;` file-scoped namespace を想定した `IsFileScopedNamespace` 判定を発火させ、以降のトップレベルシンボル全部を `container_kind='namespace'` / `container_name='sales'` で包んでしまう不具合を修正した(`ALTER FUNCTION dbo.fn_Total` や `ALTER PARTITION FUNCTION pf_OrdersByYear` の container が汚染され、`definition` / `inspect` / family grouping が崩れていた)。`IsFileScopedNamespace` は signature が `namespace` キーワードで始まる行だけを対象とするよう追加ガードを入れ、SQL の schema は file-scoped container にならないようにした(SQL では schema スコープは `sales.Money` のような修飾名で表現する方針を維持)。`Extract_SQL_DetectsTSqlDdlKinds` 回帰テストを追加し、新規 13 形の `CREATE` に加え、新しい `ALTER` の kind 契約(`ALTER PROCEDURE` / `ALTER FUNCTION` / `ALTER PARTITION FUNCTION` → `function`、`ALTER SCHEMA` → `namespace`、`ALTER EXTENSION` → `import`、`ALTER CERTIFICATE` / `ALTER DOMAIN` → `class`)もすべて固定し、`CREATE PROC` と `ALTER PROCEDURE` のペアで `dbo.sp_DailyReport` が同じ `function` kind で 2 件出ることも含めて押さえ、後続のトップレベルシンボルが `namespace=sales` 配下に吸い込まれないことも固定した。`GO` バッチ境界や `BEGIN ... END` の構造ネストは本 issue のストレッチ項目として意図的に対象外。対象: `src/CodeIndex/Indexer/SymbolExtractor.cs`、`tests/CodeIndex.Tests/SymbolExtractorTests.cs`。Closes #230。 #### 修正 -- **SQL `CREATE` / `ALTER PROCEDURE` / `PROC` / `FUNCTION` / `TRIGGER` の本体に body range を付与し、body 内の呼び出しが外側プロシージャにひも付くよう修正 (#429)** — `SymbolExtractor` はこれまで `CREATE` / `ALTER PROCEDURE` / `PROC` / `FUNCTION` / `TRIGGER` をすべて `BodyStyle.None` で発行していたため、`BodyStartLine` / `BodyEndLine` が null のまま保存されていた。`ReferenceExtractor.ResolveContainerForCall` → `FindInnermostContainer` は body 内の呼び出しを外側プロシージャに結びつけるためにこの範囲を使うので、プロシージャ本体の呼び出しはすべてファイルレベルのフォールバックへ落ち、`symbol_references.container_kind` / `container_name` が null で書かれていた。結果として `cdidx callers` / `callees` / `impact` は `graph_supported=true` のまま、どの SQL プロシージャに対しても 0 件を返していた。プロシージャ/ファンクション/トリガー行が新設の `BodyStyle.SqlProcBody` を発行するようにし、新ヘルパー `FindSqlProcBodyRange(lines, startIndex)` が前方走査して、(1) 次の非空行が `$` タグで始まれば PostgreSQL の `$$ ... $$` / `$tag$ ... $tag$` を対応つきで閉じた位置、(2) 行頭の T-SQL `GO` バッチ区切り(末尾 `;` や `--` コメントは許容)、(3) 新しい top-level DDL の開始 (`CREATE` / `ALTER` / `DROP` / `GRANT` / `REVOKE` / `DENY` / `USE`)、(4) ファイル末尾、のいずれかで body を閉じる。走査前には `MaskSqlLineForBodyScan` が各行を前処理し、`--` 行コメント、`/* ... */` ブロックコメント、`'..'` / `".."` 文字列リテラルを列幅を保ったままマスクして(`[name]` / `` `name` `` の引用識別子は SQL 上文字列ではないので保持する)、文字列やコメント内部の `GO` / `$$` / `CREATE` トークンで body を早期終了しないようにしている。`ALTER PARTITION FUNCTION` は実行コードではなく partition 境界値を書き換えるだけの宣言なので、本来 body を持たない surface として扱う方針を維持するため、独立したパターンに分離して `BodyStyle.None` のまま残している。回帰テストとして `SymbolExtractor` 側に 8 本追加した: `GO` で閉じる T-SQL `BEGIN ... END`、Postgres の `$$ ... $$` 関数本体、単一行 `AS BEGIN ... END;`、`GO` を挟まず新 DDL で閉じるケース、文字列/コメント内に埋め込まれた `GO` / `$$` / 新 `CREATE` で body を閉じないマスキング、`ALTER PROCEDURE` 本体の捕捉、`ALTER PARTITION FUNCTION` が body を持たないこと、次のプロシージャ宣言が前プロシージャの body 範囲に吸収されないことを固定する regression guard の 8 本。`ReferenceExtractor` 側に 2 本追加した: `Extract_SqlCallInsideProcedureBody_AttributesCallerToEnclosingProcedure` は T-SQL `CREATE PROCEDURE dbo.sp_Outer` 本体の `EXEC` が `ContainerKind=function` / `ContainerName=sp_Outer` に解決され、`GO` 以降の呼び出しは container が null のまま落ちることを固定し、`Extract_SqlPostgresDollarQuotedBody_AttributesCallerToEnclosingFunction` は Postgres の `$$ ... $$` 関数 `fn_outer` 本体内の `PERFORM public.fn_inner()` について同じ不変量を固定する。対象: `src/CodeIndex/Indexer/SymbolExtractor.cs`、`tests/CodeIndex.Tests/SymbolExtractorTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。Closes #429。 +- **SQL `CREATE` / `ALTER PROCEDURE` / `PROC` / `FUNCTION` / `TRIGGER` の本体に body range を付与し、body 内の呼び出しが外側プロシージャにひも付くよう修正 (#429)** — `SymbolExtractor` はこれまで `CREATE` / `ALTER PROCEDURE` / `PROC` / `FUNCTION` / `TRIGGER` をすべて `BodyStyle.None` で発行していたため、`BodyStartLine` / `BodyEndLine` が null のまま保存されていた。`ReferenceExtractor.ResolveContainerForCall` → `FindInnermostContainer` は body 内の呼び出しを外側プロシージャに結びつけるためにこの範囲を使うので、プロシージャ本体の呼び出しはすべてファイルレベルのフォールバックへ落ち、`symbol_references.container_kind` / `container_name` が null で書かれていた。結果として `cdidx callers` / `callees` / `impact` は `graph_supported=true` のまま、どの SQL プロシージャに対しても 0 件を返していた。プロシージャ/ファンクション/トリガー行が新設の `BodyStyle.SqlProcBody` を発行するようにし、新ヘルパー `FindSqlProcBodyRange(lines, startIndex)` が前方走査して、(1) 次の非空ペイロードが `$` タグで始まれば PostgreSQL の `$$ ... $$` / `$tag$ ... $tag$` を対応つきで閉じた位置、(2) 行頭の T-SQL `GO` バッチ区切り(末尾 `;` や `--` コメントは許容)、(3) sibling proc-like DDL 開始 (`CREATE` / `ALTER` / `DROP` + 任意の `OR REPLACE` + `PROCEDURE` / `PROC` / `FUNCTION` / `TRIGGER`) — 当初は `CREATE|ALTER|DROP|GRANT|REVOKE|DENY|USE` を一括 catch-all していたが、それだと body 内の `CREATE TABLE #tmp` / `ALTER TABLE` / `GRANT` / `USE` が body を早期終了させて #429 を再発させてしまうため、proc-like DDL に限定している、(4) ファイル末尾、のいずれかで body を閉じる。走査前には `MaskSqlLineForBodyScan` が各行を前処理し、`--` 行コメント、`/* ... */` ブロックコメント、`'..'` / `".."` 文字列リテラルを列幅を保ったままマスクして(`[name]` / `` `name` `` の引用識別子は SQL 上文字列ではないので保持する)、文字列やコメント内部の `GO` / `$$` / `CREATE PROCEDURE` トークンで body を早期終了しないようにしている。さらに複数行 `/* ... */` ブロックコメントに対応するため、マスクは `ref bool inBlockComment` で行をまたいだ block-comment 状態を伝播する。`*/` が現れるまではその後の行全体がマスクされ続けるので、コメント内部の `GO` や `CREATE PROCEDURE` 行で最初のコメント直後の行でフェイルクローズしない。`GO` ターミネータと sibling proc-like DDL ターミネータはいずれもマスク後のテキストに対して判定する。`ALTER PARTITION FUNCTION` は実行コードではなく partition 境界値を書き換えるだけの宣言なので、本来 body を持たない surface として扱う方針を維持するため、独立したパターンに分離して `BodyStyle.None` のまま残している。回帰テストとして `SymbolExtractor` 側に 10 本追加した: `GO` で閉じる T-SQL `BEGIN ... END`、Postgres の `$$ ... $$` 関数本体、単一行 `AS BEGIN ... END;`、`GO` を挟まず新 DDL で閉じるケース、文字列/コメント内に埋め込まれた `GO` / `$$` / 新 `CREATE` で body を閉じないマスキング、`ALTER PROCEDURE` 本体の捕捉、`ALTER PARTITION FUNCTION` が body を持たないこと、次のプロシージャ宣言が前プロシージャの body 範囲に吸収されないこと、body 内の `CREATE TABLE #tmp` / `ALTER TABLE` が body を閉じないこと、そして複数行 `/* ... */` ブロックコメント内に含まれる素の `GO` / 偽装 `CREATE PROCEDURE` ヘッダが body を閉じないことの 10 本。`ReferenceExtractor` 側に 2 本追加した: `Extract_SqlCallInsideProcedureBody_AttributesCallerToEnclosingProcedure` は T-SQL `CREATE PROCEDURE dbo.sp_Outer` 本体の `EXEC` が `ContainerKind=function` / `ContainerName=dbo.sp_Outer`(永続化される `SymbolRecord.Name` に揃えた schema 修飾名)に解決され、`GO` 以降の呼び出しは container が null のまま落ちることを固定し、`Extract_SqlPostgresDollarQuotedBody_AttributesCallerToEnclosingFunction` は Postgres の `$$ ... $$` 関数 `public.fn_outer` 本体内の `PERFORM public.fn_inner()` について同じ不変量を `ContainerName=public.fn_outer` で固定する。対象: `src/CodeIndex/Indexer/SymbolExtractor.cs`、`tests/CodeIndex.Tests/SymbolExtractorTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。Closes #429。 - **C# 補間文字列 / 逐語文字列に対する参照抽出の回帰テストを追加 (#264)** — issue 原文は `ReferenceExtractor` が C# の `$"..."` 補間ホール内の呼び出しを落とし、`@"..."` 逐語文字列本体から phantom 参照を漏らしていると報告していた。現時点の `StructuralLineMasker` 実装では、単行 `$"..."`、複数行 `$@"..."` / `@$"..."`、非補間 `@"..."` のいずれも C# 経路で正しく処理されており、補間ホール内の呼び出しは正しい `ContainerName` で捕捉され、複数行逐語本体から phantom 参照は漏れない状態になっているが、issue 原文の再現コードそのものを end-to-end で固定する回帰テストが無く、将来 masker の C# フレームスタック処理を refactor した際に静かに不具合が再発しうる状況だった。`Extract_CsharpInterpolatedAndVerbatimStrings_Issue264_Repro_CapturesHoleCallsAndSuppressesPhantoms` を追加し、issue 原文の fixture そのもの(`$"Hello {Helper.GetName()}"`、`$"Age: {Helper.GetAge()} years"`、`$"Nested {Helper.Format(Helper.GetName())}"`、補間ホール付き複数行 `$@"..."`、素の非補間呼び出し、`PhantomCall()` / `PhantomTable()` / `MoreFake()` を含む複数行 `@"..."` 逐語本体)を `ReferenceExtractor.Extract` に通し、(1) `Helper.GetName` がちょうど 4 回捕捉されること、(2) `Helper.GetAge` と `Helper.Format` がそれぞれ 1 回以上捕捉されること、(3) 捕捉された呼び出しの `ContainerName` がいずれも `Work` であること、(4) 逐語本体に埋め込まれた phantom 識別子がいずれも参照行として現れないこと、の 4 点を同時に固定する。対象: `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。Closes #264。 - **派生クラスにネストした `new interface`(ベースクラスの同名 nested interface を隠蔽する形)の抽出を専用回帰テストで固定化 (#376)** — Issue #376 は、派生クラスにネストした `public new interface INested { ... }` が無言で落ちる、`SymbolExtractor` の interface 正規表現の修飾子リストが `(?:partial|unsafe)` どまりで `new` が含まれていないのが根本原因、という報告だった。#355 の自由順修飾子書き換えでこのリストは既に `(?:partial|unsafe|file|new)` に拡張されており、現在のバイナリでは `Base` と `Derived` の双方で `interface INested` が 2 件抽出されるため、#376 の再現手順ではもう症状を再現できない。本変更はその挙動を専用回帰テスト `Extract_CSharp_NewNestedInterface_MemberHiding` として固定する。Issue 記載の fixture をそのまま流し、両 nested interface(`ContainerName == "Base"` と `ContainerName == "Derived"`)が抽出されることをアサートするので、今後の修飾子リスト編集で #376 のケースが失敗テスト無しに静かに再回帰しないようになる。対象: `tests/CodeIndex.Tests/SymbolExtractorTests.cs`。Closes #376。 - **C# 8 の struct 上 `readonly` インスタンスインデクサが `Item` シンボルとして抽出されることを回帰テストで固定化 (#352)** — `SymbolExtractor` は以前、`public readonly int this[int i] => _arr[i];` や `public readonly string this[string key] { get => key; }` を silent drop していたため、`readonly` インデクサを多用する slice / span / イミュータブル wrapper struct の表面積を `definition` / `symbols` / `outline` / `inspect` が過小にしていた。#355 の書き換えに伴って C# インデクサ行の自由順 modifier スロットが `readonly` に加えて `unsafe` / `extern` / `ref (readonly)` も受理するようになっているが、今回はその挙動を専用回帰テスト `Extract_CSharp_DetectsReadonlyIndexers` で固定する。式本体、ブロック本体、ジェネリック戻り値 (`public readonly T this[int i] { get => _items[i]; }`) の三形態と、非 readonly / readonly メソッドのベースラインまで含めてカバーし、今後の regex リファクタで #352 のケースが再回帰したときに失敗テストを伴わずに壊れないようにする。対象: `tests/CodeIndex.Tests/SymbolExtractorTests.cs`。 diff --git a/src/CodeIndex/Indexer/SymbolExtractor.cs b/src/CodeIndex/Indexer/SymbolExtractor.cs index eaa9a055d4..a6e65f392e 100644 --- a/src/CodeIndex/Indexer/SymbolExtractor.cs +++ b/src/CodeIndex/Indexer/SymbolExtractor.cs @@ -7025,8 +7025,15 @@ private static (int EndLine, int? BodyStartLine, int? BodyEndLine) FindVisualBas private static readonly Regex SqlGoSeparatorRegex = new( @"^\s*GO\s*(?:;[\s;]*)?(?:--.*)?$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + // Only close a SQL proc body when the next top-level statement looks like another proc-like + // header (`CREATE|ALTER|DROP PROCEDURE|PROC|FUNCTION|TRIGGER`, optionally with `OR REPLACE`). + // Body-internal `CREATE TABLE` / `ALTER TABLE` / `GRANT` / `USE` etc. must not prematurely + // close the enclosing procedure body. See issue #429. + // 次のトップレベル文が別の proc 系ヘッダ(`CREATE|ALTER|DROP` + `PROCEDURE|PROC|FUNCTION|TRIGGER`、 + // `OR REPLACE` 付きも許容)だった場合のみ、SQL の proc 本体を閉じる。本体内の `CREATE TABLE` / + // `ALTER TABLE` / `GRANT` / `USE` などで先走って閉じないこと。issue #429 参照。 private static readonly Regex SqlTopLevelDdlStartRegex = new( - @"^\s*(?:CREATE|ALTER|DROP|GRANT|REVOKE|DENY|USE)\b", + @"^\s*(?:CREATE|ALTER|DROP)\s+(?:OR\s+REPLACE\s+)?(?:PROCEDURE|PROC|FUNCTION|TRIGGER)\b", RegexOptions.Compiled | RegexOptions.IgnoreCase); // Dollar-quoted body tags: `$$` or `$tagname$` (PostgreSQL). Tag must be empty or an identifier. // Dollar-quoted の本体タグ: `$$` または `$タグ名$`(PostgreSQL)。タグは空か識別子のみ。 @@ -7055,6 +7062,7 @@ private static (int EndLine, int? BodyStartLine, int? BodyEndLine) FindSqlProcBo int bodyStartLine = startIndex + 1; int endLine = startIndex + 1; string? openDollarTag = null; + bool inBlockComment = false; for (int i = startIndex; i < lines.Length; i++) { @@ -7074,7 +7082,7 @@ private static (int EndLine, int? BodyStartLine, int? BodyEndLine) FindSqlProcBo continue; } - var masked = MaskSqlLineForBodyScan(raw); + var masked = MaskSqlLineForBodyScan(raw, ref inBlockComment); // Detect any unpaired dollar-quote opening on this line. Paired openings on the same // line (e.g. `AS $$ SELECT 1 $$`) are consumed without opening cross-line state. @@ -7107,7 +7115,12 @@ private static (int EndLine, int? BodyStartLine, int? BodyEndLine) FindSqlProcBo if (i > startIndex) { - if (SqlGoSeparatorRegex.IsMatch(raw)) + // Use `masked` for GO detection too so a bare `GO` appearing inside a multi-line + // block comment does not prematurely close the body (the mask blanks out + // comment-interior content). See issue #429 follow-up. + // `GO` 判定にも `masked` を使い、複数行ブロックコメント内の `GO` 単独行で本体を + // 早期終了させない(マスクでコメント内部は空白化される)。issue #429 追補参照。 + if (SqlGoSeparatorRegex.IsMatch(masked)) { // `GO` is a T-SQL batch separator that is not part of the body; close at the // previous line so the `GO` line itself is outside the procedure. @@ -7133,15 +7146,21 @@ private static (int EndLine, int? BodyStartLine, int? BodyEndLine) FindSqlProcBo } /// - /// Strip SQL line comments (`--`), block comments (`/* ... */`), and string literals - /// (`'...'` / `"..."`) from a single line so body-terminator checks do not trip on text - /// inside comments or strings. Bracket identifiers (`[name]`) and backtick identifiers + /// Strip SQL line comments (`--`), block comments (`/* ... */`, including multi-line), and + /// string literals (`'...'` / `"..."`) from a single line so body-terminator checks do not trip + /// on text inside comments or strings. Bracket identifiers (`[name]`) and backtick identifiers /// (`` `name` ``) are left untouched since they never contain SQL tokens. - /// SQL の行コメント(`--`)、ブロックコメント(`/* ... */`)、文字列リテラル(`'...'` / `"..."`)を - /// 除去して、コメントや文字列中の語で本体終端が誤検出されないようにする。角括弧識別子 `[name]` と - /// バッククォート識別子 `` `name` `` はそのまま残す(SQL トークンを含まないため)。 + /// `inBlockComment` is threaded across lines by the caller so a `/* ... */` block opened on one + /// line continues to mask terminators like bare `GO` / `CREATE` appearing on subsequent lines + /// until the closing `*/` is reached. See issue #429 follow-up. + /// SQL の行コメント(`--`)、ブロックコメント(`/* ... */`、複数行にまたがるものを含む)、 + /// および文字列リテラル(`'...'` / `"..."`)を除去して、コメントや文字列中の語で本体終端が + /// 誤検出されないようにする。角括弧識別子 `[name]` とバッククォート識別子 `` `name` `` はそのまま + /// 残す(SQL トークンを含まないため)。`inBlockComment` は呼び出し側で行間に持ち越し、ある行で + /// 開いた `/* ... */` ブロックが後続行にまたがって閉じるまで、`GO` / `CREATE` のような終端語を + /// マスクし続ける。issue #429 追補参照。 /// - private static string MaskSqlLineForBodyScan(string line) + private static string MaskSqlLineForBodyScan(string line, ref bool inBlockComment) { if (string.IsNullOrEmpty(line)) return line; @@ -7150,7 +7169,27 @@ private static string MaskSqlLineForBodyScan(string line) bool inSingle = false; bool inDouble = false; - for (int i = 0; i < line.Length; i++) + int startIndex = 0; + if (inBlockComment) + { + // Line starts inside a multi-line block comment opened by a previous line. + // Find its close (`*/`) or blank out the whole line if the comment continues. + // 前行で開かれた複数行ブロックコメントの途中から始まる行。`*/` を探すか、閉じが無ければ + // この行全体を空白化する。 + int end = line.IndexOf("*/", 0, StringComparison.Ordinal); + if (end < 0) + { + for (int k = 0; k < line.Length; k++) + sb.Append(' '); + return sb.ToString(); + } + for (int k = 0; k <= end + 1; k++) + sb.Append(' '); + inBlockComment = false; + startIndex = end + 2; + } + + for (int i = startIndex; i < line.Length; i++) { char c = line[i]; @@ -7162,7 +7201,15 @@ private static string MaskSqlLineForBodyScan(string line) { int end = line.IndexOf("*/", i + 2, StringComparison.Ordinal); if (end < 0) - break; + { + // Unterminated block comment on this line — carry the state into the next + // line so its contents remain masked for body-terminator checks. + // この行で閉じない場合は次行へ状態を持ち越し、終端判定用にマスクを継続する。 + for (int k = i; k < line.Length; k++) + sb.Append(' '); + inBlockComment = true; + return sb.ToString(); + } for (int k = i; k <= end + 1; k++) sb.Append(' '); i = end + 1; diff --git a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs index e55ba6609b..4590c45c13 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs @@ -4881,7 +4881,11 @@ CREATE PROCEDURE dbo.sp_Outer Assert.All(bodyCalls, r => { Assert.Equal("function", r.ContainerKind); - Assert.Equal("sp_Outer", r.ContainerName); + // SymbolExtractor stores SQL proc names with their schema prefix (e.g. `dbo.sp_Outer`), + // so ContainerName surfaces the qualified name too. + // SymbolExtractor は SQL プロシージャ名をスキーマ修飾付き(例: `dbo.sp_Outer`)で保持するため、 + // ContainerName にも修飾付きの名前が載る。 + Assert.Equal("dbo.sp_Outer", r.ContainerName); }); // The post-GO call (line 9) is outside any procedure body and must carry no container. @@ -4916,7 +4920,11 @@ CREATE OR REPLACE FUNCTION public.fn_outer() RETURNS void AS $$ var bodyCall = Assert.Single(innerCalls.Where(r => r.Line == 3)); Assert.Equal("function", bodyCall.ContainerKind); - Assert.Equal("fn_outer", bodyCall.ContainerName); + // Postgres function names are captured with their schema (e.g. `public.fn_outer`), + // so ContainerName surfaces the qualified name too. + // Postgres の関数名はスキーマ付き(例: `public.fn_outer`)で取られるため、ContainerName にも + // 修飾付きの名前が載る。 + Assert.Equal("public.fn_outer", bodyCall.ContainerName); var outsideCall = Assert.Single(innerCalls.Where(r => r.Line == 7)); Assert.Null(outsideCall.ContainerKind); diff --git a/tests/CodeIndex.Tests/SymbolExtractorTests.cs b/tests/CodeIndex.Tests/SymbolExtractorTests.cs index f723a6db45..4ed896fe54 100644 --- a/tests/CodeIndex.Tests/SymbolExtractorTests.cs +++ b/tests/CodeIndex.Tests/SymbolExtractorTests.cs @@ -6545,6 +6545,73 @@ public void Extract_SQL_ProcedureBodyRange_DoesNotPolluteContainer() $"dbo.sp_Second was wrapped under container=dbo.sp_First — CREATE PROCEDURE body must not wrap sibling procedures."); } + [Fact] + public void Extract_SQL_ProcedureBodyRange_BodyInternalCreateTableDoesNotCloseBody() + { + // Regression for codex review iteration 1 finding #1: the body terminator heuristic must + // only react to *another* proc-like DDL header (`CREATE|ALTER|DROP PROCEDURE|PROC|FUNCTION| + // TRIGGER`), not to ordinary body-internal DDL like `CREATE TABLE #tmp` / `ALTER TABLE`. + // Otherwise issue #429 re-appears whenever a T-SQL procedure stages a temp table before its + // real work. + // codex レビュー iteration 1 指摘 #1 の回帰テスト: 本体終端の判定は別の proc 系ヘッダ + // (`CREATE|ALTER|DROP PROCEDURE|PROC|FUNCTION|TRIGGER`)のときだけ発火し、本体内の普通の + // DDL(`CREATE TABLE #tmp` / `ALTER TABLE` など)では閉じてはならない。そうしないと、T-SQL + // プロシージャが一時テーブルを用意してから実処理する典型パターンで issue #429 が再発する。 + var content = + "CREATE PROCEDURE dbo.sp_Stage AS\n" + // line 1 + "BEGIN\n" + // line 2 + " CREATE TABLE #tmp(id INT);\n" + // line 3 — body-internal DDL, must NOT close + " ALTER TABLE #tmp ADD name NVARCHAR(100);\n" + // line 4 — same + " EXEC dbo.sp_Inner;\n" + // line 5 — real EXEC must be inside body + "END\n" + // line 6 + "GO\n" + // line 7 + "CREATE PROCEDURE dbo.sp_Inner AS BEGIN SELECT 1; END\n" + // line 8 + "GO\n"; // line 9 + + var symbols = SymbolExtractor.Extract(1, "sql", content); + + var stage = Assert.Single(symbols, s => s.Kind == "function" && s.Name == "dbo.sp_Stage"); + Assert.NotNull(stage.BodyStartLine); + Assert.NotNull(stage.BodyEndLine); + Assert.True(stage.BodyEndLine!.Value >= 5, + $"BodyEndLine={stage.BodyEndLine} must cover the EXEC on line 5; body-internal CREATE TABLE / ALTER TABLE must not close the body."); + Assert.True(stage.BodyEndLine!.Value < 8, + $"BodyEndLine={stage.BodyEndLine} must not leak into sp_Inner starting on line 8."); + } + + [Fact] + public void Extract_SQL_ProcedureBodyRange_MultiLineBlockCommentDoesNotCloseBody() + { + // Regression for codex review iteration 1 finding #2: MaskSqlLineForBodyScan threads an + // `inBlockComment` state across lines, so a bare `GO` or `CREATE` appearing inside a + // multi-line `/* ... */` block must not close the enclosing procedure body. + // codex レビュー iteration 1 指摘 #2 の回帰テスト: MaskSqlLineForBodyScan は `inBlockComment` + // を行間に持ち越し、複数行の `/* ... */` ブロック内に現れる単独 `GO` / `CREATE` では + // 外側プロシージャ本体を閉じてはならない。 + var content = + "CREATE PROCEDURE dbo.sp_CommentHeavy AS\n" + // line 1 + "BEGIN\n" + // line 2 + " /*\n" + // line 3 — block comment opens + " GO\n" + // line 4 — bare GO inside comment + " CREATE PROCEDURE dbo.sp_FakeInner AS SELECT 0;\n" + // line 5 — fake header inside comment + " */\n" + // line 6 — block comment closes + " EXEC dbo.sp_Real;\n" + // line 7 — real EXEC must be inside body + "END\n" + // line 8 + "GO\n" + // line 9 — real terminator + "CREATE PROCEDURE dbo.sp_Real AS BEGIN SELECT 1; END\n" + // line 10 + "GO\n"; // line 11 + + var symbols = SymbolExtractor.Extract(1, "sql", content); + + var heavy = Assert.Single(symbols, s => s.Kind == "function" && s.Name == "dbo.sp_CommentHeavy"); + Assert.NotNull(heavy.BodyStartLine); + Assert.NotNull(heavy.BodyEndLine); + Assert.True(heavy.BodyEndLine!.Value >= 7, + $"BodyEndLine={heavy.BodyEndLine} must cover the real EXEC on line 7; multi-line block comment must not close the body."); + Assert.True(heavy.BodyEndLine!.Value < 10, + $"BodyEndLine={heavy.BodyEndLine} must not leak into sp_Real starting on line 10."); + } + [Fact] public void Extract_Terraform_DetectsResources() { From b2287dc561382926a7f233618b2a392afcc9bd76 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sun, 19 Apr 2026 06:22:39 +0000 Subject: [PATCH 4/4] Address codex review iteration 2 findings for #429 - Accept both OR REPLACE (PostgreSQL) and OR ALTER (T-SQL / SQL Server 2016+) in SqlTopLevelDdlStartRegex so a sibling CREATE OR ALTER PROCEDURE without an intervening GO actually terminates the prior procedure body range. This keeps the terminator regex in sync with the CREATE-side symbol regex which already accepts both OR REPLACE and OR ALTER. - Track block comment depth as int instead of bool in MaskSqlLineForBodyScan so PostgreSQL-style nested /* /* ... */ ... */ block comments do not exit on the inner */ and re-expose comment-interior GO / CREATE PROCEDURE tokens that would prematurely close the body. T-SQL / MySQL / Oracle do not support nested block comments but depth is strictly safer there too (a non-nested outer */ still closes when depth returns to 0). - Add two SymbolExtractor regression tests: * Extract_SQL_ProcedureBodyRange_CreateOrAlterClosesPriorBody pins CREATE OR ALTER PROCEDURE sibling closing the prior body. * Extract_SQL_ProcedureBodyRange_NestedBlockCommentDoesNotCloseBody pins nested block comments not closing the body on inner */. - Update CHANGELOG (English and Japanese) #429 entry. Closes #429 Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 4 +- src/CodeIndex/Indexer/SymbolExtractor.cs | 145 +++++++++++------- tests/CodeIndex.Tests/SymbolExtractorTests.cs | 79 ++++++++++ 3 files changed, 170 insertions(+), 58 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 821583eddb..15006a7da9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **T-SQL / SQL Server DDL kinds now surface as symbols (#230)** — The `SymbolExtractor` SQL row previously only captured `CREATE TABLE` / `(MATERIALIZED) VIEW` / `PROCEDURE` / `FUNCTION` / `TRIGGER` / `TYPE` / `SCHEMA` / `SEQUENCE` / `DOMAIN` / `EXTENSION` / `INDEX` and `ALTER TABLE`, so on T-SQL / SQL Server migration scripts the short-form `CREATE PROC`, T-SQL `CREATE OR ALTER` (SQL Server 2016+), `CREATE SYNONYM`, `CREATE DATABASE` / `LOGIN` / `USER` / `ROLE` / `CERTIFICATE`, `CREATE PARTITION FUNCTION` / `PARTITION SCHEME`, `CREATE FULLTEXT CATALOG`, and every non-`TABLE` `ALTER` form (procedures, views, sequences, synonyms, principals, etc.) were silently dropped from the index — making `symbols` / `definition` / `outline` / `inspect` look near-empty on enterprise .NET backends whose primary surface is a stored-procedure or schema-migration repo. The TABLE/VIEW and PROCEDURE/PROC/FUNCTION/TRIGGER rows now accept `CREATE OR (REPLACE|ALTER)`, the procedure row accepts the `PROC` synonym, dedicated rows were added for `CREATE SYNONYM` (class), `CREATE DATABASE|LOGIN|USER|ROLE|CERTIFICATE` (class), `CREATE PARTITION FUNCTION` (function) / `CREATE PARTITION SCHEME` (class), `CREATE FULLTEXT CATALOG` (class), and the `ALTER` side is split into kind-consistent rows so procedure-like `ALTER` (`PROCEDURE` / `PROC` / `FUNCTION` / `TRIGGER` / `PARTITION FUNCTION`) emits kind `function`, `ALTER SCHEMA` emits kind `namespace`, `ALTER EXTENSION` emits kind `import`, and the remaining object kinds (`TABLE`, `(MATERIALIZED) VIEW`, `SEQUENCE`, `SYNONYM`, `LOGIN`, `USER`, `ROLE`, `DATABASE`, `CERTIFICATE`, `INDEX`, `TYPE`, `DOMAIN`, `PARTITION SCHEME`, `FULLTEXT CATALOG`) emit kind `class`. Splitting the `ALTER` row by kind keeps the same object's `CREATE` and `ALTER` symbols under the same `kind` so `symbols --kind function` / `definition` / `inspect` do not duplicate across kinds. The `ALTER` side also now picks up `CERTIFICATE`, `DOMAIN`, and `EXTENSION` that were missing from the original row so every non-`TABLE` `ALTER` form is actually captured rather than just claimed. Every SQL row now shares an identifier shape that accepts PG double-quoted (`"name"`), T-SQL bracketed (`[name]`), or bare (`\w+`) tokens with optional dotted qualifiers such as `[dbo].[sp_X]`, `"schema"."name"`, and `schema.name`. The existing `CREATE INDEX ON tbl` / anonymous-name guard (`(?!ON\b)`), the `CREATE TYPE ... AS ENUM` Postgres branch, and the `CREATE SCHEMA AUTHORIZATION` branch are preserved. Body-less `namespace`-kind symbols from SQL (`CREATE SCHEMA sales;`, `ALTER SCHEMA sales TRANSFER ...;`) would previously trip the `IsFileScopedNamespace` check that wraps every subsequent top-level symbol under a C# `namespace X;` file-scoped declaration — so `ALTER FUNCTION dbo.fn_Total`, `ALTER PARTITION FUNCTION pf_OrdersByYear`, and every later declaration in the same file were emitted with `container_kind='namespace'` / `container_name='sales'`, breaking `definition` / `inspect` / family grouping on real migration scripts. The helper now additionally requires the signature to start with the `namespace` keyword, so SQL schema rows no longer act as file-scoped containers; SQL schema scoping is still expressed through qualified names (`sales.Money`), matching how the rest of cdidx reads SQL. Added a dedicated `Extract_SQL_DetectsTSqlDdlKinds` regression test that locks all thirteen new T-SQL `CREATE` shapes and every new `ALTER` kind contract (`ALTER PROCEDURE` / `ALTER FUNCTION` / `ALTER PARTITION FUNCTION` → `function`; `ALTER SCHEMA` → `namespace`; `ALTER EXTENSION` → `import`; `ALTER CERTIFICATE` / `ALTER DOMAIN` → `class`) including the `CREATE PROC` + `ALTER PROCEDURE` pair that must emit two `dbo.sp_DailyReport` rows both under kind `function`, and asserts that no later top-level SQL symbol is wrapped under `namespace=sales` so the file-scoped-namespace container pollution cannot regress. `GO` batch boundary awareness and `BEGIN ... END` structural nesting are deliberately out of scope here and remain filed in #230 as stretch items. Affected: `src/CodeIndex/Indexer/SymbolExtractor.cs`, `tests/CodeIndex.Tests/SymbolExtractorTests.cs`. Closes #230. #### Fixed -- **SQL `CREATE` / `ALTER PROCEDURE` / `PROC` / `FUNCTION` / `TRIGGER` bodies now carry body ranges, so calls inside the body attribute to the enclosing proc (#429)** — `SymbolExtractor` emitted every `CREATE` / `ALTER PROCEDURE` / `PROC` / `FUNCTION` / `TRIGGER` with `BodyStyle.None`, leaving `BodyStartLine` / `BodyEndLine` null. `ReferenceExtractor.ResolveContainerForCall` → `FindInnermostContainer` relies on those ranges to attribute an in-body call to the enclosing procedure, so every call inside a procedure body fell through to the file-level fallback and `symbol_references.container_kind` / `container_name` were written as null. As a result `cdidx callers` / `callees` / `impact` returned zero for every SQL procedure even though `graph_supported` reported true. The procedure/function/trigger rows now emit a new `BodyStyle.SqlProcBody`, and the new `FindSqlProcBodyRange(lines, startIndex)` helper scans forward for (1) a balanced PostgreSQL dollar-quoted body (`$$ ... $$` / `$tag$ ... $tag$`) when the next non-blank payload begins with a dollar tag, (2) a T-SQL `GO` batch separator at line start (trailing `;` / `--` comment tolerated), (3) a sibling proc-like DDL start (`CREATE` / `ALTER` / `DROP` + optional `OR REPLACE` + `PROCEDURE` / `PROC` / `FUNCTION` / `TRIGGER`) — narrowed from the original `CREATE|ALTER|DROP|GRANT|REVOKE|DENY|USE` catch-all so body-internal `CREATE TABLE #tmp` / `ALTER TABLE` / `GRANT` / `USE` do not prematurely close the proc body and re-introduce issue #429, or (4) end-of-file. Before scanning each line, `MaskSqlLineForBodyScan` strips `--` line comments, `/* ... */` block comments, and `'..'` / `".."` string literals (preserving column width so later column alignment stays correct, and preserving `[name]` / `` `name` `` quoted identifiers because SQL quoted identifiers are not string literals) so a `GO` / `$$` / `CREATE PROCEDURE` token embedded in a string or comment does not close the body early. The mask also threads an `inBlockComment` state across lines via `ref bool`, so a multi-line `/* ... */` block — even one holding a bare `GO` or a `CREATE PROCEDURE` header on a comment-interior line — stays fully masked until `*/` is reached instead of bailing out on the first following line. Both the `GO` terminator and the sibling-proc-DDL terminator are now evaluated against the masked text for the same reason. `ALTER PARTITION FUNCTION` is intentionally split into its own pattern and keeps `BodyStyle.None` because it modifies a partition boundary value rather than executable code, so its surface should remain body-less. Added ten `SymbolExtractor` regression tests covering T-SQL `BEGIN ... END` bodies terminated by `GO`, Postgres dollar-quoted function bodies, single-line `AS BEGIN ... END;` bodies, DDL-start close without an intervening `GO`, string / comment masking (a `GO` / `$$` / new proc-like `CREATE` inside a string literal or `--` comment does not close the body), `ALTER PROCEDURE` body capture, `ALTER PARTITION FUNCTION` staying body-less, a regression guard that a second procedure is not swallowed into the previous procedure's body range, body-internal `CREATE TABLE #tmp` / `ALTER TABLE` not closing the body, and a multi-line `/* ... */` block containing bare `GO` / fake `CREATE PROCEDURE` headers not closing the body. Added two `ReferenceExtractor` regression tests: `Extract_SqlCallInsideProcedureBody_AttributesCallerToEnclosingProcedure` pins that an `EXEC` inside a T-SQL `CREATE PROCEDURE dbo.sp_Outer` body resolves to `ContainerKind=function` / `ContainerName=dbo.sp_Outer` (schema-qualified, matching the persisted `SymbolRecord.Name`) while a post-`GO` call has null container, and `Extract_SqlPostgresDollarQuotedBody_AttributesCallerToEnclosingFunction` pins the same for a `PERFORM public.fn_inner()` inside a Postgres `$$ ... $$`-quoted `public.fn_outer` body. Affected: `src/CodeIndex/Indexer/SymbolExtractor.cs`, `tests/CodeIndex.Tests/SymbolExtractorTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. Closes #429. +- **SQL `CREATE` / `ALTER PROCEDURE` / `PROC` / `FUNCTION` / `TRIGGER` bodies now carry body ranges, so calls inside the body attribute to the enclosing proc (#429)** — `SymbolExtractor` emitted every `CREATE` / `ALTER PROCEDURE` / `PROC` / `FUNCTION` / `TRIGGER` with `BodyStyle.None`, leaving `BodyStartLine` / `BodyEndLine` null. `ReferenceExtractor.ResolveContainerForCall` → `FindInnermostContainer` relies on those ranges to attribute an in-body call to the enclosing procedure, so every call inside a procedure body fell through to the file-level fallback and `symbol_references.container_kind` / `container_name` were written as null. As a result `cdidx callers` / `callees` / `impact` returned zero for every SQL procedure even though `graph_supported` reported true. The procedure/function/trigger rows now emit a new `BodyStyle.SqlProcBody`, and the new `FindSqlProcBodyRange(lines, startIndex)` helper scans forward for (1) a balanced PostgreSQL dollar-quoted body (`$$ ... $$` / `$tag$ ... $tag$`) when the next non-blank payload begins with a dollar tag, (2) a T-SQL `GO` batch separator at line start (trailing `;` / `--` comment tolerated), (3) a sibling proc-like DDL start (`CREATE` / `ALTER` / `DROP` + optional `OR REPLACE` or `OR ALTER` + `PROCEDURE` / `PROC` / `FUNCTION` / `TRIGGER`) — narrowed from the original `CREATE|ALTER|DROP|GRANT|REVOKE|DENY|USE` catch-all so body-internal `CREATE TABLE #tmp` / `ALTER TABLE` / `GRANT` / `USE` do not prematurely close the proc body and re-introduce issue #429, and the `OR REPLACE` / `OR ALTER` alternation is kept in sync with the CREATE-side symbol regex so a T-SQL `CREATE OR ALTER PROCEDURE` sibling (SQL Server 2016+) without an intervening `GO` actually terminates the previous body range, or (4) end-of-file. Before scanning each line, `MaskSqlLineForBodyScan` strips `--` line comments, `/* ... */` block comments (including PostgreSQL-style nested `/* /* ... */ ... */` via a `blockCommentDepth` counter threaded across lines via `ref int` — depth increments on every `/*` and decrements on every `*/`, so the inner `*/` no longer exits the outer comment and re-exposes `GO` / `CREATE PROCEDURE` tokens; T-SQL / MySQL / Oracle do not support nested block comments but the depth counter is strictly safer for those dialects too because a non-nested outer `*/` still closes when depth returns to 0), and `'..'` / `".."` string literals (preserving column width so later column alignment stays correct, and preserving `[name]` / `` `name` `` quoted identifiers because SQL quoted identifiers are not string literals) so a `GO` / `$$` / `CREATE PROCEDURE` token embedded in a string or comment does not close the body early. Both the `GO` terminator and the sibling-proc-DDL terminator are evaluated against the masked text for the same reason. `ALTER PARTITION FUNCTION` is intentionally split into its own pattern and keeps `BodyStyle.None` because it modifies a partition boundary value rather than executable code, so its surface should remain body-less. Added twelve `SymbolExtractor` regression tests covering T-SQL `BEGIN ... END` bodies terminated by `GO`, Postgres dollar-quoted function bodies, single-line `AS BEGIN ... END;` bodies, DDL-start close without an intervening `GO`, string / comment masking (a `GO` / `$$` / new proc-like `CREATE` inside a string literal or `--` comment does not close the body), `ALTER PROCEDURE` body capture, `ALTER PARTITION FUNCTION` staying body-less, a regression guard that a second procedure is not swallowed into the previous procedure's body range, body-internal `CREATE TABLE #tmp` / `ALTER TABLE` not closing the body, a multi-line `/* ... */` block containing bare `GO` / fake `CREATE PROCEDURE` headers not closing the body, a T-SQL `CREATE OR ALTER PROCEDURE` sibling without an intervening `GO` closing the prior body, and a nested `/* /* GO CREATE PROCEDURE ... */ GO CREATE PROCEDURE ... */` block comment that must not exit on the inner `*/`. Added two `ReferenceExtractor` regression tests: `Extract_SqlCallInsideProcedureBody_AttributesCallerToEnclosingProcedure` pins that an `EXEC` inside a T-SQL `CREATE PROCEDURE dbo.sp_Outer` body resolves to `ContainerKind=function` / `ContainerName=dbo.sp_Outer` (schema-qualified, matching the persisted `SymbolRecord.Name`) while a post-`GO` call has null container, and `Extract_SqlPostgresDollarQuotedBody_AttributesCallerToEnclosingFunction` pins the same for a `PERFORM public.fn_inner()` inside a Postgres `$$ ... $$`-quoted `public.fn_outer` body. Affected: `src/CodeIndex/Indexer/SymbolExtractor.cs`, `tests/CodeIndex.Tests/SymbolExtractorTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. Closes #429. - **Regression lock for C# interpolated and verbatim string reference extraction (#264)** — The original issue report alleged that `ReferenceExtractor` hid call-site references inside C# `$"..."` interpolation holes and leaked phantom references from `@"..."` verbatim string bodies. On the current `StructuralLineMasker` implementation the C# paths already handle single-line `$"..."`, multi-line `$@"..."` / `@$"..."`, and non-interpolated `@"..."` correctly — interpolation-hole call sites are captured with correct `ContainerName`, and multi-line verbatim bodies never leak phantom edges — but no regression test pinned that behavior end-to-end for the exact fixture in the issue report, so any future refactor of the masker's C# frame-stack handling could silently reintroduce the bug. Added `Extract_CsharpInterpolatedAndVerbatimStrings_Issue264_Repro_CapturesHoleCallsAndSuppressesPhantoms` which feeds the issue's exact fixture (`$"Hello {Helper.GetName()}"`, `$"Age: {Helper.GetAge()} years"`, `$"Nested {Helper.Format(Helper.GetName())}"`, a multi-line `$@"..."` with an interpolation hole, a plain non-interpolated call site, and a multi-line `@"..."` verbatim body containing `PhantomCall()` / `PhantomTable()` / `MoreFake()`) through `ReferenceExtractor.Extract` and pins four invariants at once: `Helper.GetName` is captured exactly four times, `Helper.GetAge` and `Helper.Format` are each captured at least once, every captured call's `ContainerName` resolves to `Work`, and no reference row ever surfaces for the phantom identifiers embedded in the verbatim body. Affected: `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. Closes #264. - **C# nested `new interface` (member-hiding a base-class nested interface) is now pinned by a dedicated regression (#376)** — Issue #376 reported that `public new interface INested { ... }` nested inside a derived class was silently dropped because the C# interface regex modifier list was `(?:partial|unsafe)` and did not include `new`. The free-modifier-order rewrite for #355 already widened the list to `(?:partial|unsafe|file|new)`, so the current binary already emits two `interface INested` symbols — one nested in `Base`, one nested in `Derived` — and the #376 repro is no longer reproducible. This change locks that behavior in with a dedicated `Extract_CSharp_NewNestedInterface_MemberHiding` regression that runs the exact fixture from the issue and asserts both nested interfaces are extracted (`ContainerName == "Base"` and `ContainerName == "Derived"`), so a future modifier-list edit cannot regress the #376 case without a failing test. Affected: `tests/CodeIndex.Tests/SymbolExtractorTests.cs`. Closes #376. - **C# 8 `readonly` instance indexers on structs are now captured as `Item` symbols (#352)** — `SymbolExtractor` previously dropped `public readonly int this[int i] => _arr[i];` and `public readonly string this[string key] { get => key; }` silently, so `definition`, `symbols`, `outline`, and `inspect` undercounted `readonly`-heavy slice / span / immutable-wrapper struct surfaces. The indexer row already accepts `readonly` in the free-order modifier slot alongside `unsafe` / `extern` / `ref (readonly)` following the #355 rewrite, and this change locks that behavior in with a dedicated `Extract_CSharp_DetectsReadonlyIndexers` regression covering expression-body, block-body, and generic-return (`public readonly T this[int i] { get => _items[i]; }`) shapes plus non-readonly / readonly-method baselines so a future regex refactor cannot regress the #352 case without a failing test. Affected: `tests/CodeIndex.Tests/SymbolExtractorTests.cs`. @@ -717,7 +717,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **T-SQL / SQL Server の DDL 種別をシンボルとして拾えるようにした (#230)** — `SymbolExtractor` の SQL 行は従来 `CREATE TABLE` / `(MATERIALIZED) VIEW` / `PROCEDURE` / `FUNCTION` / `TRIGGER` / `TYPE` / `SCHEMA` / `SEQUENCE` / `DOMAIN` / `EXTENSION` / `INDEX` と `ALTER TABLE` しか捕捉しておらず、T-SQL / SQL Server のマイグレーション系スクリプトでは短縮形 `CREATE PROC`、T-SQL の `CREATE OR ALTER`(SQL Server 2016+)、`CREATE SYNONYM`、`CREATE DATABASE` / `LOGIN` / `USER` / `ROLE` / `CERTIFICATE`、`CREATE PARTITION FUNCTION` / `PARTITION SCHEME`、`CREATE FULLTEXT CATALOG`、および `TABLE` 以外の `ALTER`(プロシージャ、ビュー、シーケンス、シノニム、プリンシパル等)がすべてインデックスから黙って落ちており、ストアドプロシージャやスキーマ・マイグレーション中心のエンタープライズ .NET バックエンドでは `symbols` / `definition` / `outline` / `inspect` がほぼ空に見えてしまっていた。TABLE/VIEW 行と PROCEDURE/PROC/FUNCTION/TRIGGER 行を `CREATE OR (REPLACE|ALTER)` 対応に拡張し、プロシージャ行に `PROC` 短縮形を追加した。`CREATE SYNONYM`(class)、`CREATE DATABASE|LOGIN|USER|ROLE|CERTIFICATE`(class)、`CREATE PARTITION FUNCTION`(function)/ `CREATE PARTITION SCHEME`(class)、`CREATE FULLTEXT CATALOG`(class)の専用行を追加し、`ALTER` 側も kind を揃える形で分割した — プロシージャ類の `ALTER`(`PROCEDURE` / `PROC` / `FUNCTION` / `TRIGGER` / `PARTITION FUNCTION`)は `function`、`ALTER SCHEMA` は `namespace`、`ALTER EXTENSION` は `import`、残りのオブジェクト種別(`TABLE`、`(MATERIALIZED) VIEW`、`SEQUENCE`、`SYNONYM`、`LOGIN`、`USER`、`ROLE`、`DATABASE`、`CERTIFICATE`、`INDEX`、`TYPE`、`DOMAIN`、`PARTITION SCHEME`、`FULLTEXT CATALOG`)は `class` を返す。`ALTER` を kind ごとに分けることで、同一オブジェクトの `CREATE` と `ALTER` が同じ `kind` に揃い、`symbols --kind function` / `definition` / `inspect` が種別またぎで重複しない。`ALTER` 側には元の行で漏れていた `CERTIFICATE` / `DOMAIN` / `EXTENSION` も追加し、`TABLE` 以外の `ALTER` を本当に網羅する形にした。SQL の全行が共通の識別子形状を共有し、PG の `"name"`、T-SQL の `[name]`、裸 `\w+` トークンをドット修飾できる形(`[dbo].[sp_X]` / `"schema"."name"` / `schema.name`)を全行で受け入れる。既存の `CREATE INDEX ON tbl` 無名ガード(`(?!ON\b)`)、Postgres の `CREATE TYPE ... AS ENUM` 分岐、`CREATE SCHEMA AUTHORIZATION` 分岐は維持している。さらに、SQL の `CREATE SCHEMA sales;` / `ALTER SCHEMA sales TRANSFER ...;` のような body 無しの `namespace` kind 行が、C# の `namespace X;` file-scoped namespace を想定した `IsFileScopedNamespace` 判定を発火させ、以降のトップレベルシンボル全部を `container_kind='namespace'` / `container_name='sales'` で包んでしまう不具合を修正した(`ALTER FUNCTION dbo.fn_Total` や `ALTER PARTITION FUNCTION pf_OrdersByYear` の container が汚染され、`definition` / `inspect` / family grouping が崩れていた)。`IsFileScopedNamespace` は signature が `namespace` キーワードで始まる行だけを対象とするよう追加ガードを入れ、SQL の schema は file-scoped container にならないようにした(SQL では schema スコープは `sales.Money` のような修飾名で表現する方針を維持)。`Extract_SQL_DetectsTSqlDdlKinds` 回帰テストを追加し、新規 13 形の `CREATE` に加え、新しい `ALTER` の kind 契約(`ALTER PROCEDURE` / `ALTER FUNCTION` / `ALTER PARTITION FUNCTION` → `function`、`ALTER SCHEMA` → `namespace`、`ALTER EXTENSION` → `import`、`ALTER CERTIFICATE` / `ALTER DOMAIN` → `class`)もすべて固定し、`CREATE PROC` と `ALTER PROCEDURE` のペアで `dbo.sp_DailyReport` が同じ `function` kind で 2 件出ることも含めて押さえ、後続のトップレベルシンボルが `namespace=sales` 配下に吸い込まれないことも固定した。`GO` バッチ境界や `BEGIN ... END` の構造ネストは本 issue のストレッチ項目として意図的に対象外。対象: `src/CodeIndex/Indexer/SymbolExtractor.cs`、`tests/CodeIndex.Tests/SymbolExtractorTests.cs`。Closes #230。 #### 修正 -- **SQL `CREATE` / `ALTER PROCEDURE` / `PROC` / `FUNCTION` / `TRIGGER` の本体に body range を付与し、body 内の呼び出しが外側プロシージャにひも付くよう修正 (#429)** — `SymbolExtractor` はこれまで `CREATE` / `ALTER PROCEDURE` / `PROC` / `FUNCTION` / `TRIGGER` をすべて `BodyStyle.None` で発行していたため、`BodyStartLine` / `BodyEndLine` が null のまま保存されていた。`ReferenceExtractor.ResolveContainerForCall` → `FindInnermostContainer` は body 内の呼び出しを外側プロシージャに結びつけるためにこの範囲を使うので、プロシージャ本体の呼び出しはすべてファイルレベルのフォールバックへ落ち、`symbol_references.container_kind` / `container_name` が null で書かれていた。結果として `cdidx callers` / `callees` / `impact` は `graph_supported=true` のまま、どの SQL プロシージャに対しても 0 件を返していた。プロシージャ/ファンクション/トリガー行が新設の `BodyStyle.SqlProcBody` を発行するようにし、新ヘルパー `FindSqlProcBodyRange(lines, startIndex)` が前方走査して、(1) 次の非空ペイロードが `$` タグで始まれば PostgreSQL の `$$ ... $$` / `$tag$ ... $tag$` を対応つきで閉じた位置、(2) 行頭の T-SQL `GO` バッチ区切り(末尾 `;` や `--` コメントは許容)、(3) sibling proc-like DDL 開始 (`CREATE` / `ALTER` / `DROP` + 任意の `OR REPLACE` + `PROCEDURE` / `PROC` / `FUNCTION` / `TRIGGER`) — 当初は `CREATE|ALTER|DROP|GRANT|REVOKE|DENY|USE` を一括 catch-all していたが、それだと body 内の `CREATE TABLE #tmp` / `ALTER TABLE` / `GRANT` / `USE` が body を早期終了させて #429 を再発させてしまうため、proc-like DDL に限定している、(4) ファイル末尾、のいずれかで body を閉じる。走査前には `MaskSqlLineForBodyScan` が各行を前処理し、`--` 行コメント、`/* ... */` ブロックコメント、`'..'` / `".."` 文字列リテラルを列幅を保ったままマスクして(`[name]` / `` `name` `` の引用識別子は SQL 上文字列ではないので保持する)、文字列やコメント内部の `GO` / `$$` / `CREATE PROCEDURE` トークンで body を早期終了しないようにしている。さらに複数行 `/* ... */` ブロックコメントに対応するため、マスクは `ref bool inBlockComment` で行をまたいだ block-comment 状態を伝播する。`*/` が現れるまではその後の行全体がマスクされ続けるので、コメント内部の `GO` や `CREATE PROCEDURE` 行で最初のコメント直後の行でフェイルクローズしない。`GO` ターミネータと sibling proc-like DDL ターミネータはいずれもマスク後のテキストに対して判定する。`ALTER PARTITION FUNCTION` は実行コードではなく partition 境界値を書き換えるだけの宣言なので、本来 body を持たない surface として扱う方針を維持するため、独立したパターンに分離して `BodyStyle.None` のまま残している。回帰テストとして `SymbolExtractor` 側に 10 本追加した: `GO` で閉じる T-SQL `BEGIN ... END`、Postgres の `$$ ... $$` 関数本体、単一行 `AS BEGIN ... END;`、`GO` を挟まず新 DDL で閉じるケース、文字列/コメント内に埋め込まれた `GO` / `$$` / 新 `CREATE` で body を閉じないマスキング、`ALTER PROCEDURE` 本体の捕捉、`ALTER PARTITION FUNCTION` が body を持たないこと、次のプロシージャ宣言が前プロシージャの body 範囲に吸収されないこと、body 内の `CREATE TABLE #tmp` / `ALTER TABLE` が body を閉じないこと、そして複数行 `/* ... */` ブロックコメント内に含まれる素の `GO` / 偽装 `CREATE PROCEDURE` ヘッダが body を閉じないことの 10 本。`ReferenceExtractor` 側に 2 本追加した: `Extract_SqlCallInsideProcedureBody_AttributesCallerToEnclosingProcedure` は T-SQL `CREATE PROCEDURE dbo.sp_Outer` 本体の `EXEC` が `ContainerKind=function` / `ContainerName=dbo.sp_Outer`(永続化される `SymbolRecord.Name` に揃えた schema 修飾名)に解決され、`GO` 以降の呼び出しは container が null のまま落ちることを固定し、`Extract_SqlPostgresDollarQuotedBody_AttributesCallerToEnclosingFunction` は Postgres の `$$ ... $$` 関数 `public.fn_outer` 本体内の `PERFORM public.fn_inner()` について同じ不変量を `ContainerName=public.fn_outer` で固定する。対象: `src/CodeIndex/Indexer/SymbolExtractor.cs`、`tests/CodeIndex.Tests/SymbolExtractorTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。Closes #429。 +- **SQL `CREATE` / `ALTER PROCEDURE` / `PROC` / `FUNCTION` / `TRIGGER` の本体に body range を付与し、body 内の呼び出しが外側プロシージャにひも付くよう修正 (#429)** — `SymbolExtractor` はこれまで `CREATE` / `ALTER PROCEDURE` / `PROC` / `FUNCTION` / `TRIGGER` をすべて `BodyStyle.None` で発行していたため、`BodyStartLine` / `BodyEndLine` が null のまま保存されていた。`ReferenceExtractor.ResolveContainerForCall` → `FindInnermostContainer` は body 内の呼び出しを外側プロシージャに結びつけるためにこの範囲を使うので、プロシージャ本体の呼び出しはすべてファイルレベルのフォールバックへ落ち、`symbol_references.container_kind` / `container_name` が null で書かれていた。結果として `cdidx callers` / `callees` / `impact` は `graph_supported=true` のまま、どの SQL プロシージャに対しても 0 件を返していた。プロシージャ/ファンクション/トリガー行が新設の `BodyStyle.SqlProcBody` を発行するようにし、新ヘルパー `FindSqlProcBodyRange(lines, startIndex)` が前方走査して、(1) 次の非空ペイロードが `$` タグで始まれば PostgreSQL の `$$ ... $$` / `$tag$ ... $tag$` を対応つきで閉じた位置、(2) 行頭の T-SQL `GO` バッチ区切り(末尾 `;` や `--` コメントは許容)、(3) sibling proc-like DDL 開始 (`CREATE` / `ALTER` / `DROP` + 任意の `OR REPLACE` または `OR ALTER` + `PROCEDURE` / `PROC` / `FUNCTION` / `TRIGGER`) — 当初は `CREATE|ALTER|DROP|GRANT|REVOKE|DENY|USE` を一括 catch-all していたが、それだと body 内の `CREATE TABLE #tmp` / `ALTER TABLE` / `GRANT` / `USE` が body を早期終了させて #429 を再発させてしまうため proc-like DDL に限定し、`OR REPLACE` / `OR ALTER` の分岐は CREATE 側シンボル正規表現と揃えているので、SQL Server 2016+ の `CREATE OR ALTER PROCEDURE` を `GO` なしで隣接宣言した場合でも前 proc の body 範囲が確実に終端する、(4) ファイル末尾、のいずれかで body を閉じる。走査前には `MaskSqlLineForBodyScan` が各行を前処理し、`--` 行コメント、`/* ... */` ブロックコメント(PostgreSQL 風のネスト `/* /* ... */ ... */` も `ref int blockCommentDepth` で行間追跡し、`/*` 毎に depth++、`*/` 毎に depth-- とすることで、内側 `*/` で外側コメントを抜けてコメント内の `GO` / `CREATE PROCEDURE` が再露出する問題を防いでいる。T-SQL / MySQL / Oracle は block comment のネストを許容しないが、depth カウンタはそれらの方言でも depth が 0 に戻った時点で閉じるだけで strictly safer)、および `'..'` / `".."` 文字列リテラルを列幅を保ったままマスクして(`[name]` / `` `name` `` の引用識別子は SQL 上文字列ではないので保持する)、文字列やコメント内部の `GO` / `$$` / `CREATE PROCEDURE` トークンで body を早期終了しないようにしている。`GO` ターミネータと sibling proc-like DDL ターミネータはいずれもマスク後のテキストに対して判定する。`ALTER PARTITION FUNCTION` は実行コードではなく partition 境界値を書き換えるだけの宣言なので、本来 body を持たない surface として扱う方針を維持するため、独立したパターンに分離して `BodyStyle.None` のまま残している。回帰テストとして `SymbolExtractor` 側に 12 本追加した: `GO` で閉じる T-SQL `BEGIN ... END`、Postgres の `$$ ... $$` 関数本体、単一行 `AS BEGIN ... END;`、`GO` を挟まず新 DDL で閉じるケース、文字列/コメント内に埋め込まれた `GO` / `$$` / 新 `CREATE` で body を閉じないマスキング、`ALTER PROCEDURE` 本体の捕捉、`ALTER PARTITION FUNCTION` が body を持たないこと、次のプロシージャ宣言が前プロシージャの body 範囲に吸収されないこと、body 内の `CREATE TABLE #tmp` / `ALTER TABLE` が body を閉じないこと、複数行 `/* ... */` ブロックコメント内に含まれる素の `GO` / 偽装 `CREATE PROCEDURE` ヘッダが body を閉じないこと、`GO` を挟まない T-SQL `CREATE OR ALTER PROCEDURE` 隣接宣言が前 proc の body を閉じること、ネスト `/* /* GO CREATE PROCEDURE ... */ GO CREATE PROCEDURE ... */` ブロックコメントが内側 `*/` で抜けてはならないこと、の 12 本。`ReferenceExtractor` 側に 2 本追加した: `Extract_SqlCallInsideProcedureBody_AttributesCallerToEnclosingProcedure` は T-SQL `CREATE PROCEDURE dbo.sp_Outer` 本体の `EXEC` が `ContainerKind=function` / `ContainerName=dbo.sp_Outer`(永続化される `SymbolRecord.Name` に揃えた schema 修飾名)に解決され、`GO` 以降の呼び出しは container が null のまま落ちることを固定し、`Extract_SqlPostgresDollarQuotedBody_AttributesCallerToEnclosingFunction` は Postgres の `$$ ... $$` 関数 `public.fn_outer` 本体内の `PERFORM public.fn_inner()` について同じ不変量を `ContainerName=public.fn_outer` で固定する。対象: `src/CodeIndex/Indexer/SymbolExtractor.cs`、`tests/CodeIndex.Tests/SymbolExtractorTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。Closes #429。 - **C# 補間文字列 / 逐語文字列に対する参照抽出の回帰テストを追加 (#264)** — issue 原文は `ReferenceExtractor` が C# の `$"..."` 補間ホール内の呼び出しを落とし、`@"..."` 逐語文字列本体から phantom 参照を漏らしていると報告していた。現時点の `StructuralLineMasker` 実装では、単行 `$"..."`、複数行 `$@"..."` / `@$"..."`、非補間 `@"..."` のいずれも C# 経路で正しく処理されており、補間ホール内の呼び出しは正しい `ContainerName` で捕捉され、複数行逐語本体から phantom 参照は漏れない状態になっているが、issue 原文の再現コードそのものを end-to-end で固定する回帰テストが無く、将来 masker の C# フレームスタック処理を refactor した際に静かに不具合が再発しうる状況だった。`Extract_CsharpInterpolatedAndVerbatimStrings_Issue264_Repro_CapturesHoleCallsAndSuppressesPhantoms` を追加し、issue 原文の fixture そのもの(`$"Hello {Helper.GetName()}"`、`$"Age: {Helper.GetAge()} years"`、`$"Nested {Helper.Format(Helper.GetName())}"`、補間ホール付き複数行 `$@"..."`、素の非補間呼び出し、`PhantomCall()` / `PhantomTable()` / `MoreFake()` を含む複数行 `@"..."` 逐語本体)を `ReferenceExtractor.Extract` に通し、(1) `Helper.GetName` がちょうど 4 回捕捉されること、(2) `Helper.GetAge` と `Helper.Format` がそれぞれ 1 回以上捕捉されること、(3) 捕捉された呼び出しの `ContainerName` がいずれも `Work` であること、(4) 逐語本体に埋め込まれた phantom 識別子がいずれも参照行として現れないこと、の 4 点を同時に固定する。対象: `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。Closes #264。 - **派生クラスにネストした `new interface`(ベースクラスの同名 nested interface を隠蔽する形)の抽出を専用回帰テストで固定化 (#376)** — Issue #376 は、派生クラスにネストした `public new interface INested { ... }` が無言で落ちる、`SymbolExtractor` の interface 正規表現の修飾子リストが `(?:partial|unsafe)` どまりで `new` が含まれていないのが根本原因、という報告だった。#355 の自由順修飾子書き換えでこのリストは既に `(?:partial|unsafe|file|new)` に拡張されており、現在のバイナリでは `Base` と `Derived` の双方で `interface INested` が 2 件抽出されるため、#376 の再現手順ではもう症状を再現できない。本変更はその挙動を専用回帰テスト `Extract_CSharp_NewNestedInterface_MemberHiding` として固定する。Issue 記載の fixture をそのまま流し、両 nested interface(`ContainerName == "Base"` と `ContainerName == "Derived"`)が抽出されることをアサートするので、今後の修飾子リスト編集で #376 のケースが失敗テスト無しに静かに再回帰しないようになる。対象: `tests/CodeIndex.Tests/SymbolExtractorTests.cs`。Closes #376。 - **C# 8 の struct 上 `readonly` インスタンスインデクサが `Item` シンボルとして抽出されることを回帰テストで固定化 (#352)** — `SymbolExtractor` は以前、`public readonly int this[int i] => _arr[i];` や `public readonly string this[string key] { get => key; }` を silent drop していたため、`readonly` インデクサを多用する slice / span / イミュータブル wrapper struct の表面積を `definition` / `symbols` / `outline` / `inspect` が過小にしていた。#355 の書き換えに伴って C# インデクサ行の自由順 modifier スロットが `readonly` に加えて `unsafe` / `extern` / `ref (readonly)` も受理するようになっているが、今回はその挙動を専用回帰テスト `Extract_CSharp_DetectsReadonlyIndexers` で固定する。式本体、ブロック本体、ジェネリック戻り値 (`public readonly T this[int i] { get => _items[i]; }`) の三形態と、非 readonly / readonly メソッドのベースラインまで含めてカバーし、今後の regex リファクタで #352 のケースが再回帰したときに失敗テストを伴わずに壊れないようにする。対象: `tests/CodeIndex.Tests/SymbolExtractorTests.cs`。 diff --git a/src/CodeIndex/Indexer/SymbolExtractor.cs b/src/CodeIndex/Indexer/SymbolExtractor.cs index a6e65f392e..8e4ee1b95c 100644 --- a/src/CodeIndex/Indexer/SymbolExtractor.cs +++ b/src/CodeIndex/Indexer/SymbolExtractor.cs @@ -7026,14 +7026,18 @@ private static (int EndLine, int? BodyStartLine, int? BodyEndLine) FindVisualBas @"^\s*GO\s*(?:;[\s;]*)?(?:--.*)?$", RegexOptions.Compiled | RegexOptions.IgnoreCase); // Only close a SQL proc body when the next top-level statement looks like another proc-like - // header (`CREATE|ALTER|DROP PROCEDURE|PROC|FUNCTION|TRIGGER`, optionally with `OR REPLACE`). - // Body-internal `CREATE TABLE` / `ALTER TABLE` / `GRANT` / `USE` etc. must not prematurely - // close the enclosing procedure body. See issue #429. + // header (`CREATE|ALTER|DROP PROCEDURE|PROC|FUNCTION|TRIGGER`, optionally with `OR REPLACE` for + // PostgreSQL or `OR ALTER` for T-SQL / SQL Server 2016+). Body-internal `CREATE TABLE` / + // `ALTER TABLE` / `GRANT` / `USE` etc. must not prematurely close the enclosing procedure body. + // The `OR REPLACE` / `OR ALTER` alternation must match the CREATE-side symbol regex above so a + // `CREATE OR ALTER PROCEDURE` sibling actually terminates the previous body range. See issue #429. // 次のトップレベル文が別の proc 系ヘッダ(`CREATE|ALTER|DROP` + `PROCEDURE|PROC|FUNCTION|TRIGGER`、 - // `OR REPLACE` 付きも許容)だった場合のみ、SQL の proc 本体を閉じる。本体内の `CREATE TABLE` / - // `ALTER TABLE` / `GRANT` / `USE` などで先走って閉じないこと。issue #429 参照。 + // PostgreSQL の `OR REPLACE` / T-SQL・SQL Server 2016+ の `OR ALTER` 付きも許容)だった場合のみ + // SQL の proc 本体を閉じる。本体内の `CREATE TABLE` / `ALTER TABLE` / `GRANT` / `USE` などで + // 先走って閉じないこと。`OR REPLACE` / `OR ALTER` の分岐は上の CREATE 側シンボル正規表現と揃え、 + // `CREATE OR ALTER PROCEDURE` の隣接宣言でも前の body 範囲を確実に終端させる。issue #429 参照。 private static readonly Regex SqlTopLevelDdlStartRegex = new( - @"^\s*(?:CREATE|ALTER|DROP)\s+(?:OR\s+REPLACE\s+)?(?:PROCEDURE|PROC|FUNCTION|TRIGGER)\b", + @"^\s*(?:CREATE|ALTER|DROP)\s+(?:OR\s+(?:REPLACE|ALTER)\s+)?(?:PROCEDURE|PROC|FUNCTION|TRIGGER)\b", RegexOptions.Compiled | RegexOptions.IgnoreCase); // Dollar-quoted body tags: `$$` or `$tagname$` (PostgreSQL). Tag must be empty or an identifier. // Dollar-quoted の本体タグ: `$$` または `$タグ名$`(PostgreSQL)。タグは空か識別子のみ。 @@ -7062,7 +7066,7 @@ private static (int EndLine, int? BodyStartLine, int? BodyEndLine) FindSqlProcBo int bodyStartLine = startIndex + 1; int endLine = startIndex + 1; string? openDollarTag = null; - bool inBlockComment = false; + int blockCommentDepth = 0; for (int i = startIndex; i < lines.Length; i++) { @@ -7082,7 +7086,7 @@ private static (int EndLine, int? BodyStartLine, int? BodyEndLine) FindSqlProcBo continue; } - var masked = MaskSqlLineForBodyScan(raw, ref inBlockComment); + var masked = MaskSqlLineForBodyScan(raw, ref blockCommentDepth); // Detect any unpaired dollar-quote opening on this line. Paired openings on the same // line (e.g. `AS $$ SELECT 1 $$`) are consumed without opening cross-line state. @@ -7146,21 +7150,27 @@ private static (int EndLine, int? BodyStartLine, int? BodyEndLine) FindSqlProcBo } /// - /// Strip SQL line comments (`--`), block comments (`/* ... */`, including multi-line), and - /// string literals (`'...'` / `"..."`) from a single line so body-terminator checks do not trip - /// on text inside comments or strings. Bracket identifiers (`[name]`) and backtick identifiers - /// (`` `name` ``) are left untouched since they never contain SQL tokens. - /// `inBlockComment` is threaded across lines by the caller so a `/* ... */` block opened on one - /// line continues to mask terminators like bare `GO` / `CREATE` appearing on subsequent lines - /// until the closing `*/` is reached. See issue #429 follow-up. - /// SQL の行コメント(`--`)、ブロックコメント(`/* ... */`、複数行にまたがるものを含む)、 - /// および文字列リテラル(`'...'` / `"..."`)を除去して、コメントや文字列中の語で本体終端が - /// 誤検出されないようにする。角括弧識別子 `[name]` とバッククォート識別子 `` `name` `` はそのまま - /// 残す(SQL トークンを含まないため)。`inBlockComment` は呼び出し側で行間に持ち越し、ある行で - /// 開いた `/* ... */` ブロックが後続行にまたがって閉じるまで、`GO` / `CREATE` のような終端語を - /// マスクし続ける。issue #429 追補参照。 + /// Strip SQL line comments (`--`), block comments (`/* ... */`, including multi-line and + /// PostgreSQL-style nested `/* /* ... */ ... */`), and string literals (`'...'` / `"..."`) + /// from a single line so body-terminator checks do not trip on text inside comments or + /// strings. Bracket identifiers (`[name]`) and backtick identifiers (`` `name` ``) are left + /// untouched since they never contain SQL tokens. + /// `blockCommentDepth` is threaded across lines by the caller so a `/* ... */` block opened on + /// one line continues to mask terminators like bare `GO` / `CREATE` appearing on subsequent + /// lines until all open `/*` are balanced. PostgreSQL allows nested block comments; T-SQL / + /// MySQL / Oracle do not, but using depth here is strictly safer for the dialects that do not + /// (every outer `*/` still closes when depth returns to 0). See issue #429 follow-up. + /// SQL の行コメント(`--`)、ブロックコメント(`/* ... */`、複数行にまたがるものと PostgreSQL 風の + /// ネスト `/* /* ... */ ... */` を含む)、および文字列リテラル(`'...'` / `"..."`)を除去して、 + /// コメントや文字列中の語で本体終端が誤検出されないようにする。角括弧識別子 `[name]` と + /// バッククォート識別子 `` `name` `` はそのまま残す(SQL トークンを含まないため)。 + /// `blockCommentDepth` は呼び出し側で行間に持ち越し、ある行で開いた `/* ... */` がすべての `/*` + /// と均衡するまで、後続行の `GO` / `CREATE` のような終端語をマスクし続ける。PostgreSQL は + /// ブロックコメントのネストを許容する一方、T-SQL / MySQL / Oracle は許容しないが、ここで depth を + /// 使っても後者では単に外側の `*/` で depth が 0 に戻って閉じるだけなので、厳密に safer。 + /// issue #429 追補参照。 /// - private static string MaskSqlLineForBodyScan(string line, ref bool inBlockComment) + private static string MaskSqlLineForBodyScan(string line, ref int blockCommentDepth) { if (string.IsNullOrEmpty(line)) return line; @@ -7168,66 +7178,85 @@ private static string MaskSqlLineForBodyScan(string line, ref bool inBlockCommen var sb = new StringBuilder(line.Length); bool inSingle = false; bool inDouble = false; + int i = 0; - int startIndex = 0; - if (inBlockComment) + while (i < line.Length) { - // Line starts inside a multi-line block comment opened by a previous line. - // Find its close (`*/`) or blank out the whole line if the comment continues. - // 前行で開かれた複数行ブロックコメントの途中から始まる行。`*/` を探すか、閉じが無ければ - // この行全体を空白化する。 - int end = line.IndexOf("*/", 0, StringComparison.Ordinal); - if (end < 0) + if (blockCommentDepth > 0) { - for (int k = 0; k < line.Length; k++) + // Inside a (possibly nested) block comment. Look for the next `/*` (increases depth) + // or `*/` (decreases depth), whichever comes first. Blank every column until we + // either close back to depth 0 or hit end of line. + // ブロックコメント内(ネスト可)。次に来る `/*`(深さ増)か `*/`(深さ減)のうち早い方を探し、 + // depth が 0 に戻るか行末に到達するまで各列を空白化する。 + int open = line.IndexOf("/*", i, StringComparison.Ordinal); + int close = line.IndexOf("*/", i, StringComparison.Ordinal); + + if (close < 0 && open < 0) + { + for (int k = i; k < line.Length; k++) + sb.Append(' '); + return sb.ToString(); + } + + if (close >= 0 && (open < 0 || close < open)) + { + for (int k = i; k <= close + 1; k++) + sb.Append(' '); + blockCommentDepth--; + i = close + 2; + continue; + } + + // `/*` comes first (or is tied — but `close < open` already handles the tie in favor + // of `*/`, so here we know `open <= close` strictly and `/*` is the next token). + // `/*` が先に現れる(`close < open` の場合は close 優先で既に分岐済みなので、ここでは + // `open <= close` かつ `/*` が次のトークン)。 + for (int k = i; k <= open + 1; k++) sb.Append(' '); - return sb.ToString(); + blockCommentDepth++; + i = open + 2; + continue; } - for (int k = 0; k <= end + 1; k++) - sb.Append(' '); - inBlockComment = false; - startIndex = end + 2; - } - for (int i = startIndex; i < line.Length; i++) - { char c = line[i]; if (!inSingle && !inDouble) { if (c == '-' && i + 1 < line.Length && line[i + 1] == '-') - break; - if (c == '/' && i + 1 < line.Length && line[i + 1] == '*') { - int end = line.IndexOf("*/", i + 2, StringComparison.Ordinal); - if (end < 0) - { - // Unterminated block comment on this line — carry the state into the next - // line so its contents remain masked for body-terminator checks. - // この行で閉じない場合は次行へ状態を持ち越し、終端判定用にマスクを継続する。 - for (int k = i; k < line.Length; k++) - sb.Append(' '); - inBlockComment = true; - return sb.ToString(); - } - for (int k = i; k <= end + 1; k++) + for (int k = i; k < line.Length; k++) sb.Append(' '); - i = end + 1; + return sb.ToString(); + } + if (c == '/' && i + 1 < line.Length && line[i + 1] == '*') + { + // Open a block comment; let the `blockCommentDepth > 0` branch close it on this + // or a later line. + // ブロックコメントを開始する。閉じ処理は `blockCommentDepth > 0` 分岐で同じ行か + // 後続行のどちらかが担当する。 + sb.Append(' '); + sb.Append(' '); + blockCommentDepth = 1; + i += 2; continue; } if (c == '\'') { inSingle = true; sb.Append(' '); + i++; continue; } if (c == '"') { inDouble = true; sb.Append(' '); + i++; continue; } sb.Append(c); + i++; } else if (inSingle) { @@ -7235,16 +7264,18 @@ private static string MaskSqlLineForBodyScan(string line, ref bool inBlockCommen { sb.Append(' '); sb.Append(' '); - i++; + i += 2; continue; } if (c == '\'') { inSingle = false; sb.Append(' '); + i++; continue; } sb.Append(' '); + i++; } else { @@ -7252,16 +7283,18 @@ private static string MaskSqlLineForBodyScan(string line, ref bool inBlockCommen { sb.Append(' '); sb.Append(' '); - i++; + i += 2; continue; } if (c == '"') { inDouble = false; sb.Append(' '); + i++; continue; } sb.Append(' '); + i++; } } diff --git a/tests/CodeIndex.Tests/SymbolExtractorTests.cs b/tests/CodeIndex.Tests/SymbolExtractorTests.cs index 4ed896fe54..d802f33265 100644 --- a/tests/CodeIndex.Tests/SymbolExtractorTests.cs +++ b/tests/CodeIndex.Tests/SymbolExtractorTests.cs @@ -6612,6 +6612,85 @@ public void Extract_SQL_ProcedureBodyRange_MultiLineBlockCommentDoesNotCloseBody $"BodyEndLine={heavy.BodyEndLine} must not leak into sp_Real starting on line 10."); } + [Fact] + public void Extract_SQL_ProcedureBodyRange_CreateOrAlterClosesPriorBody() + { + // Regression for codex review iteration 2 finding #1: SqlTopLevelDdlStartRegex must accept + // both PostgreSQL `CREATE OR REPLACE PROCEDURE` and T-SQL `CREATE OR ALTER PROCEDURE` + // (SQL Server 2016+) so a sibling `CREATE OR ALTER PROCEDURE` declaration without an + // intervening `GO` actually terminates the previous procedure's body range. + // codex レビュー iteration 2 指摘 #1 の回帰テスト: SqlTopLevelDdlStartRegex は PostgreSQL の + // `CREATE OR REPLACE PROCEDURE` と T-SQL(SQL Server 2016+)の `CREATE OR ALTER PROCEDURE` + // の両方を受理し、`GO` 区切りなしの隣接 `CREATE OR ALTER PROCEDURE` 宣言で前 proc の + // body 範囲を確実に閉じなければならない。 + var content = + "CREATE OR ALTER PROCEDURE dbo.sp_First AS\n" + // line 1 + "BEGIN\n" + // line 2 + " EXEC dbo.sp_Inner;\n" + // line 3 — must be inside sp_First body + "END\n" + // line 4 + "CREATE OR ALTER PROCEDURE dbo.sp_Second AS\n" +// line 5 — sibling must close sp_First + "BEGIN\n" + // line 6 + " EXEC dbo.sp_Other;\n" + // line 7 — must be inside sp_Second body + "END\n"; // line 8 + + var symbols = SymbolExtractor.Extract(1, "sql", content); + + var first = Assert.Single(symbols, s => s.Kind == "function" && s.Name == "dbo.sp_First"); + Assert.NotNull(first.BodyStartLine); + Assert.NotNull(first.BodyEndLine); + Assert.True(first.BodyEndLine!.Value >= 3, + $"BodyEndLine={first.BodyEndLine} must cover sp_First's EXEC on line 3."); + Assert.True(first.BodyEndLine!.Value < 5, + $"BodyEndLine={first.BodyEndLine} must close before sp_Second begins on line 5; CREATE OR ALTER PROCEDURE sibling must terminate the prior body."); + + var second = Assert.Single(symbols, s => s.Kind == "function" && s.Name == "dbo.sp_Second"); + Assert.NotNull(second.BodyStartLine); + Assert.NotNull(second.BodyEndLine); + Assert.True(second.BodyStartLine!.Value >= 5, + $"BodyStartLine={second.BodyStartLine} for sp_Second must start at or after line 5."); + Assert.True(second.BodyEndLine!.Value >= 7, + $"BodyEndLine={second.BodyEndLine} must cover sp_Second's EXEC on line 7."); + } + + [Fact] + public void Extract_SQL_ProcedureBodyRange_NestedBlockCommentDoesNotCloseBody() + { + // Regression for codex review iteration 2 finding #2: MaskSqlLineForBodyScan must track + // block-comment depth instead of a plain bool so PostgreSQL-style nested + // `/* /* ... */ ... */` block comments do not exit on the inner `*/` and re-expose comment- + // interior `GO` / `CREATE PROCEDURE` tokens that would prematurely close the body. + // codex レビュー iteration 2 指摘 #2 の回帰テスト: MaskSqlLineForBodyScan は plain bool ではなく + // ブロックコメント depth を追う必要があり、PostgreSQL 風のネスト `/* /* ... */ ... */` を + // 内側の `*/` で誤って抜けてはならない。抜けるとコメント内部の `GO` / `CREATE PROCEDURE` が + // 露出し、本体を早期に閉じてしまう。 + var content = + "CREATE PROCEDURE dbo.sp_NestedComment AS\n" + // line 1 + "BEGIN\n" + // line 2 + " /*\n" + // line 3 — outer block opens + " /*\n" + // line 4 — inner block opens + " GO\n" + // line 5 — bare GO in nested comment + " CREATE PROCEDURE dbo.sp_FakeNested AS SELECT 0;\n" + // line 6 — fake header + " */\n" + // line 7 — inner closes; outer still open + " GO\n" + // line 8 — still inside outer comment + " CREATE PROCEDURE dbo.sp_FakeOuter AS SELECT 0;\n" + // line 9 — still inside outer + " */\n" + // line 10 — outer closes + " EXEC dbo.sp_RealCall;\n" + // line 11 — real EXEC must be inside body + "END\n" + // line 12 + "GO\n" + // line 13 — real terminator + "CREATE PROCEDURE dbo.sp_AfterNested AS BEGIN SELECT 1; END\n" + // line 14 + "GO\n"; // line 15 + + var symbols = SymbolExtractor.Extract(1, "sql", content); + + var nested = Assert.Single(symbols, s => s.Kind == "function" && s.Name == "dbo.sp_NestedComment"); + Assert.NotNull(nested.BodyStartLine); + Assert.NotNull(nested.BodyEndLine); + Assert.True(nested.BodyEndLine!.Value >= 11, + $"BodyEndLine={nested.BodyEndLine} must cover the real EXEC on line 11; nested block comment must not close the body via the inner `*/`."); + Assert.True(nested.BodyEndLine!.Value < 14, + $"BodyEndLine={nested.BodyEndLine} must not leak into sp_AfterNested starting on line 14."); + } + [Fact] public void Extract_Terraform_DetectsResources() {