From 2849c4d2579dd7f680b09caf381303b302653f64 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 17 Apr 2026 19:04:47 +0900 Subject: [PATCH 01/30] Fix #293 classify C# attribute and Java-family annotation refs separately C# `[Attribute(args)]` and Java/Kotlin/Scala/TypeScript `@Annotation(args)` usages were previously recorded as `call` references, which polluted `callers`, `impact`, and `hotspots` with phantom method-call edges that are actually metadata annotations. ReferenceExtractor now reclassifies these identifiers as dedicated `attribute` / `annotation` reference kinds when they appear inside a C# `[...]` attribute list (direct, targeted `[return: Attr]` / `[assembly: Attr]`, or comma-separated `[Foo("a"), Bar("b")]` form), or when they follow a Java-family `@` marker (optionally through a dotted qualifier chain such as `@org.junit.Test(args)`). The call-graph read paths (default-kind `callers`, `callees`, their count queries, hotspot CTEs, and symbol-hotspot grouping) now filter by `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` so these metadata kinds no longer inflate caller lists or hotspot counts. `references --kind attribute` / `--kind annotation` remains available for consumers that want to inspect metadata usage explicitly, and the CLI help / `--kind` validator now lists the new reference kinds. Regression coverage pins the new classification for `[Obsolete("msg")]`, `[return: NotNull("x")]`, `[Foo("a"), Bar("b")]`, `[AttributeUsage(...)]`, Java `@Deprecated(since="1.0")`, qualified `@org.junit.Test(...)`, Kotlin `@Deprecated("msg")`, and guards that ordinary method bodies still emit `call`. Co-Authored-By: Claude Opus 4 --- CHANGELOG.md | 2 + DEVELOPER_GUIDE.md | 4 +- src/CodeIndex/Cli/ConsoleUi.cs | 2 +- src/CodeIndex/Cli/QueryCommandRunner.cs | 2 +- src/CodeIndex/Database/DbReader.cs | 16 ++ src/CodeIndex/Database/DbSymbolReader.cs | 2 + src/CodeIndex/Indexer/ReferenceExtractor.cs | 137 ++++++++++++- tests/CodeIndex.Tests/ConsoleUiTests.cs | 2 +- .../ReferenceExtractorTests.cs | 194 +++++++++++++++++- 9 files changed, 348 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c46a01240f..133c1622e3 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/), - **Developer Guide now distinguishes SQLite's role from "zero dependencies" and labels release numbers as examples** — Clarified that cdidx remains a zero-configuration, single-file CLI with exactly one production dependency (`Microsoft.Data.Sqlite`), and made the release-workflow checklist explicitly state that `1.9.0` is only an example version to substitute during an actual release. Affected: `DEVELOPER_GUIDE.md`. #### Fixed +- **C# attribute and Java/Kotlin/Scala/TypeScript annotation usages no longer pollute the call-graph as phantom `call` rows (#293)** — `ReferenceExtractor` now reclassifies identifiers that appear inside C# `[...]` attribute lists (including direct `[Attr(args)]`, targeted `[return: Attr]` / `[assembly: Attr]` forms, and comma-separated `[Foo("a"), Bar("b")]` lists) as `attribute` references, and identifiers preceded by a Java-family `@` marker (optionally through a dotted qualifier chain such as `@org.junit.Test(args)`) as `annotation` references. The shared graph filter `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` is now applied to default-kind `callers`, `callees`, count queries, hotspot CTEs, and symbol-hotspot grouping, so `callers Obsolete`, `callers Deprecated`, `impact Conditional`, and `hotspots` no longer surface phantom caller edges or inflate hotspot counts for metadata-only references. `references --kind attribute` / `--kind annotation` still expose these rows for callers that explicitly want to inspect metadata usage, and the CLI help / `--kind` validator now lists the new reference kinds. Regression coverage pins the new classification for `[Obsolete("msg")]`, `[return: NotNull("x")]`, `[Foo("a"), Bar("b")]`, `[AttributeUsage(...)]`, Java `@Deprecated(since="1.0")`, qualified `@org.junit.Test(...)`, Kotlin `@Deprecated("msg")`, plus guards that ordinary method bodies still emit `call`. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Database/DbReader.cs`, `src/CodeIndex/Database/DbSymbolReader.cs`, `src/CodeIndex/Cli/QueryCommandRunner.cs`, `src/CodeIndex/Cli/ConsoleUi.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`, `tests/CodeIndex.Tests/ConsoleUiTests.cs`. Closes #293. - **C# expression-bodied and block-bodied property callers attribute to the individual method/property instead of the enclosing class (#233)** — `SymbolExtractor` now detects `=> expr;` at top level inside `FindCSharpBraceRange` and assigns a body range covering the declaration line through the terminating `;` (including the multi-line form where `=>` is placed on a following indented line, both for methods and for expression-bodied properties). The block-bodied property scanner now also merges the common Microsoft-style `public int Wrap {` + next-line `get { ... }` shape with its accessor line so that form is recognized alongside the existing K&R (`public int Wrap { get { ... } }`) and Allman (`public int Wrap` then `{` on the next line) shapes, without misclassifying `public class X {` or other non-property headers. Block comments spanning several lines between the header and the body are handled correctly by the same line-scanning path. `ReferenceExtractor.FindInnermostContainer` now accepts `property` as a container kind alongside `function` / `class` / `namespace`. As a result `callers `, `impact_analysis`, and the caller rows surfaced by `inspect` / `analyze_symbol` no longer collapse `public int Wrap() => Compute();`, `public int Wrap => Compute();`, the block-bodied `public int Wrap { get { return Compute(); } }` (K&R), `public int Wrap {` / ` get { return Compute(); }` / `}` (same-line brace with next-line accessor), the Allman-style `public int Wrap` / `{` / ` get { return Compute(); }` / `}`, the multi-line expression-bodied `public int Wrap` / ` => Compute();`, or the same shapes with multi-line block comments between header and body into a single enclosing-class row — each caller now appears on its own row with the correct `caller_kind` (`function` or `property`) and `caller_name`. Regression coverage spans symbol body ranges, all three block-bodied property shapes plus a misclassification guard for bare-brace lines whose body is not an accessor, accessor-attribute and visibility-prefixed accessor lines, reference attribution for expression-bodied methods / properties / multi-line `=> expr;` / K&R block-bodied accessor / bare-brace-same-line accessor-next-line / Allman-style / multi-line expression-bodied property forms, the block-comment-between-header-and-body variants for both Allman and multi-line expression-bodied properties, the previously-encoded ternary-continuation caller test, and end-to-end `callers` assertions through the CLI for the K&R shape (covered by existing property tests), the bare-brace-same-line shape, the Allman block-body shape, the multi-line expression-bodied shape, and both block-comment-separated variants. Affected: `src/CodeIndex/Indexer/SymbolExtractor.cs`, `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/SymbolExtractorTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`, `tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`. Closes #233. - **C# 13 partial properties now participate in symbol extraction (#228)** — The C# extractor now accepts the `partial` modifier on properties and looks ahead across multiline headers, so brace-on-next-line declarations, block-bodied implementations, multiline expression-bodied properties, and `partial override` forms no longer disappear from `symbols`, `definition`, `outline`, `hotspots`, and related symbol-driven commands. Added regression coverage for multiline declaration-style, block-bodied implementation, and multiline `partial override` properties. Affected: `src/CodeIndex/Indexer/SymbolExtractor.cs`, `tests/CodeIndex.Tests/SymbolExtractorTests.cs`. Closes #228. - **C# operators, conversion operators, and indexers now use navigable symbol names (#213)** — `SymbolExtractor` now stores overloads as names like `operator +` and `operator checked +`, stores `implicit` / `explicit` conversion operators as exact names like `implicit operator decimal`, `explicit operator Money`, `explicit operator Dictionary`, and `explicit operator (int whole, int cents)` so they no longer collide with constructors, and normalizes C# indexers from the keyword `this` to the CLR-style name `Item`. Tuple/generic conversion target types now go through a lightweight type scanner and canonical whitespace normalization, including named tuple elements after generic, array, nested-tuple, nullable type tokens, `unsafe` pointer targets, and function-pointer target types, so exact-name queries do not depend on the source file's spacing style. Incremental `cdidx index .` now tracks a C# symbol-name contract version in `codeindex_meta` and automatically re-extracts unchanged C# files once when a canonical-name upgrade lands, so existing DBs pick up the new exact names without requiring `--rebuild`; `status` also exposes `csharp_symbol_name_ready`, exact-name queries surface stale canonical-name degradation instead of silent false negatives, `status --db ...` now prints a repair command that targets the diagnosed DB instead of the caller's default workspace DB, and MCP exact-name trust metadata now honors the active query scope instead of degrading unrelated languages or paths. Added regression coverage in extractor, DB query, CLI query, MCP query, status, and index-upgrade tests, and updated the developer guide and README to document the naming convention. Affected: `src/CodeIndex/Indexer/SymbolExtractor.cs`, `src/CodeIndex/Cli/IndexCommandRunner.cs`, `src/CodeIndex/Cli/QueryCommandRunner.cs`, `src/CodeIndex/Database/DbContext.cs`, `src/CodeIndex/Database/DbReader.cs`, `src/CodeIndex/Database/DbWriter.cs`, `src/CodeIndex/Models/QueryResults.cs`, `src/CodeIndex/Mcp/McpToolHandlers.cs`, `tests/CodeIndex.Tests/SymbolExtractorTests.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`, `tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `tests/CodeIndex.Tests/IndexCommandRunnerTests.cs`, `DEVELOPER_GUIDE.md`, `README.md`. Closes #213. @@ -677,6 +678,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **DEVELOPER_GUIDE が SQLite の位置付けとリリース番号の例示を明確化** — cdidx が「設定ゼロ・単一ファイル・本番依存1個」の CLI であり、その唯一の本番依存が `Microsoft.Data.Sqlite` であることを明記した。あわせて、リリース手順のチェックリスト中に出てくる `1.9.0` は固定値ではなく例示であり、実際のリリース番号へ読み替える前提だと明示した。対象: `DEVELOPER_GUIDE.md`。 #### 修正 +- **C# 属性と Java/Kotlin/Scala/TypeScript の annotation 使用が phantom な `call` 行として call-graph を汚染しないよう修正 (#293)** — `ReferenceExtractor` が C# の `[...]` 属性リスト内(直接形 `[Attr(args)]`、target 付き `[return: Attr]` / `[assembly: Attr]`、カンマ区切り `[Foo("a"), Bar("b")]` を含む)に現れる識別子を `attribute` 参照として、Java 系 `@` マーカーが先行する識別子(`@org.junit.Test(args)` のようなドット修飾チェーン越しも含む)を `annotation` 参照として分類するようになった。共有 filter `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` を default-kind の `callers`・`callees`・count クエリ・hotspot CTE・symbol-hotspot グルーピングにも適用し、`callers Obsolete`・`callers Deprecated`・`impact Conditional`・`hotspots` で phantom caller edge や metadata のみの参照による件数膨張が発生しないようにした。`references --kind attribute` / `--kind annotation` では metadata 使用を明示的に確認したい呼び出し元向けに従来通り表示し、CLI help と `--kind` バリデータも新しい reference kind を列挙する。`[Obsolete("msg")]`・`[return: NotNull("x")]`・`[Foo("a"), Bar("b")]`・`[AttributeUsage(...)]`、Java `@Deprecated(since="1.0")`・修飾付き `@org.junit.Test(...)`、Kotlin `@Deprecated("msg")` の分類と、通常のメソッド本体が引き続き `call` を返すことを押さえる回帰テストを追加。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Database/DbReader.cs`、`src/CodeIndex/Database/DbSymbolReader.cs`、`src/CodeIndex/Cli/QueryCommandRunner.cs`、`src/CodeIndex/Cli/ConsoleUi.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`、`tests/CodeIndex.Tests/ConsoleUiTests.cs`。Closes #293。 - **C# 式本体メンバーと block-bodied プロパティの caller が外側クラスではなく個別メソッド/プロパティに帰属するよう修正 (#233)** — `SymbolExtractor` の `FindCSharpBraceRange` がトップレベルの `=> expr;` を検出し、宣言行から終端 `;` までを本体範囲として扱うようになった(宣言行の次行にインデントして `=>` を置く multi-line 形にも、メソッドと式本体プロパティの双方で対応)。block-bodied プロパティの行結合ロジックも、`public int Wrap {` の次行に ` get { ... }` を置く Microsoft 標準形を accessor 行と結合するよう拡張し、既存の K&R (`public int Wrap { get { ... } }`) や Allman (`public int Wrap` の次行に `{`) と並ぶ 3 形を抽出対象にしつつ、`public class X {` のような非プロパティ header を property と誤分類しないようにした。ヘッダと本体の間に複数行またがるブロックコメントが挟まる場合も同じ行スキャン経路で正しく処理される。`ReferenceExtractor.FindInnermostContainer` は `property` も `function` / `class` / `namespace` と並ぶ container kind として受け入れるようにした。これにより `callers ` / `impact_analysis` と `inspect` / `analyze_symbol` に現れる caller 行が、`public int Wrap() => Compute();`、`public int Wrap => Compute();`、K&R の `public int Wrap { get { return Compute(); } }`、`public int Wrap {` / ` get { return Compute(); }` / `}`(同一行 bare `{` + 次行 accessor)、Allman の `public int Wrap` / `{` / ` get { return Compute(); }` / `}`、次行に ` => Compute();` を置く multi-line 式本体プロパティ、さらにヘッダと本体の間に複数行のブロックコメントを挟む同種の形まで、外側クラスの 1 行にまとめてしまわず、各 caller が固有の `caller_kind`(`function` または `property`)と `caller_name` を持つ別行として現れる。シンボル本体範囲、3 形すべての block-bodied property 抽出と、本体が accessor でない bare `{` の誤分類防止ガード、accessor attribute / visibility 修飾子付き accessor 行、式本体メソッド / プロパティ / multi-line `=> expr;` / K&R block-bodied accessor / 同一行 bare `{` + 次行 accessor / Allman / multi-line 式本体プロパティの参照属性、Allman と multi-line 式本体プロパティのブロックコメント挟み込み版、既存の ternary-continuation caller テスト、K&R 形(既存テストでカバー)、bare `{` + 次行 accessor 形、Allman block-body 形、multi-line 式本体プロパティ形、そしてブロックコメント挟み込みの両形の CLI `callers` end-to-end 検証まで押さえる回帰テストを追加。対象: `src/CodeIndex/Indexer/SymbolExtractor.cs`、`src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/SymbolExtractorTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`、`tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`。Closes #233。 - **C# 13 の partial property がシンボル抽出から欠落しないよう修正 (#228)** — C# extractor が property の `partial` 修飾子を受け付け、さらに複数行 header を先読みするようになった。これにより brace-on-next-line の宣言、block-bodied な実装、複数行の expression-bodied property、`partial override` 形が `symbols`、`definition`、`outline`、`hotspots` などのシンボル系コマンドから消えなくなった。複数行 declaration-style、block-bodied implementation、複数行 `partial override` property を押さえる回帰テストも追加。対象: `src/CodeIndex/Indexer/SymbolExtractor.cs`、`tests/CodeIndex.Tests/SymbolExtractorTests.cs`。Closes #228。 - **C# の演算子・変換演算子・インデクサが検索しやすいシンボル名で保存されるよう修正 (#213)** — `SymbolExtractor` が C# の演算子オーバーロードを `operator +` や `operator checked +` のような名前で保持し、`implicit` / `explicit` 変換演算子は `implicit operator decimal`、`explicit operator Money`、`explicit operator Dictionary`、`explicit operator (int whole, int cents)` のような exact name で保持してコンストラクタと衝突しないようにし、インデクサはキーワードの `this` ではなく CLR 互換の `Item` に正規化するようにした。tuple / generic target type は軽量な型スキャナと空白正規化を通し、generic・array・nested tuple・nullable type の直後に named tuple element が来る場合に加えて、`unsafe` pointer target や function-pointer target type も含めて、exact-name query が source の空白スタイルに依存しない。さらに `codeindex_meta` に C# symbol-name contract version を保持し、canonical name の変更が入ったときは通常の `cdidx index .` でも unchanged な C# ファイルを 1 回だけ自動再抽出して `--rebuild` なしで既存 DB を更新できるようにし、`status` でも `csharp_symbol_name_ready` を返して stale な operator/indexer exact-name trust を可視化したうえで、exact-name query でも silent false negative ではなく stale canonical-name の縮退を返し、`status --db ...` の修復案内も診断対象 DB 自体を指すようにし、MCP の exact-name trust metadata も query scope に従って無関係な言語や path を誤って縮退扱いしないようにした。extractor / DB query / CLI query / MCP query / status / index-upgrade の回帰テストを追加し、開発者ガイドと README も新しい命名規約に合わせて更新した。対象: `src/CodeIndex/Indexer/SymbolExtractor.cs`, `src/CodeIndex/Cli/IndexCommandRunner.cs`, `src/CodeIndex/Cli/QueryCommandRunner.cs`, `src/CodeIndex/Database/DbContext.cs`, `src/CodeIndex/Database/DbReader.cs`, `src/CodeIndex/Database/DbWriter.cs`, `src/CodeIndex/Models/QueryResults.cs`, `src/CodeIndex/Mcp/McpToolHandlers.cs`, `tests/CodeIndex.Tests/SymbolExtractorTests.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`, `tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `tests/CodeIndex.Tests/IndexCommandRunnerTests.cs`, `DEVELOPER_GUIDE.md`, `README.md`。Closes #213。 diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index af724e9a72..56eb9e8158 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -130,7 +130,7 @@ symbol_references ( id INTEGER PRIMARY KEY AUTOINCREMENT, file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, symbol_name TEXT, -- referenced symbol name - reference_kind TEXT, -- "call", "instantiate", ... + reference_kind TEXT, -- "call", "instantiate", "subscribe", "attribute", "annotation" line INTEGER, -- 1-based line number column_number INTEGER, -- 1-based column number context TEXT, -- trimmed source line @@ -1149,7 +1149,7 @@ symbol_references ( id INTEGER PRIMARY KEY AUTOINCREMENT, file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, symbol_name TEXT, -- 参照先シンボル名 - reference_kind TEXT, -- "call", "instantiate" など + reference_kind TEXT, -- "call", "instantiate", "subscribe", "attribute", "annotation" line INTEGER, -- 1始まりの行番号 column_number INTEGER, -- 1始まりの列番号 context TEXT, -- trim済みソース行 diff --git a/src/CodeIndex/Cli/ConsoleUi.cs b/src/CodeIndex/Cli/ConsoleUi.cs index 8abaca8ac4..2129ada457 100644 --- a/src/CodeIndex/Cli/ConsoleUi.cs +++ b/src/CodeIndex/Cli/ConsoleUi.cs @@ -388,7 +388,7 @@ public static void PrintUsage(bool showBanner = true) Console.WriteLine(" --exact Backward-compatible shorthand. Prefer --exact-substring for search, keep --exact for find, and prefer --exact-name for symbols/definition/references/callers/callees/inspect."); Console.WriteLine(" --exact-substring Search only: case-sensitive exact substring (no FTS5)"); Console.WriteLine(" --exact-name symbols/definition/references/callers/callees/inspect: NFKC + Unicode CaseFold exact name match (legacy/stale-fold DBs fall back to ASCII NOCASE; use `cdidx backfill-fold` or check `status --json` fold_ready)"); - Console.WriteLine(" --kind definition/symbols/hotspots/unused: symbol kind; references/callers/callees: reference kind (call/instantiate/subscribe); validate: issue kind"); + Console.WriteLine(" --kind definition/symbols/hotspots/unused: symbol kind; references/callers/callees: reference kind (call/instantiate/subscribe/attribute/annotation); validate: issue kind"); Console.WriteLine(" --count Count only; search/definition/references/callers/callees/symbols/files/find/unused ignore --limit, impact/hotspots still use visible page counts"); Console.WriteLine(" --since Filter to files modified since this timestamp (ISO 8601)"); Console.WriteLine(" --depth Max BFS depth for impact analysis (default: 5)"); diff --git a/src/CodeIndex/Cli/QueryCommandRunner.cs b/src/CodeIndex/Cli/QueryCommandRunner.cs index 93f039f880..da433be4a0 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.cs @@ -2956,7 +2956,7 @@ private static void WriteLangHint(string? lang, DbReader reader) private static readonly string[] AllValidKinds = ["class", "delegate", "enum", "event", "function", "import", "interface", "namespace", "property", "struct"]; private static readonly string[] AllValidReferenceKinds = - ["call", "instantiate", "subscribe"]; + ["annotation", "attribute", "call", "instantiate", "subscribe"]; private static void WriteKindHint(string? kind, DbReader reader) { diff --git a/src/CodeIndex/Database/DbReader.cs b/src/CodeIndex/Database/DbReader.cs index 9eede17217..058af308d9 100644 --- a/src/CodeIndex/Database/DbReader.cs +++ b/src/CodeIndex/Database/DbReader.cs @@ -63,6 +63,12 @@ THEN 2 ELSE 0 END"; private const string InvokeReferenceKindsSql = "('call', 'instantiate')"; + // Reference kinds that participate in the call-graph (callers/callees/hotspots). Metadata + // kinds such as `attribute` / `annotation` are excluded so they do not inflate the graph + // with non-call edges (issue #293). + // call-graph (callers/callees/hotspots) に参加する reference kind。`attribute` / `annotation` + // のようなメタデータ kind は非呼び出しエッジなのでここから除外する (issue #293)。 + internal const string CallGraphReferenceKindsSql = "('call', 'instantiate', 'subscribe')"; /// /// Visibility ranking: public symbols first, then protected, internal, private, unknown last. @@ -872,6 +878,7 @@ WITH logical_references AS ( FROM symbol_references r JOIN files f ON r.file_id = f.id WHERE r.container_name IS NOT NULL + AND r.reference_kind IN {CallGraphReferenceKindsSql} AND {BuildGraphSupportedLanguagePredicate(cmd, "f", "graphLang")}" : @" SELECT f.path, f.lang, r.container_kind, r.container_name, r.symbol_name, @@ -962,6 +969,8 @@ FROM symbol_references r if (referenceKind != null) groupedSql += " AND r.reference_kind = @referenceKind"; + else + groupedSql += $" AND r.reference_kind IN {CallGraphReferenceKindsSql}"; if (exact && _foldReady) groupedSql += " AND r.symbol_name_folded = @query"; else if (exact) @@ -1011,6 +1020,8 @@ FROM symbol_references r if (referenceKind != null) groupedSql += " AND r.reference_kind = @referenceKind"; + else + groupedSql += $" AND r.reference_kind IN {CallGraphReferenceKindsSql}"; if (exact && _foldReady) groupedSql += " AND r.symbol_name_folded = @query"; else if (exact) @@ -1058,6 +1069,7 @@ WITH logical_references AS ( FROM symbol_references r JOIN files f ON r.file_id = f.id WHERE r.container_name IS NOT NULL + AND r.reference_kind IN {CallGraphReferenceKindsSql} AND {BuildGraphSupportedLanguagePredicate(cmd, "f", "graphLang")}" : @" SELECT f.path, f.lang, r.container_kind, r.container_name, r.symbol_name, @@ -1152,6 +1164,8 @@ FROM symbol_references r if (referenceKind != null) groupedSql += " AND r.reference_kind = @referenceKind"; + else + groupedSql += $" AND r.reference_kind IN {CallGraphReferenceKindsSql}"; if (exact && _foldReady) groupedSql += " AND r.container_name_folded = @query"; else if (exact) @@ -1204,6 +1218,8 @@ FROM symbol_references r if (referenceKind != null) groupedSql += " AND r.reference_kind = @referenceKind"; + else + groupedSql += $" AND r.reference_kind IN {CallGraphReferenceKindsSql}"; if (exact && _foldReady) groupedSql += " AND r.container_name_folded = @query"; else if (exact) diff --git a/src/CodeIndex/Database/DbSymbolReader.cs b/src/CodeIndex/Database/DbSymbolReader.cs index 504fef3173..aa4a013bf4 100644 --- a/src/CodeIndex/Database/DbSymbolReader.cs +++ b/src/CodeIndex/Database/DbSymbolReader.cs @@ -871,6 +871,7 @@ logical_references AS ( " + GetLogicalReferenceKindSql("sr.reference_kind") + @" AS logical_reference_kind FROM symbol_references sr JOIN files rf ON rf.id = sr.file_id + WHERE sr.reference_kind IN " + CallGraphReferenceKindsSql + @" GROUP BY rf.lang, sr.file_id, sr.symbol_name, sr.line, sr.column_number, logical_reference_kind ), global_reference_counts AS ( @@ -1084,6 +1085,7 @@ logical_references AS ( " + GetLogicalReferenceKindSql("sr.reference_kind") + @" AS logical_reference_kind FROM symbol_references sr JOIN files rf ON rf.id = sr.file_id + WHERE sr.reference_kind IN " + CallGraphReferenceKindsSql + @" GROUP BY rf.lang, sr.file_id, sr.symbol_name, sr.line, sr.column_number, logical_reference_kind ), global_reference_counts AS ( diff --git a/src/CodeIndex/Indexer/ReferenceExtractor.cs b/src/CodeIndex/Indexer/ReferenceExtractor.cs index 9f2032dd1b..15acff3671 100644 --- a/src/CodeIndex/Indexer/ReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/ReferenceExtractor.cs @@ -132,6 +132,22 @@ public static class ReferenceExtractor // C# イベント購読・解除: Click += OnClick — LHS と RHS の両方が PascalCase 識別子のみ private static readonly Regex EventSubscriptionRegex = new(@"(?[A-Z]\w*)\s*[+-]=\s*(?:new\s+)?[A-Z]\w*", RegexOptions.Compiled); + // C# targeted-attribute prefixes (e.g. [return: NotNull], [assembly: InternalsVisibleTo(...)]). + // C# のターゲット付き属性の prefix。 + private static readonly HashSet CSharpAttributeTargets = new(StringComparer.Ordinal) + { + "return", "param", "method", "type", "field", "property", "event", "assembly", "module", + }; + + // Languages whose `@Decorator(args)` / `@Annotation(args)` syntax should produce + // `annotation` reference rows rather than `call` rows (issue #293). + // `@Decorator(args)` / `@Annotation(args)` 構文を `call` ではなく `annotation` として + // 記録すべき言語 (issue #293)。 + private static readonly HashSet AnnotationLanguages = new(StringComparer.Ordinal) + { + "java", "kotlin", "scala", "typescript", + }; + public static IReadOnlyCollection GetSupportedLanguages() => SupportedLanguages; public static bool SupportsLanguage(string? lang) => @@ -211,7 +227,8 @@ public static List Extract(long fileId, string? lang, string co foreach (Match match in CallRegex.Matches(preparedLine)) { var name = match.Groups["name"].Value; - if (IsConstructorCallName(language, preparedLine, match.Groups["name"].Index)) + var nameIndex = match.Groups["name"].Index; + if (IsConstructorCallName(language, preparedLine, nameIndex)) { AddReference(references, seen, fileId, match, "instantiate", context, lineNumber, container); continue; @@ -221,7 +238,12 @@ public static List Extract(long fileId, string? lang, string co if (definitionNames != null && definitionNames.Contains(name)) continue; - AddReference(references, seen, fileId, match, "call", context, lineNumber, container); + // issue #293: reclassify C# attribute / Java/Kotlin/Scala/TypeScript annotation + // usages with arguments so they do not pollute the call-graph as phantom `call` rows. + // issue #293: 引数付きの C# attribute と Java/Kotlin/Scala/TypeScript annotation 使用を + // `call` ではなく専用の種別に分類し、call-graph の phantom エッジを防ぐ。 + var metadataKind = TryClassifyMetadataReference(language, preparedLine, nameIndex); + AddReference(references, seen, fileId, match, metadataKind ?? "call", context, lineNumber, container); } } @@ -380,6 +402,117 @@ private static bool IsConstructorCallName(string language, string preparedLine, private static bool IsIdentifierChar(char ch) => char.IsLetterOrDigit(ch) || ch == '_'; + /// + /// Classify a call-looking identifier as an attribute/annotation when it appears inside + /// a C# `[...]` attribute list or is preceded by a Java-family `@` marker. Returns null + /// for ordinary method calls so the caller emits the default `call` reference kind. + /// 呼び出しに見える識別子を、C# の `[...]` 属性リスト内や Java 系 `@` 付き注釈に該当する + /// 場合に専用の reference kind へ分類する。通常の呼び出しは null を返して既定の `call` を維持する。 + /// + private static string? TryClassifyMetadataReference(string language, string preparedLine, int nameIndex) + { + var probe = nameIndex - 1; + while (probe >= 0 && char.IsWhiteSpace(preparedLine[probe])) + probe--; + if (probe < 0) + return null; + + if (language == "csharp") + return IsCSharpAttributeContext(preparedLine, probe) ? "attribute" : null; + + if (AnnotationLanguages.Contains(language)) + return IsAnnotationContext(preparedLine, probe) ? "annotation" : null; + + return null; + } + + private static bool IsCSharpAttributeContext(string line, int probe) + { + // Direct form: `[Foo(...)`. 直接形 `[Foo(...)`。 + if (line[probe] == '[') + return true; + + // Targeted form: `[target: Foo(...)]` — peek past the `target:` prefix. + // ターゲット指定形式 `[target: Foo(...)]` — `target:` 部分をスキップして `[` を探す。 + if (line[probe] == ':') + { + var j = probe - 1; + var idEnd = j; + while (j >= 0 && IsIdentifierChar(line[j])) + j--; + if (j + 1 <= idEnd) + { + var target = line[(j + 1)..(idEnd + 1)]; + if (CSharpAttributeTargets.Contains(target)) + { + var k = j; + while (k >= 0 && char.IsWhiteSpace(line[k])) + k--; + if (k >= 0 && line[k] == '[') + return true; + } + } + return false; + } + + // Comma-separated list: `[Foo("a"), Bar("b")]` — walk past prior attribute entries + // (and any balanced parens they contain) until an unbalanced opening `[` appears. + // 一行に並んだ複数属性 `[Foo("a"), Bar("b")]` では、先行する属性と対応する括弧を + // 読み飛ばして未対応の `[` に到達した場合にのみ属性として扱う。 + if (line[probe] == ',') + return ScanLeftForAttributeOpen(line, probe - 1); + + return false; + } + + private static bool ScanLeftForAttributeOpen(string line, int start) + { + var parenDepth = 0; + for (var i = start; i >= 0; i--) + { + var c = line[i]; + if (c == ')') + { + parenDepth++; + continue; + } + if (c == '(') + { + if (parenDepth == 0) + return false; + parenDepth--; + continue; + } + if (parenDepth > 0) + continue; + if (c == '[') + return true; + if (c == ']' || c == ';' || c == '{' || c == '}') + return false; + } + return false; + } + + private static bool IsAnnotationContext(string line, int probe) + { + // `@Annotation(args)` — direct marker. 直接 `@Annotation(args)` の場合。 + if (line[probe] == '@') + return true; + + // `@module.Annotation(args)` — walk past the dotted qualifier chain to the `@`. + // `@module.Annotation(args)` のようにドット区切り修飾子がある場合は、`.` と識別子を + // 順に遡って `@` を探す。 + while (probe >= 0 && line[probe] == '.') + { + probe--; + while (probe >= 0 && IsIdentifierChar(line[probe])) + probe--; + while (probe >= 0 && char.IsWhiteSpace(line[probe])) + probe--; + } + return probe >= 0 && line[probe] == '@'; + } + private static bool UsesHashComments(string lang) => lang is "python" or "ruby" or "php" or "elixir" or "r" or "powershell" or "makefile" or "terraform" or "dockerfile" or "protobuf"; diff --git a/tests/CodeIndex.Tests/ConsoleUiTests.cs b/tests/CodeIndex.Tests/ConsoleUiTests.cs index 08fdf865d5..8528276cf2 100644 --- a/tests/CodeIndex.Tests/ConsoleUiTests.cs +++ b/tests/CodeIndex.Tests/ConsoleUiTests.cs @@ -40,7 +40,7 @@ public void PrintUsage_WithoutBanner_HidesAsciiArtAndEasterEggFlags() Assert.Contains("cdidx find --path ", output); Assert.Contains("--exact-substring Search only: case-sensitive exact substring (no FTS5)", output); Assert.Contains("--exact-name symbols/definition/references/callers/callees/inspect: NFKC + Unicode CaseFold exact name match", output); - Assert.Contains("--kind definition/symbols/hotspots/unused: symbol kind; references/callers/callees: reference kind (call/instantiate/subscribe); validate: issue kind", output); + Assert.Contains("--kind definition/symbols/hotspots/unused: symbol kind; references/callers/callees: reference kind (call/instantiate/subscribe/attribute/annotation); validate: issue kind", output); Assert.Contains("--count Count only; search/definition/references/callers/callees/symbols/files/find/unused ignore --limit, impact/hotspots still use visible page counts", output); Assert.Contains("--commits [id ...] Update only files changed in the specified git commits (preferred after commits)", output); Assert.Contains("--files [path ...] Update only the specified files; old rename/delete paths are not purged unless also listed", output); diff --git a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs index fce7797c64..e471d8748e 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs @@ -614,8 +614,13 @@ public void Execute(Task task) } [Fact] - public void Extract_CsharpReturnTargetAttribute_CallIsNotDropped() + public void Extract_CsharpReturnTargetAttribute_ReferenceIsRecordedAsAttribute() { + // issue #293: C# attributes (including targeted `[return: ...]` form) are recorded as + // `attribute` kind — the reference must not be dropped, but also must not pollute the + // call-graph with a phantom `call` row. + // issue #293: C# 属性(`[return: ...]` の target 付きも含む)は `attribute` として + // 記録される。参照自体は失われないが、call-graph を `call` 行で汚染してはならない。 const string content = """ using System.Runtime.InteropServices; @@ -632,14 +637,15 @@ public class Foo var symbols = SymbolExtractor.Extract(1, "csharp", content); var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); - var marshalAsCalls = references - .Where(reference => reference.SymbolName == "MarshalAs" && reference.ReferenceKind == "call") + var marshalAsRefs = references + .Where(reference => reference.SymbolName == "MarshalAs") .OrderBy(reference => reference.Line) .ToList(); - Assert.Equal(2, marshalAsCalls.Count); - Assert.Equal([5, 8], marshalAsCalls.Select(reference => reference.Line).ToArray()); - Assert.All(marshalAsCalls, reference => Assert.Equal("Foo", reference.ContainerName)); + Assert.Equal(2, marshalAsRefs.Count); + Assert.Equal([5, 8], marshalAsRefs.Select(reference => reference.Line).ToArray()); + Assert.All(marshalAsRefs, reference => Assert.Equal("attribute", reference.ReferenceKind)); + Assert.All(marshalAsRefs, reference => Assert.Equal("Foo", reference.ContainerName)); } [Fact] @@ -1030,4 +1036,180 @@ public void Extract_Haskell_DetectsParenthesizedCalls() // Comments should not produce references / コメントは参照を生成しないこと Assert.DoesNotContain(references, r => r.SymbolName == "fakeCall"); } + + [Fact] + public void Extract_CsharpAttribute_ClassifiedAsAttribute() + { + // issue #293: `[Obsolete("msg")]` must produce an `attribute` reference, not a phantom `call`. + // issue #293: `[Obsolete("msg")]` は `call` ではなく `attribute` として記録されること。 + const string content = """ + using System; + [Obsolete("old")] + public class Old + { + } + """; + + var symbols = SymbolExtractor.Extract(1, "csharp", content); + var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); + + var obsolete = Assert.Single(references.Where(r => r.SymbolName == "Obsolete")); + Assert.Equal("attribute", obsolete.ReferenceKind); + } + + [Fact] + public void Extract_CsharpTargetedAttribute_ClassifiedAsAttribute() + { + // issue #293: `[return: NotNull("x")]` targeted attribute is classified as `attribute`. + // issue #293: `[return: NotNull("x")]` のターゲット付き属性も `attribute` になること。 + const string content = """ + public class C + { + [return: NotNull("x")] + public string M() => string.Empty; + } + """; + + var symbols = SymbolExtractor.Extract(1, "csharp", content); + var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); + + var notNull = Assert.Single(references.Where(r => r.SymbolName == "NotNull")); + Assert.Equal("attribute", notNull.ReferenceKind); + } + + [Fact] + public void Extract_CsharpMultipleAttributes_ClassifiedAsAttribute() + { + // issue #293: `[Foo("a"), Bar("b")]` — both entries in a comma-separated attribute list + // must be classified as `attribute`. + // issue #293: `[Foo("a"), Bar("b")]` のカンマ区切り属性リストは全て `attribute` になること。 + const string content = """ + [Foo("a"), Bar("b")] + public class C { } + """; + + var symbols = SymbolExtractor.Extract(1, "csharp", content); + var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); + + var foo = Assert.Single(references.Where(r => r.SymbolName == "Foo")); + var bar = Assert.Single(references.Where(r => r.SymbolName == "Bar")); + Assert.Equal("attribute", foo.ReferenceKind); + Assert.Equal("attribute", bar.ReferenceKind); + } + + [Fact] + public void Extract_CsharpAttributeWithNewArgument_InstantiateStaysInstantiate() + { + // Inside attribute arguments, `new Foo()` still counts as `instantiate` — only the + // attribute identifier itself is reclassified. + // 属性引数内の `new Foo()` は従来通り `instantiate`。属性名本体のみが再分類される。 + const string content = """ + [AttributeUsage(AttributeTargets.Class)] + public class C { } + """; + + var symbols = SymbolExtractor.Extract(1, "csharp", content); + var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); + + var au = Assert.Single(references.Where(r => r.SymbolName == "AttributeUsage")); + Assert.Equal("attribute", au.ReferenceKind); + } + + [Fact] + public void Extract_CsharpMethodBodyCall_StaysCall() + { + // Regression guard: ordinary method calls inside method bodies must still produce `call`, + // not be mistaken for attribute references due to unrelated `[` tokens on nearby lines. + // 回帰防止: メソッド本体内の通常呼び出しは、近くの `[` トークンの影響で `attribute` と + // 誤判定されず `call` のまま残ること。 + const string content = """ + public class C + { + public int Run() => Compute(42); + public int Compute(int x) => x; + } + """; + + var symbols = SymbolExtractor.Extract(1, "csharp", content); + var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); + + var compute = Assert.Single(references.Where(r => r.SymbolName == "Compute")); + Assert.Equal("call", compute.ReferenceKind); + } + + [Fact] + public void Extract_JavaAnnotation_ClassifiedAsAnnotation() + { + // issue #293: `@Deprecated(since="1.0")` must produce an `annotation` reference, not a phantom `call`. + // issue #293: `@Deprecated(since="1.0")` は `call` ではなく `annotation` として記録されること。 + const string content = """ + public class AnnotatedClass { + @Deprecated(since="1.0") + public void doWork() { } + } + """; + + var symbols = SymbolExtractor.Extract(1, "java", content); + var references = ReferenceExtractor.Extract(1, "java", content, symbols); + + var deprecated = Assert.Single(references.Where(r => r.SymbolName == "Deprecated")); + Assert.Equal("annotation", deprecated.ReferenceKind); + } + + [Fact] + public void Extract_JavaQualifiedAnnotation_ClassifiedAsAnnotation() + { + // issue #293: `@org.junit.Test(timeout=1000)` — dotted qualifier chain still resolves to `@`. + // issue #293: `@org.junit.Test(timeout=1000)` のような修飾付き注釈も `annotation` になること。 + const string content = """ + public class T { + @org.junit.Test(timeout=1000) + public void testIt() { } + } + """; + + var symbols = SymbolExtractor.Extract(1, "java", content); + var references = ReferenceExtractor.Extract(1, "java", content, symbols); + + var testAnno = Assert.Single(references.Where(r => r.SymbolName == "Test")); + Assert.Equal("annotation", testAnno.ReferenceKind); + } + + [Fact] + public void Extract_KotlinAnnotation_ClassifiedAsAnnotation() + { + // issue #293: Kotlin `@Deprecated("msg")` also emits `annotation`. + // issue #293: Kotlin の `@Deprecated("msg")` も `annotation` になること。 + const string content = """ + class K { + @Deprecated("msg") + fun old() {} + } + """; + + var symbols = SymbolExtractor.Extract(1, "kotlin", content); + var references = ReferenceExtractor.Extract(1, "kotlin", content, symbols); + + var deprecated = Assert.Single(references.Where(r => r.SymbolName == "Deprecated")); + Assert.Equal("annotation", deprecated.ReferenceKind); + } + + [Fact] + public void Extract_JavaMethodBodyCall_StaysCall() + { + // Regression guard: ordinary Java method call remains `call`, not `annotation`. + // 回帰防止: Java のメソッド本体内の通常呼び出しは `annotation` に誤判定されず `call` のまま。 + const string content = """ + public class J { + public int add(int a, int b) { return compute(a, b); } + public int compute(int a, int b) { return a + b; } + } + """; + + var symbols = SymbolExtractor.Extract(1, "java", content); + var references = ReferenceExtractor.Extract(1, "java", content, symbols); + + var compute = Assert.Single(references.Where(r => r.SymbolName == "compute")); + Assert.Equal("call", compute.ReferenceKind); + } } From da76116aa55c33ef6e0dba84fa18ea063084b1fa Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 17 Apr 2026 19:23:21 +0900 Subject: [PATCH 02/30] Fix #293 address review: collection-expr, Kotlin use-site target, README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to 2849c4d based on codex review feedback. 1. [blocking] C# 12 collection expressions `var xs = [Make(), Make()]`, `Consume([...])`, `return [...]`, and indexer accesses `arr[Compute()]` were misclassified as `attribute` references, which removed real call edges from the default `callers` / `callees` / `hotspots` / `impact` results. `IsCSharpAttributeContext` (and `ScanLeftForAttributeOpen`) now gate the `[` match through a new `IsCSharpAttributeOpenBracket` check that requires the `[` to sit at declaration position — only whitespace, `]`, `{`, `}`, or `;` may precede it on the same line. 2. [high] Kotlin use-site target annotations (`@field:Deprecated("msg")`, `@get:JsonName("x")`, `@file:JvmName("Foo")`, etc.) were still recorded as `call` because the probe lands on `:` rather than `@`. Added a `KotlinAnnotationTargets` set and a `:` branch in `IsAnnotationContext` that skips the target prefix and confirms the `@` marker before classifying as `annotation`. 3. [medium] README (English + Japanese) now documents the new default contract for `callers` / `callees` / `hotspots` / `impact` — only call-graph kinds (`call`, `instantiate`, `subscribe`) are visible by default; `--kind attribute` / `--kind annotation` opts in to metadata rows. The `--kind` column in both READMEs also lists the new kinds. Regression tests added: - `Extract_CsharpCollectionExpression_StaysCall` - `Extract_CsharpCollectionExpressionInArgument_StaysCall` - `Extract_CsharpIndexerAccess_StaysCall` - `Extract_KotlinFieldTargetAnnotation_ClassifiedAsAnnotation` - `Extract_KotlinGetTargetAnnotation_ClassifiedAsAnnotation` - `Extract_KotlinFileTargetAnnotation_ClassifiedAsAnnotation` All 1522 tests pass. Co-Authored-By: Claude Opus 4 --- CHANGELOG.md | 4 +- README.md | 12 +- src/CodeIndex/Indexer/ReferenceExtractor.cs | 62 +++++++- .../ReferenceExtractorTests.cs | 132 ++++++++++++++++++ 4 files changed, 199 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 133c1622e3..ef67434ed5 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/), - **Developer Guide now distinguishes SQLite's role from "zero dependencies" and labels release numbers as examples** — Clarified that cdidx remains a zero-configuration, single-file CLI with exactly one production dependency (`Microsoft.Data.Sqlite`), and made the release-workflow checklist explicitly state that `1.9.0` is only an example version to substitute during an actual release. Affected: `DEVELOPER_GUIDE.md`. #### Fixed -- **C# attribute and Java/Kotlin/Scala/TypeScript annotation usages no longer pollute the call-graph as phantom `call` rows (#293)** — `ReferenceExtractor` now reclassifies identifiers that appear inside C# `[...]` attribute lists (including direct `[Attr(args)]`, targeted `[return: Attr]` / `[assembly: Attr]` forms, and comma-separated `[Foo("a"), Bar("b")]` lists) as `attribute` references, and identifiers preceded by a Java-family `@` marker (optionally through a dotted qualifier chain such as `@org.junit.Test(args)`) as `annotation` references. The shared graph filter `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` is now applied to default-kind `callers`, `callees`, count queries, hotspot CTEs, and symbol-hotspot grouping, so `callers Obsolete`, `callers Deprecated`, `impact Conditional`, and `hotspots` no longer surface phantom caller edges or inflate hotspot counts for metadata-only references. `references --kind attribute` / `--kind annotation` still expose these rows for callers that explicitly want to inspect metadata usage, and the CLI help / `--kind` validator now lists the new reference kinds. Regression coverage pins the new classification for `[Obsolete("msg")]`, `[return: NotNull("x")]`, `[Foo("a"), Bar("b")]`, `[AttributeUsage(...)]`, Java `@Deprecated(since="1.0")`, qualified `@org.junit.Test(...)`, Kotlin `@Deprecated("msg")`, plus guards that ordinary method bodies still emit `call`. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Database/DbReader.cs`, `src/CodeIndex/Database/DbSymbolReader.cs`, `src/CodeIndex/Cli/QueryCommandRunner.cs`, `src/CodeIndex/Cli/ConsoleUi.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`, `tests/CodeIndex.Tests/ConsoleUiTests.cs`. Closes #293. +- **C# attribute and Java/Kotlin/Scala/TypeScript annotation usages no longer pollute the call-graph as phantom `call` rows (#293)** — `ReferenceExtractor` now reclassifies identifiers that appear inside C# `[...]` attribute lists (including direct `[Attr(args)]`, targeted `[return: Attr]` / `[assembly: Attr]` forms, and comma-separated `[Foo("a"), Bar("b")]` lists) as `attribute` references, and identifiers preceded by a Java-family `@` marker (optionally through a dotted qualifier chain such as `@org.junit.Test(args)`, or through a Kotlin use-site target such as `@field:Deprecated("msg")` / `@get:JsonName("x")` / `@file:JvmName("Foo")`) as `annotation` references. Attribute `[` detection is gated by a declaration-position guard so C# 12 collection expressions (`var xs = [Make(), Make()]`, `Consume([Make(), Make()])`, `return [Make(), Make()]`) and indexer accesses (`arr[Compute()]`) keep their inner calls as `call` instead of being misclassified as metadata. The shared graph filter `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` is applied to default-kind `callers`, `callees`, count queries, hotspot CTEs, and symbol-hotspot grouping, so `callers Obsolete`, `callers Deprecated`, `impact Conditional`, and `hotspots` no longer surface phantom caller edges or inflate hotspot counts for metadata-only references. `references` defaults keep every indexed reference kind visible so metadata usages remain inspectable; `references --kind attribute` / `--kind annotation` (and the MCP tool equivalents) explicitly surface metadata rows, and the CLI help / `--kind` validator / README now list the new reference kinds and document the new default contract for `callers` / `callees` / `hotspots` / `impact`. Regression coverage pins the new classification for `[Obsolete("msg")]`, `[return: NotNull("x")]`, `[Foo("a"), Bar("b")]`, `[AttributeUsage(...)]`, Java `@Deprecated(since="1.0")`, qualified `@org.junit.Test(...)`, Kotlin `@Deprecated("msg")`, Kotlin `@field:Deprecated` / `@get:JsonName` / `@file:JvmName`, ordinary method bodies still emitting `call`, and C# collection-expression / indexer call sites staying `call`. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Database/DbReader.cs`, `src/CodeIndex/Database/DbSymbolReader.cs`, `src/CodeIndex/Cli/QueryCommandRunner.cs`, `src/CodeIndex/Cli/ConsoleUi.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`, `tests/CodeIndex.Tests/ConsoleUiTests.cs`, `README.md`, `DEVELOPER_GUIDE.md`. Closes #293. - **C# expression-bodied and block-bodied property callers attribute to the individual method/property instead of the enclosing class (#233)** — `SymbolExtractor` now detects `=> expr;` at top level inside `FindCSharpBraceRange` and assigns a body range covering the declaration line through the terminating `;` (including the multi-line form where `=>` is placed on a following indented line, both for methods and for expression-bodied properties). The block-bodied property scanner now also merges the common Microsoft-style `public int Wrap {` + next-line `get { ... }` shape with its accessor line so that form is recognized alongside the existing K&R (`public int Wrap { get { ... } }`) and Allman (`public int Wrap` then `{` on the next line) shapes, without misclassifying `public class X {` or other non-property headers. Block comments spanning several lines between the header and the body are handled correctly by the same line-scanning path. `ReferenceExtractor.FindInnermostContainer` now accepts `property` as a container kind alongside `function` / `class` / `namespace`. As a result `callers `, `impact_analysis`, and the caller rows surfaced by `inspect` / `analyze_symbol` no longer collapse `public int Wrap() => Compute();`, `public int Wrap => Compute();`, the block-bodied `public int Wrap { get { return Compute(); } }` (K&R), `public int Wrap {` / ` get { return Compute(); }` / `}` (same-line brace with next-line accessor), the Allman-style `public int Wrap` / `{` / ` get { return Compute(); }` / `}`, the multi-line expression-bodied `public int Wrap` / ` => Compute();`, or the same shapes with multi-line block comments between header and body into a single enclosing-class row — each caller now appears on its own row with the correct `caller_kind` (`function` or `property`) and `caller_name`. Regression coverage spans symbol body ranges, all three block-bodied property shapes plus a misclassification guard for bare-brace lines whose body is not an accessor, accessor-attribute and visibility-prefixed accessor lines, reference attribution for expression-bodied methods / properties / multi-line `=> expr;` / K&R block-bodied accessor / bare-brace-same-line accessor-next-line / Allman-style / multi-line expression-bodied property forms, the block-comment-between-header-and-body variants for both Allman and multi-line expression-bodied properties, the previously-encoded ternary-continuation caller test, and end-to-end `callers` assertions through the CLI for the K&R shape (covered by existing property tests), the bare-brace-same-line shape, the Allman block-body shape, the multi-line expression-bodied shape, and both block-comment-separated variants. Affected: `src/CodeIndex/Indexer/SymbolExtractor.cs`, `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/SymbolExtractorTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`, `tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`. Closes #233. - **C# 13 partial properties now participate in symbol extraction (#228)** — The C# extractor now accepts the `partial` modifier on properties and looks ahead across multiline headers, so brace-on-next-line declarations, block-bodied implementations, multiline expression-bodied properties, and `partial override` forms no longer disappear from `symbols`, `definition`, `outline`, `hotspots`, and related symbol-driven commands. Added regression coverage for multiline declaration-style, block-bodied implementation, and multiline `partial override` properties. Affected: `src/CodeIndex/Indexer/SymbolExtractor.cs`, `tests/CodeIndex.Tests/SymbolExtractorTests.cs`. Closes #228. - **C# operators, conversion operators, and indexers now use navigable symbol names (#213)** — `SymbolExtractor` now stores overloads as names like `operator +` and `operator checked +`, stores `implicit` / `explicit` conversion operators as exact names like `implicit operator decimal`, `explicit operator Money`, `explicit operator Dictionary`, and `explicit operator (int whole, int cents)` so they no longer collide with constructors, and normalizes C# indexers from the keyword `this` to the CLR-style name `Item`. Tuple/generic conversion target types now go through a lightweight type scanner and canonical whitespace normalization, including named tuple elements after generic, array, nested-tuple, nullable type tokens, `unsafe` pointer targets, and function-pointer target types, so exact-name queries do not depend on the source file's spacing style. Incremental `cdidx index .` now tracks a C# symbol-name contract version in `codeindex_meta` and automatically re-extracts unchanged C# files once when a canonical-name upgrade lands, so existing DBs pick up the new exact names without requiring `--rebuild`; `status` also exposes `csharp_symbol_name_ready`, exact-name queries surface stale canonical-name degradation instead of silent false negatives, `status --db ...` now prints a repair command that targets the diagnosed DB instead of the caller's default workspace DB, and MCP exact-name trust metadata now honors the active query scope instead of degrading unrelated languages or paths. Added regression coverage in extractor, DB query, CLI query, MCP query, status, and index-upgrade tests, and updated the developer guide and README to document the naming convention. Affected: `src/CodeIndex/Indexer/SymbolExtractor.cs`, `src/CodeIndex/Cli/IndexCommandRunner.cs`, `src/CodeIndex/Cli/QueryCommandRunner.cs`, `src/CodeIndex/Database/DbContext.cs`, `src/CodeIndex/Database/DbReader.cs`, `src/CodeIndex/Database/DbWriter.cs`, `src/CodeIndex/Models/QueryResults.cs`, `src/CodeIndex/Mcp/McpToolHandlers.cs`, `tests/CodeIndex.Tests/SymbolExtractorTests.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`, `tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `tests/CodeIndex.Tests/IndexCommandRunnerTests.cs`, `DEVELOPER_GUIDE.md`, `README.md`. Closes #213. @@ -678,7 +678,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **DEVELOPER_GUIDE が SQLite の位置付けとリリース番号の例示を明確化** — cdidx が「設定ゼロ・単一ファイル・本番依存1個」の CLI であり、その唯一の本番依存が `Microsoft.Data.Sqlite` であることを明記した。あわせて、リリース手順のチェックリスト中に出てくる `1.9.0` は固定値ではなく例示であり、実際のリリース番号へ読み替える前提だと明示した。対象: `DEVELOPER_GUIDE.md`。 #### 修正 -- **C# 属性と Java/Kotlin/Scala/TypeScript の annotation 使用が phantom な `call` 行として call-graph を汚染しないよう修正 (#293)** — `ReferenceExtractor` が C# の `[...]` 属性リスト内(直接形 `[Attr(args)]`、target 付き `[return: Attr]` / `[assembly: Attr]`、カンマ区切り `[Foo("a"), Bar("b")]` を含む)に現れる識別子を `attribute` 参照として、Java 系 `@` マーカーが先行する識別子(`@org.junit.Test(args)` のようなドット修飾チェーン越しも含む)を `annotation` 参照として分類するようになった。共有 filter `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` を default-kind の `callers`・`callees`・count クエリ・hotspot CTE・symbol-hotspot グルーピングにも適用し、`callers Obsolete`・`callers Deprecated`・`impact Conditional`・`hotspots` で phantom caller edge や metadata のみの参照による件数膨張が発生しないようにした。`references --kind attribute` / `--kind annotation` では metadata 使用を明示的に確認したい呼び出し元向けに従来通り表示し、CLI help と `--kind` バリデータも新しい reference kind を列挙する。`[Obsolete("msg")]`・`[return: NotNull("x")]`・`[Foo("a"), Bar("b")]`・`[AttributeUsage(...)]`、Java `@Deprecated(since="1.0")`・修飾付き `@org.junit.Test(...)`、Kotlin `@Deprecated("msg")` の分類と、通常のメソッド本体が引き続き `call` を返すことを押さえる回帰テストを追加。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Database/DbReader.cs`、`src/CodeIndex/Database/DbSymbolReader.cs`、`src/CodeIndex/Cli/QueryCommandRunner.cs`、`src/CodeIndex/Cli/ConsoleUi.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`、`tests/CodeIndex.Tests/ConsoleUiTests.cs`。Closes #293。 +- **C# 属性と Java/Kotlin/Scala/TypeScript の annotation 使用が phantom な `call` 行として call-graph を汚染しないよう修正 (#293)** — `ReferenceExtractor` が C# の `[...]` 属性リスト内(直接形 `[Attr(args)]`、target 付き `[return: Attr]` / `[assembly: Attr]`、カンマ区切り `[Foo("a"), Bar("b")]` を含む)に現れる識別子を `attribute` 参照、Java 系 `@` マーカーが先行する識別子(`@org.junit.Test(args)` のようなドット修飾チェーン越し、および Kotlin の use-site target `@field:Deprecated("msg")` / `@get:JsonName("x")` / `@file:JvmName("Foo")` 越しを含む)を `annotation` 参照として分類するようになった。属性 `[` の判定には宣言位置ガードを入れ、C# 12 の collection expression(`var xs = [Make(), Make()]`、`Consume([Make(), Make()])`、`return [Make(), Make()]`)や indexer アクセス(`arr[Compute()]`)の内側の呼び出しは `call` のまま残り、metadata に誤分類されないようにした。共有 filter `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` を default-kind の `callers`・`callees`・count クエリ・hotspot CTE・symbol-hotspot グルーピングに適用し、`callers Obsolete`・`callers Deprecated`・`impact Conditional`・`hotspots` で phantom caller edge や metadata のみの参照による件数膨張が発生しないようにした。`references` の既定は全 reference kind を表示して metadata 使用も確認できる状態を保ちつつ、`references --kind attribute` / `--kind annotation`(と MCP ツール)で metadata 行を明示取得できる。CLI help・`--kind` バリデータ・README も新しい reference kind と `callers` / `callees` / `hotspots` / `impact` の新しい既定契約を反映する。`[Obsolete("msg")]`・`[return: NotNull("x")]`・`[Foo("a"), Bar("b")]`・`[AttributeUsage(...)]`、Java `@Deprecated(since="1.0")`・修飾付き `@org.junit.Test(...)`、Kotlin `@Deprecated("msg")`、Kotlin `@field:Deprecated` / `@get:JsonName` / `@file:JvmName` の分類、通常のメソッド本体が引き続き `call` を返すこと、C# collection expression や indexer アクセス内の呼び出しが `call` のままであることを押さえる回帰テストを追加。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Database/DbReader.cs`、`src/CodeIndex/Database/DbSymbolReader.cs`、`src/CodeIndex/Cli/QueryCommandRunner.cs`、`src/CodeIndex/Cli/ConsoleUi.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`、`tests/CodeIndex.Tests/ConsoleUiTests.cs`、`README.md`、`DEVELOPER_GUIDE.md`。Closes #293。 - **C# 式本体メンバーと block-bodied プロパティの caller が外側クラスではなく個別メソッド/プロパティに帰属するよう修正 (#233)** — `SymbolExtractor` の `FindCSharpBraceRange` がトップレベルの `=> expr;` を検出し、宣言行から終端 `;` までを本体範囲として扱うようになった(宣言行の次行にインデントして `=>` を置く multi-line 形にも、メソッドと式本体プロパティの双方で対応)。block-bodied プロパティの行結合ロジックも、`public int Wrap {` の次行に ` get { ... }` を置く Microsoft 標準形を accessor 行と結合するよう拡張し、既存の K&R (`public int Wrap { get { ... } }`) や Allman (`public int Wrap` の次行に `{`) と並ぶ 3 形を抽出対象にしつつ、`public class X {` のような非プロパティ header を property と誤分類しないようにした。ヘッダと本体の間に複数行またがるブロックコメントが挟まる場合も同じ行スキャン経路で正しく処理される。`ReferenceExtractor.FindInnermostContainer` は `property` も `function` / `class` / `namespace` と並ぶ container kind として受け入れるようにした。これにより `callers ` / `impact_analysis` と `inspect` / `analyze_symbol` に現れる caller 行が、`public int Wrap() => Compute();`、`public int Wrap => Compute();`、K&R の `public int Wrap { get { return Compute(); } }`、`public int Wrap {` / ` get { return Compute(); }` / `}`(同一行 bare `{` + 次行 accessor)、Allman の `public int Wrap` / `{` / ` get { return Compute(); }` / `}`、次行に ` => Compute();` を置く multi-line 式本体プロパティ、さらにヘッダと本体の間に複数行のブロックコメントを挟む同種の形まで、外側クラスの 1 行にまとめてしまわず、各 caller が固有の `caller_kind`(`function` または `property`)と `caller_name` を持つ別行として現れる。シンボル本体範囲、3 形すべての block-bodied property 抽出と、本体が accessor でない bare `{` の誤分類防止ガード、accessor attribute / visibility 修飾子付き accessor 行、式本体メソッド / プロパティ / multi-line `=> expr;` / K&R block-bodied accessor / 同一行 bare `{` + 次行 accessor / Allman / multi-line 式本体プロパティの参照属性、Allman と multi-line 式本体プロパティのブロックコメント挟み込み版、既存の ternary-continuation caller テスト、K&R 形(既存テストでカバー)、bare `{` + 次行 accessor 形、Allman block-body 形、multi-line 式本体プロパティ形、そしてブロックコメント挟み込みの両形の CLI `callers` end-to-end 検証まで押さえる回帰テストを追加。対象: `src/CodeIndex/Indexer/SymbolExtractor.cs`、`src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/SymbolExtractorTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`、`tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`。Closes #233。 - **C# 13 の partial property がシンボル抽出から欠落しないよう修正 (#228)** — C# extractor が property の `partial` 修飾子を受け付け、さらに複数行 header を先読みするようになった。これにより brace-on-next-line の宣言、block-bodied な実装、複数行の expression-bodied property、`partial override` 形が `symbols`、`definition`、`outline`、`hotspots` などのシンボル系コマンドから消えなくなった。複数行 declaration-style、block-bodied implementation、複数行 `partial override` property を押さえる回帰テストも追加。対象: `src/CodeIndex/Indexer/SymbolExtractor.cs`、`tests/CodeIndex.Tests/SymbolExtractorTests.cs`。Closes #228。 - **C# の演算子・変換演算子・インデクサが検索しやすいシンボル名で保存されるよう修正 (#213)** — `SymbolExtractor` が C# の演算子オーバーロードを `operator +` や `operator checked +` のような名前で保持し、`implicit` / `explicit` 変換演算子は `implicit operator decimal`、`explicit operator Money`、`explicit operator Dictionary`、`explicit operator (int whole, int cents)` のような exact name で保持してコンストラクタと衝突しないようにし、インデクサはキーワードの `this` ではなく CLR 互換の `Item` に正規化するようにした。tuple / generic target type は軽量な型スキャナと空白正規化を通し、generic・array・nested tuple・nullable type の直後に named tuple element が来る場合に加えて、`unsafe` pointer target や function-pointer target type も含めて、exact-name query が source の空白スタイルに依存しない。さらに `codeindex_meta` に C# symbol-name contract version を保持し、canonical name の変更が入ったときは通常の `cdidx index .` でも unchanged な C# ファイルを 1 回だけ自動再抽出して `--rebuild` なしで既存 DB を更新できるようにし、`status` でも `csharp_symbol_name_ready` を返して stale な operator/indexer exact-name trust を可視化したうえで、exact-name query でも silent false negative ではなく stale canonical-name の縮退を返し、`status --db ...` の修復案内も診断対象 DB 自体を指すようにし、MCP の exact-name trust metadata も query scope に従って無関係な言語や path を誤って縮退扱いしないようにした。extractor / DB query / CLI query / MCP query / status / index-upgrade の回帰テストを追加し、開発者ガイドと README も新しい命名規約に合わせて更新した。対象: `src/CodeIndex/Indexer/SymbolExtractor.cs`, `src/CodeIndex/Cli/IndexCommandRunner.cs`, `src/CodeIndex/Cli/QueryCommandRunner.cs`, `src/CodeIndex/Database/DbContext.cs`, `src/CodeIndex/Database/DbReader.cs`, `src/CodeIndex/Database/DbWriter.cs`, `src/CodeIndex/Models/QueryResults.cs`, `src/CodeIndex/Mcp/McpToolHandlers.cs`, `tests/CodeIndex.Tests/SymbolExtractorTests.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`, `tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `tests/CodeIndex.Tests/IndexCommandRunnerTests.cs`, `DEVELOPER_GUIDE.md`, `README.md`。Closes #213。 diff --git a/README.md b/README.md index 8385cc91e8..c678b42bf2 100644 --- a/README.md +++ b/README.md @@ -433,7 +433,7 @@ cdidx map --path src/ --exclude-tests --json | `--exact` | `search`, `find`, `symbols`, `definition`, `references`, `callers`, `callees`, `inspect` | Backward-compatible shorthand. Prefer `--exact-substring` for `search`, keep `--exact` for `find`, and prefer `--exact-name` for symbol / graph commands plus `inspect`. CLI JSON and MCP `structuredContent` expose `exact_index_available` / `degraded_reason`; MCP also keeps the legacy camelCase aliases `exactIndexAvailable` / `degradedReason` for backward compatibility. | | `--exact-substring` | `search` | Preferred explicit name for search exactness: case-sensitive exact substring (FTS5 bypassed). | | `--exact-name` | `symbols`, `definition`, `references`, `callers`, `callees`, `inspect` | Preferred explicit name for symbol-name exactness: NFKC + Unicode CaseFold exact equality (`Ä` / `ä`, `Run` / `Run`, ligatures, sharp-S, and Greek final sigma collapse). Unicode CaseFold remains locale-invariant, so Turkish dotted `İ` is still distinct from plain `i`. For C#, pass the canonical extracted name (`operator +`, `operator checked +`, `explicit operator Money`, `implicit operator decimal`, `Item`) rather than source keywords like `this` / `explicit`. Falls back to ASCII `COLLATE NOCASE` while the DB still contains stale fold metadata; prefer `cdidx backfill-fold`, or use a plain `cdidx index .` if it rewrites or purges every stale row, otherwise `--rebuild`. `status --json` exposes `fold_ready` and `csharp_symbol_name_ready` so AI clients can tell which path is active. When a read-only legacy DB is missing the fallback exact-match indexes, human-readable output warns and CLI JSON / MCP `structuredContent` expose degraded-state metadata. | -| `--kind ` | `definition`, `references`, `callers`, `callees`, `symbols`, `hotspots`, `unused`, `validate` | Filter by kind. `definition` / `symbols` / `hotspots` / `unused` use symbol kinds (`function`, `class`, `struct`, `interface`, `enum`, `property`, `event`, `delegate`, `namespace`, `import`); `references` / `callers` / `callees` use reference kinds (`call`, `instantiate`, `subscribe`), and omitting `--kind` keeps all indexed reference kinds visible while collapsing identical constructor `call` + `instantiate` rows at one physical site; `validate` uses issue kinds such as `bom` | +| `--kind ` | `definition`, `references`, `callers`, `callees`, `symbols`, `hotspots`, `unused`, `validate` | Filter by kind. `definition` / `symbols` / `hotspots` / `unused` use symbol kinds (`function`, `class`, `struct`, `interface`, `enum`, `property`, `event`, `delegate`, `namespace`, `import`); `references` / `callers` / `callees` use reference kinds (`call`, `instantiate`, `subscribe`, `attribute`, `annotation`). `references` defaults to every indexed reference kind so metadata usages remain visible, while `callers` / `callees` / `hotspots` / `impact` default to the call-graph kinds only (`call`, `instantiate`, `subscribe`) and exclude metadata edges (`attribute`, `annotation`); pass `--kind attribute` or `--kind annotation` to surface C# `[...]` attribute / Java-family `@Annotation(...)` usages explicitly. Identical constructor `call` + `instantiate` rows at one physical site still collapse; `validate` uses issue kinds such as `bom` | | `--body` | `definition`, `inspect` | Include reconstructed body content when the language extractor can infer the body range | | `--count` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `impact`, `unused`, `hotspots` | Return only counts. `search` / `definition` / `references` / `callers` / `callees` / `symbols` / `files` / `find` / `unused` ignore `--limit` and return authoritative totals; `impact` and `hotspots` still report the visible page count and may truncate with `--limit` (with `--json`: a single count object; commands that expose file counts add `files`) | | `--group-by-name` | `hotspots` | Collapse rows that share the same `(name, kind)` across files into one representative result while preserving `definition_sites` / `paths` metadata in JSON | @@ -810,8 +810,8 @@ Once configured, the AI can directly call these tools: | `search` | Full-text search across code chunks | | `definition` | Reconstruct a symbol declaration and optional body | | `references` | Find indexed references for supported languages; identical constructor `call` + `instantiate` rows collapse by default | -| `callers` | List callers for a named symbol in supported languages; `kind` filters by reference kind, and the default keeps all indexed kinds visible while collapsing identical constructor `call` + `instantiate` rows at one physical site | -| `callees` | List callees for a named symbol in supported languages; the default keeps all indexed kinds visible while collapsing identical constructor `call` + `instantiate` rows at one physical site | +| `callers` | List callers for a named symbol in supported languages; `kind` filters by reference kind. The default keeps call-graph kinds visible (`call`, `instantiate`, `subscribe`) while excluding metadata edges (`attribute`, `annotation`); pass `kind: "attribute"` or `kind: "annotation"` to surface those explicitly. Identical constructor `call` + `instantiate` rows at one physical site collapse. | +| `callees` | List callees for a named symbol in supported languages; the default keeps call-graph kinds visible (`call`, `instantiate`, `subscribe`) while excluding metadata edges (`attribute`, `annotation`). Identical constructor `call` + `instantiate` rows at one physical site collapse. | | `symbols` | Find functions, classes, interfaces, imports, and namespaces by name | | `files` | List indexed files | | `find_in_file` | Find literal substring matches inside known indexed files with line/column context | @@ -1304,7 +1304,7 @@ cdidx map --path src/ --exclude-tests --json | `--exact` | `search`, `find`, `symbols`, `definition`, `references`, `callers`, `callees`, `inspect` | 後方互換の短縮形。`search` では `--exact-substring`、`find` では `--exact` を使い、symbol / graph 系コマンドと `inspect` では `--exact-name` を推奨。CLI JSON と MCP `structuredContent` は `exact_index_available` / `degraded_reason` を返し、MCP では後方互換の camelCase alias も維持する。 | | `--exact-substring` | `search` | `search` 用の推奨 explicit alias。大文字小文字を区別する完全部分一致(FTS5 バイパス)。 | | `--exact-name` | `symbols`, `definition`, `references`, `callers`, `callees`, `inspect` | symbol-name exactness 用の推奨 explicit alias。NFKC + Unicode CaseFold による完全一致(`Ä` / `ä`、全角 `Run` / `Run`、合字、sharp-S、Greek final sigma を畳み込む)。Unicode CaseFold は locale-invariant のため、トルコ語の dotted `İ` は plain `i` と同一視しない。C# では `this` / `explicit` のような source keyword ではなく、抽出済みの canonical name(`operator +`、`operator checked +`、`explicit operator Money`、`implicit operator decimal`、`Item`)を渡す。DB に stale な fold metadata が残る間は ASCII `COLLATE NOCASE` に fallback するため、まず `cdidx backfill-fold`、または stale row を全置換できる通常の `cdidx index .`、それが無理なら `--rebuild` を使う(`status --json` の `fold_ready` と `csharp_symbol_name_ready` で判定)。read-only な旧DBに fallback exact-match index が無い場合は、人間向け出力が WARN を表示し、CLI JSON と MCP `structuredContent` が縮退メタデータを返す。 | -| `--kind ` | `definition`, `references`, `callers`, `callees`, `symbols`, `hotspots`, `unused`, `validate` | 種別でフィルタ。`definition` / `symbols` / `hotspots` / `unused` は symbol kind(`function`、`class`、`struct`、`interface`、`enum`、`property`、`event`、`delegate`、`namespace`、`import`)、`references` / `callers` / `callees` は reference kind(`call`、`instantiate`、`subscribe`)を使い、`--kind` 未指定時は全 reference kind を表示したまま同じ物理位置にある constructor の `call` + `instantiate` 重複行を集約する。`validate` は `bom` などの issue kind を使う | +| `--kind ` | `definition`, `references`, `callers`, `callees`, `symbols`, `hotspots`, `unused`, `validate` | 種別でフィルタ。`definition` / `symbols` / `hotspots` / `unused` は symbol kind(`function`、`class`、`struct`、`interface`、`enum`、`property`、`event`、`delegate`、`namespace`、`import`)、`references` / `callers` / `callees` は reference kind(`call`、`instantiate`、`subscribe`、`attribute`、`annotation`)を使う。`references` の既定は全 reference kind を表示して metadata 参照も見えるままにするが、`callers` / `callees` / `hotspots` / `impact` の既定は call-graph kind(`call`、`instantiate`、`subscribe`)のみで、`attribute` / `annotation` のような metadata edge は除外する。C# の `[...]` 属性や Java 系 `@Annotation(...)` を明示的に取りたいときは `--kind attribute` / `--kind annotation` を指定する。同じ物理位置にある constructor の `call` + `instantiate` 重複行は引き続き集約する。`validate` は `bom` などの issue kind を使う | | `--body` | `definition`, `inspect` | 言語抽出器が本体範囲を推論できる場合に本体内容も含める | | `--count` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `impact`, `unused`, `hotspots` | 件数だけを返す。`search` / `definition` / `references` / `callers` / `callees` / `symbols` / `files` / `find` / `unused` は `--limit` を無視した総件数を返し、`impact` と `hotspots` は visible page count のままで `--limit` によって切り詰められることがある(`--json` 併用時は単一の count オブジェクト。files 件数を出すコマンドは `files` も返す) | | `--group-by-name` | `hotspots` | ファイルをまたいで同じ `(name, kind)` を共有する行を代表1件に集約し、JSON では `definition_sites` / `paths` metadata を保持したまま返す | @@ -1680,8 +1680,8 @@ OpenAI Codex CLI (`codex.json` または `~/.codex/config.json`): | `search` | コードチャンクの全文検索 | | `definition` | シンボルの宣言と必要なら本体を再構成して取得 | | `references` | 対応言語でインデックス済み参照を検索。constructor site の `call` + `instantiate` 重複は既定で集約 | -| `callers` | 対応言語で指定シンボルの caller を列挙。`kind` は reference kind を指し、未指定時は全 reference kind を表示したまま同じ物理位置にある constructor の `call` + `instantiate` 重複を集約 | -| `callees` | 対応言語で指定シンボルの callee を列挙。未指定時は全 reference kind を表示したまま同じ物理位置にある constructor の `call` + `instantiate` 重複を集約 | +| `callers` | 対応言語で指定シンボルの caller を列挙。`kind` は reference kind を指し、既定では call-graph kind(`call`、`instantiate`、`subscribe`)のみを表示して `attribute` / `annotation` のような metadata edge は除外する。C# の `[...]` 属性や Java 系 `@Annotation(...)` を取りたいときは `kind: "attribute"` / `kind: "annotation"` を明示する。同じ物理位置にある constructor の `call` + `instantiate` 重複は集約する。 | +| `callees` | 対応言語で指定シンボルの callee を列挙。既定は call-graph kind(`call`、`instantiate`、`subscribe`)のみで、`attribute` / `annotation` のような metadata edge は除外する。同じ物理位置にある constructor の `call` + `instantiate` 重複は集約する。 | | `symbols` | 関数・クラス・インターフェース・import・namespace を名前で検索 | | `files` | インデックス済みファイル一覧 | | `find_in_file` | 既知のインデックス済みファイル内でリテラル部分文字列一致を行・列付きで検索 | diff --git a/src/CodeIndex/Indexer/ReferenceExtractor.cs b/src/CodeIndex/Indexer/ReferenceExtractor.cs index 15acff3671..a60037de4b 100644 --- a/src/CodeIndex/Indexer/ReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/ReferenceExtractor.cs @@ -148,6 +148,14 @@ public static class ReferenceExtractor "java", "kotlin", "scala", "typescript", }; + // Kotlin use-site target prefixes for annotations (e.g. `@field:Deprecated("msg")`, + // `@file:JvmName("Foo")`). Keep aligned with the Kotlin language spec use-site targets. + // Kotlin の use-site target 付き注釈用の接頭辞。 + private static readonly HashSet KotlinAnnotationTargets = new(StringComparer.Ordinal) + { + "field", "get", "set", "param", "setparam", "property", "receiver", "file", "delegate", "all", + }; + public static IReadOnlyCollection GetSupportedLanguages() => SupportedLanguages; public static bool SupportsLanguage(string? lang) => @@ -429,8 +437,12 @@ private static bool IsIdentifierChar(char ch) => private static bool IsCSharpAttributeContext(string line, int probe) { // Direct form: `[Foo(...)`. 直接形 `[Foo(...)`。 + // Must verify that this `[` sits at a declaration position, not inside a C# 12 + // collection expression such as `var xs = [Make(), Make()]`. + // この `[` が宣言位置(`var xs = [...]` のような C# 12 collection expression ではない) + // であることを必ず確認する必要がある。 if (line[probe] == '[') - return true; + return IsCSharpAttributeOpenBracket(line, probe); // Targeted form: `[target: Foo(...)]` — peek past the `target:` prefix. // ターゲット指定形式 `[target: Foo(...)]` — `target:` 部分をスキップして `[` を探す。 @@ -448,7 +460,7 @@ private static bool IsCSharpAttributeContext(string line, int probe) var k = j; while (k >= 0 && char.IsWhiteSpace(line[k])) k--; - if (k >= 0 && line[k] == '[') + if (k >= 0 && line[k] == '[' && IsCSharpAttributeOpenBracket(line, k)) return true; } } @@ -465,6 +477,25 @@ private static bool IsCSharpAttributeContext(string line, int probe) return false; } + /// + /// Return true when the `[` at is at a C# declaration + /// position rather than an expression position. Attribute `[` must be preceded on the + /// same line only by whitespace, another attribute list's closing `]`, or a scope/ + /// statement boundary (`{`, `}`, `;`). Any expression-context token (identifier, digit, + /// `=`, `,`, `(`, `:`, etc.) means this is a collection expression or indexer. + /// `[` が C# の宣言位置にあるか(collection expression や indexer ではないか)を判定する。 + /// + private static bool IsCSharpAttributeOpenBracket(string line, int bracketIndex) + { + var i = bracketIndex - 1; + while (i >= 0 && char.IsWhiteSpace(line[i])) + i--; + if (i < 0) + return true; + var c = line[i]; + return c == ']' || c == '{' || c == '}' || c == ';'; + } + private static bool ScanLeftForAttributeOpen(string line, int start) { var parenDepth = 0; @@ -486,7 +517,7 @@ private static bool ScanLeftForAttributeOpen(string line, int start) if (parenDepth > 0) continue; if (c == '[') - return true; + return IsCSharpAttributeOpenBracket(line, i); if (c == ']' || c == ';' || c == '{' || c == '}') return false; } @@ -499,6 +530,31 @@ private static bool IsAnnotationContext(string line, int probe) if (line[probe] == '@') return true; + // Kotlin use-site target: `@field:Deprecated("msg")` — skip the `target:` prefix + // and confirm an `@` marker precedes it. + // Kotlin の use-site target 付き注釈 `@field:Deprecated("msg")` では、`target:` を + // 読み飛ばして `@` を探す。 + if (line[probe] == ':') + { + var j = probe - 1; + var idEnd = j; + while (j >= 0 && IsIdentifierChar(line[j])) + j--; + if (j + 1 <= idEnd) + { + var target = line[(j + 1)..(idEnd + 1)]; + if (KotlinAnnotationTargets.Contains(target)) + { + var k = j; + while (k >= 0 && char.IsWhiteSpace(line[k])) + k--; + if (k >= 0 && line[k] == '@') + return true; + } + } + return false; + } + // `@module.Annotation(args)` — walk past the dotted qualifier chain to the `@`. // `@module.Annotation(args)` のようにドット区切り修飾子がある場合は、`.` と識別子を // 順に遡って `@` を探す。 diff --git a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs index e471d8748e..d8e969eb17 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs @@ -1212,4 +1212,136 @@ public class J { var compute = Assert.Single(references.Where(r => r.SymbolName == "compute")); Assert.Equal("call", compute.ReferenceKind); } + + [Fact] + public void Extract_CsharpCollectionExpression_StaysCall() + { + // issue #293 regression: C# 12 collection expressions `var xs = [Make(), Make()]` + // share the `[...]` syntax with attributes but must NOT be classified as `attribute`. + // issue #293 回帰防止: C# 12 collection expression `var xs = [Make(), Make()]` は + // 属性と同じ `[...]` 構文を共有するが `attribute` に誤分類してはならない。 + const string content = """ + public class C + { + public int Make() => 42; + public void Run() + { + var xs = [Make(), Make()]; + } + } + """; + + var symbols = SymbolExtractor.Extract(1, "csharp", content); + var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); + + var makeRefs = references.Where(r => r.SymbolName == "Make").ToList(); + Assert.Equal(2, makeRefs.Count); + Assert.All(makeRefs, r => Assert.Equal("call", r.ReferenceKind)); + Assert.All(makeRefs, r => Assert.Equal("Run", r.ContainerName)); + } + + [Fact] + public void Extract_CsharpCollectionExpressionInArgument_StaysCall() + { + // Collection expressions appearing as arguments, nested in other expressions, or + // after `return` must still classify inner calls as `call`, not `attribute`. + // 引数やネストされた式、`return` 後の collection expression 内の呼び出しは `call` のまま。 + const string content = """ + public class C + { + public int Make() => 42; + public int[] Wrap() => [Make(), Make()]; + public void Consume(int[] xs) { } + public void Run() + { + Consume([Make(), Make()]); + } + } + """; + + var symbols = SymbolExtractor.Extract(1, "csharp", content); + var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); + + var makeRefs = references.Where(r => r.SymbolName == "Make").ToList(); + Assert.Equal(4, makeRefs.Count); + Assert.All(makeRefs, r => Assert.Equal("call", r.ReferenceKind)); + } + + [Fact] + public void Extract_CsharpIndexerAccess_StaysCall() + { + // `arr[Compute()]` — `[` is preceded by an identifier, so it is an indexer, not an + // attribute, and the inner call must stay `call`. + // `arr[Compute()]` は indexer で、`[` の直前が識別子のため attribute 扱いにはしない。 + const string content = """ + public class C + { + public int Compute() => 0; + public int Read(int[] arr) => arr[Compute()]; + } + """; + + var symbols = SymbolExtractor.Extract(1, "csharp", content); + var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); + + var compute = Assert.Single(references.Where(r => r.SymbolName == "Compute")); + Assert.Equal("call", compute.ReferenceKind); + } + + [Fact] + public void Extract_KotlinFieldTargetAnnotation_ClassifiedAsAnnotation() + { + // issue #293 follow-up: Kotlin use-site target `@field:Deprecated("msg")` must be + // classified as `annotation`, not `call`. + // issue #293 補足: Kotlin の use-site target `@field:Deprecated("msg")` も `annotation`。 + const string content = """ + class Example { + @field:Deprecated("msg") + val value: Int = 0 + } + """; + + var symbols = SymbolExtractor.Extract(1, "kotlin", content); + var references = ReferenceExtractor.Extract(1, "kotlin", content, symbols); + + var deprecated = Assert.Single(references.Where(r => r.SymbolName == "Deprecated")); + Assert.Equal("annotation", deprecated.ReferenceKind); + } + + [Fact] + public void Extract_KotlinGetTargetAnnotation_ClassifiedAsAnnotation() + { + // Kotlin `@get:JsonName("x")` property getter target annotation. + // Kotlin の `@get:JsonName("x")` プロパティ getter 向け注釈も `annotation`。 + const string content = """ + class K { + @get:JsonName("x") + val x: Int = 0 + } + """; + + var symbols = SymbolExtractor.Extract(1, "kotlin", content); + var references = ReferenceExtractor.Extract(1, "kotlin", content, symbols); + + var jsonName = Assert.Single(references.Where(r => r.SymbolName == "JsonName")); + Assert.Equal("annotation", jsonName.ReferenceKind); + } + + [Fact] + public void Extract_KotlinFileTargetAnnotation_ClassifiedAsAnnotation() + { + // Kotlin `@file:JvmName("Foo")` file-level target annotation. + // Kotlin の `@file:JvmName("Foo")` ファイル単位注釈も `annotation`。 + const string content = """ + @file:JvmName("Foo") + + package example + """; + + var symbols = SymbolExtractor.Extract(1, "kotlin", content); + var references = ReferenceExtractor.Extract(1, "kotlin", content, symbols); + + var jvmName = Assert.Single(references.Where(r => r.SymbolName == "JvmName")); + Assert.Equal("annotation", jvmName.ReferenceKind); + } } From e97623f462c6e179bc6cbfe1f2cb41ef371bd96f Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 17 Apr 2026 19:40:43 +0900 Subject: [PATCH 03/30] Fix #293 address review: chained indexer walk-back, qualified use-site target - IsCSharpAttributeOpenBracket: when preceded by `]`, walk back to the matching `[` (skipping balanced parens) and re-check its declaration position. Prevents arr[Compute()][Compute()] and matrix[Row()][Col()] from misclassifying the second inner call as `attribute` while still recognizing chained attribute lists like [A("x")][B("y")]. - IsAnnotationContext: unwind dotted qualifier chain first, then accept either `@` or Kotlin use-site target `:` + `@`. Adds support for @field:com.example.Deprecated("msg") and @get:com.fasterxml.jackson.annotation.JsonProperty("x") while keeping @org.junit.Test(...) classified as annotation. - Regression tests cover chained indexer, matrix indexer, chained attribute lists, qualified Kotlin use-site targets, and qualified annotation without use-site target. Co-Authored-By: Claude Opus 4 --- CHANGELOG.md | 4 +- src/CodeIndex/Indexer/ReferenceExtractor.cs | 98 ++++++++++--- .../ReferenceExtractorTests.cs | 135 ++++++++++++++++++ 3 files changed, 217 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef67434ed5..d7f5a3ce5c 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/), - **Developer Guide now distinguishes SQLite's role from "zero dependencies" and labels release numbers as examples** — Clarified that cdidx remains a zero-configuration, single-file CLI with exactly one production dependency (`Microsoft.Data.Sqlite`), and made the release-workflow checklist explicitly state that `1.9.0` is only an example version to substitute during an actual release. Affected: `DEVELOPER_GUIDE.md`. #### Fixed -- **C# attribute and Java/Kotlin/Scala/TypeScript annotation usages no longer pollute the call-graph as phantom `call` rows (#293)** — `ReferenceExtractor` now reclassifies identifiers that appear inside C# `[...]` attribute lists (including direct `[Attr(args)]`, targeted `[return: Attr]` / `[assembly: Attr]` forms, and comma-separated `[Foo("a"), Bar("b")]` lists) as `attribute` references, and identifiers preceded by a Java-family `@` marker (optionally through a dotted qualifier chain such as `@org.junit.Test(args)`, or through a Kotlin use-site target such as `@field:Deprecated("msg")` / `@get:JsonName("x")` / `@file:JvmName("Foo")`) as `annotation` references. Attribute `[` detection is gated by a declaration-position guard so C# 12 collection expressions (`var xs = [Make(), Make()]`, `Consume([Make(), Make()])`, `return [Make(), Make()]`) and indexer accesses (`arr[Compute()]`) keep their inner calls as `call` instead of being misclassified as metadata. The shared graph filter `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` is applied to default-kind `callers`, `callees`, count queries, hotspot CTEs, and symbol-hotspot grouping, so `callers Obsolete`, `callers Deprecated`, `impact Conditional`, and `hotspots` no longer surface phantom caller edges or inflate hotspot counts for metadata-only references. `references` defaults keep every indexed reference kind visible so metadata usages remain inspectable; `references --kind attribute` / `--kind annotation` (and the MCP tool equivalents) explicitly surface metadata rows, and the CLI help / `--kind` validator / README now list the new reference kinds and document the new default contract for `callers` / `callees` / `hotspots` / `impact`. Regression coverage pins the new classification for `[Obsolete("msg")]`, `[return: NotNull("x")]`, `[Foo("a"), Bar("b")]`, `[AttributeUsage(...)]`, Java `@Deprecated(since="1.0")`, qualified `@org.junit.Test(...)`, Kotlin `@Deprecated("msg")`, Kotlin `@field:Deprecated` / `@get:JsonName` / `@file:JvmName`, ordinary method bodies still emitting `call`, and C# collection-expression / indexer call sites staying `call`. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Database/DbReader.cs`, `src/CodeIndex/Database/DbSymbolReader.cs`, `src/CodeIndex/Cli/QueryCommandRunner.cs`, `src/CodeIndex/Cli/ConsoleUi.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`, `tests/CodeIndex.Tests/ConsoleUiTests.cs`, `README.md`, `DEVELOPER_GUIDE.md`. Closes #293. +- **C# attribute and Java/Kotlin/Scala/TypeScript annotation usages no longer pollute the call-graph as phantom `call` rows (#293)** — `ReferenceExtractor` now reclassifies identifiers that appear inside C# `[...]` attribute lists (including direct `[Attr(args)]`, targeted `[return: Attr]` / `[assembly: Attr]` forms, and comma-separated `[Foo("a"), Bar("b")]` lists) as `attribute` references, and identifiers preceded by a Java-family `@` marker (optionally through a dotted qualifier chain such as `@org.junit.Test(args)`, or through a Kotlin use-site target such as `@field:Deprecated("msg")` / `@get:JsonName("x")` / `@file:JvmName("Foo")`) as `annotation` references. Attribute `[` detection is gated by a declaration-position guard so C# 12 collection expressions (`var xs = [Make(), Make()]`, `Consume([Make(), Make()])`, `return [Make(), Make()]`) and indexer accesses (`arr[Compute()]`, chained `arr[Compute()][Compute()]`, `matrix[Row()][Col()]`) keep their inner calls as `call` instead of being misclassified as metadata — when the `[` is preceded by another `]`, the walk-back now finds the matching `[` (skipping balanced parens) and re-checks that opening bracket's declaration position so `[A("x")][B("y")]` chained attribute lists still classify as `attribute` while `arr[i][Compute()]` stays `call`. Kotlin use-site target detection also handles the qualifier-chain combination `@field:com.example.Deprecated("msg")` / `@get:com.fasterxml.jackson.annotation.JsonProperty("x")` by unwinding the dotted qualifier first and then matching either `@` or a Kotlin `target:` + `@`. The shared graph filter `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` is applied to default-kind `callers`, `callees`, count queries, hotspot CTEs, and symbol-hotspot grouping, so `callers Obsolete`, `callers Deprecated`, `impact Conditional`, and `hotspots` no longer surface phantom caller edges or inflate hotspot counts for metadata-only references. `references` defaults keep every indexed reference kind visible so metadata usages remain inspectable; `references --kind attribute` / `--kind annotation` (and the MCP tool equivalents) explicitly surface metadata rows, and the CLI help / `--kind` validator / README now list the new reference kinds and document the new default contract for `callers` / `callees` / `hotspots` / `impact`. Regression coverage pins the new classification for `[Obsolete("msg")]`, `[return: NotNull("x")]`, `[Foo("a"), Bar("b")]`, `[AttributeUsage(...)]`, Java `@Deprecated(since="1.0")`, qualified `@org.junit.Test(...)`, Kotlin `@Deprecated("msg")`, Kotlin `@field:Deprecated` / `@get:JsonName` / `@file:JvmName`, ordinary method bodies still emitting `call`, and C# collection-expression / indexer call sites staying `call`. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Database/DbReader.cs`, `src/CodeIndex/Database/DbSymbolReader.cs`, `src/CodeIndex/Cli/QueryCommandRunner.cs`, `src/CodeIndex/Cli/ConsoleUi.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`, `tests/CodeIndex.Tests/ConsoleUiTests.cs`, `README.md`, `DEVELOPER_GUIDE.md`. Closes #293. - **C# expression-bodied and block-bodied property callers attribute to the individual method/property instead of the enclosing class (#233)** — `SymbolExtractor` now detects `=> expr;` at top level inside `FindCSharpBraceRange` and assigns a body range covering the declaration line through the terminating `;` (including the multi-line form where `=>` is placed on a following indented line, both for methods and for expression-bodied properties). The block-bodied property scanner now also merges the common Microsoft-style `public int Wrap {` + next-line `get { ... }` shape with its accessor line so that form is recognized alongside the existing K&R (`public int Wrap { get { ... } }`) and Allman (`public int Wrap` then `{` on the next line) shapes, without misclassifying `public class X {` or other non-property headers. Block comments spanning several lines between the header and the body are handled correctly by the same line-scanning path. `ReferenceExtractor.FindInnermostContainer` now accepts `property` as a container kind alongside `function` / `class` / `namespace`. As a result `callers `, `impact_analysis`, and the caller rows surfaced by `inspect` / `analyze_symbol` no longer collapse `public int Wrap() => Compute();`, `public int Wrap => Compute();`, the block-bodied `public int Wrap { get { return Compute(); } }` (K&R), `public int Wrap {` / ` get { return Compute(); }` / `}` (same-line brace with next-line accessor), the Allman-style `public int Wrap` / `{` / ` get { return Compute(); }` / `}`, the multi-line expression-bodied `public int Wrap` / ` => Compute();`, or the same shapes with multi-line block comments between header and body into a single enclosing-class row — each caller now appears on its own row with the correct `caller_kind` (`function` or `property`) and `caller_name`. Regression coverage spans symbol body ranges, all three block-bodied property shapes plus a misclassification guard for bare-brace lines whose body is not an accessor, accessor-attribute and visibility-prefixed accessor lines, reference attribution for expression-bodied methods / properties / multi-line `=> expr;` / K&R block-bodied accessor / bare-brace-same-line accessor-next-line / Allman-style / multi-line expression-bodied property forms, the block-comment-between-header-and-body variants for both Allman and multi-line expression-bodied properties, the previously-encoded ternary-continuation caller test, and end-to-end `callers` assertions through the CLI for the K&R shape (covered by existing property tests), the bare-brace-same-line shape, the Allman block-body shape, the multi-line expression-bodied shape, and both block-comment-separated variants. Affected: `src/CodeIndex/Indexer/SymbolExtractor.cs`, `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/SymbolExtractorTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`, `tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`. Closes #233. - **C# 13 partial properties now participate in symbol extraction (#228)** — The C# extractor now accepts the `partial` modifier on properties and looks ahead across multiline headers, so brace-on-next-line declarations, block-bodied implementations, multiline expression-bodied properties, and `partial override` forms no longer disappear from `symbols`, `definition`, `outline`, `hotspots`, and related symbol-driven commands. Added regression coverage for multiline declaration-style, block-bodied implementation, and multiline `partial override` properties. Affected: `src/CodeIndex/Indexer/SymbolExtractor.cs`, `tests/CodeIndex.Tests/SymbolExtractorTests.cs`. Closes #228. - **C# operators, conversion operators, and indexers now use navigable symbol names (#213)** — `SymbolExtractor` now stores overloads as names like `operator +` and `operator checked +`, stores `implicit` / `explicit` conversion operators as exact names like `implicit operator decimal`, `explicit operator Money`, `explicit operator Dictionary`, and `explicit operator (int whole, int cents)` so they no longer collide with constructors, and normalizes C# indexers from the keyword `this` to the CLR-style name `Item`. Tuple/generic conversion target types now go through a lightweight type scanner and canonical whitespace normalization, including named tuple elements after generic, array, nested-tuple, nullable type tokens, `unsafe` pointer targets, and function-pointer target types, so exact-name queries do not depend on the source file's spacing style. Incremental `cdidx index .` now tracks a C# symbol-name contract version in `codeindex_meta` and automatically re-extracts unchanged C# files once when a canonical-name upgrade lands, so existing DBs pick up the new exact names without requiring `--rebuild`; `status` also exposes `csharp_symbol_name_ready`, exact-name queries surface stale canonical-name degradation instead of silent false negatives, `status --db ...` now prints a repair command that targets the diagnosed DB instead of the caller's default workspace DB, and MCP exact-name trust metadata now honors the active query scope instead of degrading unrelated languages or paths. Added regression coverage in extractor, DB query, CLI query, MCP query, status, and index-upgrade tests, and updated the developer guide and README to document the naming convention. Affected: `src/CodeIndex/Indexer/SymbolExtractor.cs`, `src/CodeIndex/Cli/IndexCommandRunner.cs`, `src/CodeIndex/Cli/QueryCommandRunner.cs`, `src/CodeIndex/Database/DbContext.cs`, `src/CodeIndex/Database/DbReader.cs`, `src/CodeIndex/Database/DbWriter.cs`, `src/CodeIndex/Models/QueryResults.cs`, `src/CodeIndex/Mcp/McpToolHandlers.cs`, `tests/CodeIndex.Tests/SymbolExtractorTests.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`, `tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `tests/CodeIndex.Tests/IndexCommandRunnerTests.cs`, `DEVELOPER_GUIDE.md`, `README.md`. Closes #213. @@ -678,7 +678,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **DEVELOPER_GUIDE が SQLite の位置付けとリリース番号の例示を明確化** — cdidx が「設定ゼロ・単一ファイル・本番依存1個」の CLI であり、その唯一の本番依存が `Microsoft.Data.Sqlite` であることを明記した。あわせて、リリース手順のチェックリスト中に出てくる `1.9.0` は固定値ではなく例示であり、実際のリリース番号へ読み替える前提だと明示した。対象: `DEVELOPER_GUIDE.md`。 #### 修正 -- **C# 属性と Java/Kotlin/Scala/TypeScript の annotation 使用が phantom な `call` 行として call-graph を汚染しないよう修正 (#293)** — `ReferenceExtractor` が C# の `[...]` 属性リスト内(直接形 `[Attr(args)]`、target 付き `[return: Attr]` / `[assembly: Attr]`、カンマ区切り `[Foo("a"), Bar("b")]` を含む)に現れる識別子を `attribute` 参照、Java 系 `@` マーカーが先行する識別子(`@org.junit.Test(args)` のようなドット修飾チェーン越し、および Kotlin の use-site target `@field:Deprecated("msg")` / `@get:JsonName("x")` / `@file:JvmName("Foo")` 越しを含む)を `annotation` 参照として分類するようになった。属性 `[` の判定には宣言位置ガードを入れ、C# 12 の collection expression(`var xs = [Make(), Make()]`、`Consume([Make(), Make()])`、`return [Make(), Make()]`)や indexer アクセス(`arr[Compute()]`)の内側の呼び出しは `call` のまま残り、metadata に誤分類されないようにした。共有 filter `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` を default-kind の `callers`・`callees`・count クエリ・hotspot CTE・symbol-hotspot グルーピングに適用し、`callers Obsolete`・`callers Deprecated`・`impact Conditional`・`hotspots` で phantom caller edge や metadata のみの参照による件数膨張が発生しないようにした。`references` の既定は全 reference kind を表示して metadata 使用も確認できる状態を保ちつつ、`references --kind attribute` / `--kind annotation`(と MCP ツール)で metadata 行を明示取得できる。CLI help・`--kind` バリデータ・README も新しい reference kind と `callers` / `callees` / `hotspots` / `impact` の新しい既定契約を反映する。`[Obsolete("msg")]`・`[return: NotNull("x")]`・`[Foo("a"), Bar("b")]`・`[AttributeUsage(...)]`、Java `@Deprecated(since="1.0")`・修飾付き `@org.junit.Test(...)`、Kotlin `@Deprecated("msg")`、Kotlin `@field:Deprecated` / `@get:JsonName` / `@file:JvmName` の分類、通常のメソッド本体が引き続き `call` を返すこと、C# collection expression や indexer アクセス内の呼び出しが `call` のままであることを押さえる回帰テストを追加。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Database/DbReader.cs`、`src/CodeIndex/Database/DbSymbolReader.cs`、`src/CodeIndex/Cli/QueryCommandRunner.cs`、`src/CodeIndex/Cli/ConsoleUi.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`、`tests/CodeIndex.Tests/ConsoleUiTests.cs`、`README.md`、`DEVELOPER_GUIDE.md`。Closes #293。 +- **C# 属性と Java/Kotlin/Scala/TypeScript の annotation 使用が phantom な `call` 行として call-graph を汚染しないよう修正 (#293)** — `ReferenceExtractor` が C# の `[...]` 属性リスト内(直接形 `[Attr(args)]`、target 付き `[return: Attr]` / `[assembly: Attr]`、カンマ区切り `[Foo("a"), Bar("b")]` を含む)に現れる識別子を `attribute` 参照、Java 系 `@` マーカーが先行する識別子(`@org.junit.Test(args)` のようなドット修飾チェーン越し、および Kotlin の use-site target `@field:Deprecated("msg")` / `@get:JsonName("x")` / `@file:JvmName("Foo")` 越しを含む)を `annotation` 参照として分類するようになった。属性 `[` の判定には宣言位置ガードを入れ、C# 12 の collection expression(`var xs = [Make(), Make()]`、`Consume([Make(), Make()])`、`return [Make(), Make()]`)や indexer アクセス(`arr[Compute()]`、連続 `arr[Compute()][Compute()]`、`matrix[Row()][Col()]`)の内側の呼び出しは `call` のまま残り、metadata に誤分類されないようにした。`[` の直前が別の `]` だった場合は、対応する `[` まで左方向に(balanced paren を跨いで)巻き戻してその開き bracket 自体が宣言位置かを再判定するため、`[A("x")][B("y")]` のような連続 attribute list は引き続き `attribute` として認識される一方、`arr[i][Compute()]` のような連続 indexer は `call` のまま残る。Kotlin の use-site target 判定も、ドット修飾子チェーンを先に剥がしたうえで `@` または Kotlin の `target:` + `@` を判定する構成に変え、`@field:com.example.Deprecated("msg")` / `@get:com.fasterxml.jackson.annotation.JsonProperty("x")` のような修飾付き注釈名と use-site target の組み合わせにも対応するようにした。共有 filter `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` を default-kind の `callers`・`callees`・count クエリ・hotspot CTE・symbol-hotspot グルーピングに適用し、`callers Obsolete`・`callers Deprecated`・`impact Conditional`・`hotspots` で phantom caller edge や metadata のみの参照による件数膨張が発生しないようにした。`references` の既定は全 reference kind を表示して metadata 使用も確認できる状態を保ちつつ、`references --kind attribute` / `--kind annotation`(と MCP ツール)で metadata 行を明示取得できる。CLI help・`--kind` バリデータ・README も新しい reference kind と `callers` / `callees` / `hotspots` / `impact` の新しい既定契約を反映する。`[Obsolete("msg")]`・`[return: NotNull("x")]`・`[Foo("a"), Bar("b")]`・`[AttributeUsage(...)]`、Java `@Deprecated(since="1.0")`・修飾付き `@org.junit.Test(...)`、Kotlin `@Deprecated("msg")`、Kotlin `@field:Deprecated` / `@get:JsonName` / `@file:JvmName` の分類、通常のメソッド本体が引き続き `call` を返すこと、C# collection expression や indexer アクセス内の呼び出しが `call` のままであることを押さえる回帰テストを追加。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Database/DbReader.cs`、`src/CodeIndex/Database/DbSymbolReader.cs`、`src/CodeIndex/Cli/QueryCommandRunner.cs`、`src/CodeIndex/Cli/ConsoleUi.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`、`tests/CodeIndex.Tests/ConsoleUiTests.cs`、`README.md`、`DEVELOPER_GUIDE.md`。Closes #293。 - **C# 式本体メンバーと block-bodied プロパティの caller が外側クラスではなく個別メソッド/プロパティに帰属するよう修正 (#233)** — `SymbolExtractor` の `FindCSharpBraceRange` がトップレベルの `=> expr;` を検出し、宣言行から終端 `;` までを本体範囲として扱うようになった(宣言行の次行にインデントして `=>` を置く multi-line 形にも、メソッドと式本体プロパティの双方で対応)。block-bodied プロパティの行結合ロジックも、`public int Wrap {` の次行に ` get { ... }` を置く Microsoft 標準形を accessor 行と結合するよう拡張し、既存の K&R (`public int Wrap { get { ... } }`) や Allman (`public int Wrap` の次行に `{`) と並ぶ 3 形を抽出対象にしつつ、`public class X {` のような非プロパティ header を property と誤分類しないようにした。ヘッダと本体の間に複数行またがるブロックコメントが挟まる場合も同じ行スキャン経路で正しく処理される。`ReferenceExtractor.FindInnermostContainer` は `property` も `function` / `class` / `namespace` と並ぶ container kind として受け入れるようにした。これにより `callers ` / `impact_analysis` と `inspect` / `analyze_symbol` に現れる caller 行が、`public int Wrap() => Compute();`、`public int Wrap => Compute();`、K&R の `public int Wrap { get { return Compute(); } }`、`public int Wrap {` / ` get { return Compute(); }` / `}`(同一行 bare `{` + 次行 accessor)、Allman の `public int Wrap` / `{` / ` get { return Compute(); }` / `}`、次行に ` => Compute();` を置く multi-line 式本体プロパティ、さらにヘッダと本体の間に複数行のブロックコメントを挟む同種の形まで、外側クラスの 1 行にまとめてしまわず、各 caller が固有の `caller_kind`(`function` または `property`)と `caller_name` を持つ別行として現れる。シンボル本体範囲、3 形すべての block-bodied property 抽出と、本体が accessor でない bare `{` の誤分類防止ガード、accessor attribute / visibility 修飾子付き accessor 行、式本体メソッド / プロパティ / multi-line `=> expr;` / K&R block-bodied accessor / 同一行 bare `{` + 次行 accessor / Allman / multi-line 式本体プロパティの参照属性、Allman と multi-line 式本体プロパティのブロックコメント挟み込み版、既存の ternary-continuation caller テスト、K&R 形(既存テストでカバー)、bare `{` + 次行 accessor 形、Allman block-body 形、multi-line 式本体プロパティ形、そしてブロックコメント挟み込みの両形の CLI `callers` end-to-end 検証まで押さえる回帰テストを追加。対象: `src/CodeIndex/Indexer/SymbolExtractor.cs`、`src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/SymbolExtractorTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`、`tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`。Closes #233。 - **C# 13 の partial property がシンボル抽出から欠落しないよう修正 (#228)** — C# extractor が property の `partial` 修飾子を受け付け、さらに複数行 header を先読みするようになった。これにより brace-on-next-line の宣言、block-bodied な実装、複数行の expression-bodied property、`partial override` 形が `symbols`、`definition`、`outline`、`hotspots` などのシンボル系コマンドから消えなくなった。複数行 declaration-style、block-bodied implementation、複数行 `partial override` property を押さえる回帰テストも追加。対象: `src/CodeIndex/Indexer/SymbolExtractor.cs`、`tests/CodeIndex.Tests/SymbolExtractorTests.cs`。Closes #228。 - **C# の演算子・変換演算子・インデクサが検索しやすいシンボル名で保存されるよう修正 (#213)** — `SymbolExtractor` が C# の演算子オーバーロードを `operator +` や `operator checked +` のような名前で保持し、`implicit` / `explicit` 変換演算子は `implicit operator decimal`、`explicit operator Money`、`explicit operator Dictionary`、`explicit operator (int whole, int cents)` のような exact name で保持してコンストラクタと衝突しないようにし、インデクサはキーワードの `this` ではなく CLR 互換の `Item` に正規化するようにした。tuple / generic target type は軽量な型スキャナと空白正規化を通し、generic・array・nested tuple・nullable type の直後に named tuple element が来る場合に加えて、`unsafe` pointer target や function-pointer target type も含めて、exact-name query が source の空白スタイルに依存しない。さらに `codeindex_meta` に C# symbol-name contract version を保持し、canonical name の変更が入ったときは通常の `cdidx index .` でも unchanged な C# ファイルを 1 回だけ自動再抽出して `--rebuild` なしで既存 DB を更新できるようにし、`status` でも `csharp_symbol_name_ready` を返して stale な operator/indexer exact-name trust を可視化したうえで、exact-name query でも silent false negative ではなく stale canonical-name の縮退を返し、`status --db ...` の修復案内も診断対象 DB 自体を指すようにし、MCP の exact-name trust metadata も query scope に従って無関係な言語や path を誤って縮退扱いしないようにした。extractor / DB query / CLI query / MCP query / status / index-upgrade の回帰テストを追加し、開発者ガイドと README も新しい命名規約に合わせて更新した。対象: `src/CodeIndex/Indexer/SymbolExtractor.cs`, `src/CodeIndex/Cli/IndexCommandRunner.cs`, `src/CodeIndex/Cli/QueryCommandRunner.cs`, `src/CodeIndex/Database/DbContext.cs`, `src/CodeIndex/Database/DbReader.cs`, `src/CodeIndex/Database/DbWriter.cs`, `src/CodeIndex/Models/QueryResults.cs`, `src/CodeIndex/Mcp/McpToolHandlers.cs`, `tests/CodeIndex.Tests/SymbolExtractorTests.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`, `tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `tests/CodeIndex.Tests/IndexCommandRunnerTests.cs`, `DEVELOPER_GUIDE.md`, `README.md`。Closes #213。 diff --git a/src/CodeIndex/Indexer/ReferenceExtractor.cs b/src/CodeIndex/Indexer/ReferenceExtractor.cs index a60037de4b..358297fbd5 100644 --- a/src/CodeIndex/Indexer/ReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/ReferenceExtractor.cs @@ -493,7 +493,58 @@ private static bool IsCSharpAttributeOpenBracket(string line, int bracketIndex) if (i < 0) return true; var c = line[i]; - return c == ']' || c == '{' || c == '}' || c == ';'; + if (c == '{' || c == '}' || c == ';') + return true; + // A preceding `]` only indicates attribute-list chaining (`[A][B]`) when that `]` + // actually closed an attribute section. For `arr[i][Compute()]` the preceding `]` + // closes an indexer, so we must walk back to the matching `[` and re-check that + // opening bracket's declaration position. + // 直前の `]` が attribute list のチェーン (`[A][B]`) を意味するのは、その `]` が + // 実際に attribute section を閉じていたときだけ。`arr[i][Compute()]` のように + // indexer を閉じた `]` の場合は、対応する `[` まで戻ってそこが宣言位置かを再判定する。 + if (c == ']') + return IsCSharpAttributeSectionClose(line, i); + return false; + } + + /// + /// Walk left from a `]` to its matching `[`, skipping balanced parens, and return true + /// when that opening bracket itself was at a C# declaration position. + /// `]` から対応する `[` まで左方向に遡り、その開き bracket 自体が宣言位置だった場合のみ true を返す。 + /// + private static bool IsCSharpAttributeSectionClose(string line, int closeBracketIndex) + { + var bracketDepth = 1; + var parenDepth = 0; + for (var i = closeBracketIndex - 1; i >= 0; i--) + { + var c = line[i]; + if (c == ')') + { + parenDepth++; + continue; + } + if (c == '(') + { + if (parenDepth > 0) + parenDepth--; + continue; + } + if (parenDepth > 0) + continue; + if (c == ']') + { + bracketDepth++; + continue; + } + if (c == '[') + { + bracketDepth--; + if (bracketDepth == 0) + return IsCSharpAttributeOpenBracket(line, i); + } + } + return false; } private static bool ScanLeftForAttributeOpen(string line, int start) @@ -530,10 +581,33 @@ private static bool IsAnnotationContext(string line, int probe) if (line[probe] == '@') return true; - // Kotlin use-site target: `@field:Deprecated("msg")` — skip the `target:` prefix - // and confirm an `@` marker precedes it. - // Kotlin の use-site target 付き注釈 `@field:Deprecated("msg")` では、`target:` を - // 読み飛ばして `@` を探す。 + // `@module.Annotation(args)` — walk past the dotted qualifier chain first so that + // both `@module.Annotation` and `@field:com.example.Annotation` land the probe on + // either `@` or the Kotlin use-site target `:`. + // `@module.Annotation(args)` や `@field:com.example.Annotation(args)` のように修飾子が + // 付く場合も対応するため、先にドット区切り修飾子チェーンを剥がしてから `@` または + // Kotlin の use-site target `:` を判定する。 + while (probe >= 0 && line[probe] == '.') + { + probe--; + while (probe >= 0 && IsIdentifierChar(line[probe])) + probe--; + while (probe >= 0 && char.IsWhiteSpace(line[probe])) + probe--; + } + + if (probe < 0) + return false; + + if (line[probe] == '@') + return true; + + // Kotlin use-site target: `@field:Deprecated("msg")` or + // `@field:com.example.Deprecated("msg")`. After unwinding the dotted qualifier, the + // probe lands on `:`; walk past the target identifier and confirm `@`. + // Kotlin の use-site target `@field:Deprecated("msg")` や + // `@field:com.example.Deprecated("msg")` では、ドット修飾子を剥がしたあと probe が `:` + // に着地するため、target 識別子を読み飛ばして `@` を確認する。 if (line[probe] == ':') { var j = probe - 1; @@ -552,21 +626,9 @@ private static bool IsAnnotationContext(string line, int probe) return true; } } - return false; } - // `@module.Annotation(args)` — walk past the dotted qualifier chain to the `@`. - // `@module.Annotation(args)` のようにドット区切り修飾子がある場合は、`.` と識別子を - // 順に遡って `@` を探す。 - while (probe >= 0 && line[probe] == '.') - { - probe--; - while (probe >= 0 && IsIdentifierChar(line[probe])) - probe--; - while (probe >= 0 && char.IsWhiteSpace(line[probe])) - probe--; - } - return probe >= 0 && line[probe] == '@'; + return false; } private static bool UsesHashComments(string lang) => diff --git a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs index d8e969eb17..d31acd240e 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs @@ -1344,4 +1344,139 @@ package example var jvmName = Assert.Single(references.Where(r => r.SymbolName == "JvmName")); Assert.Equal("annotation", jvmName.ReferenceKind); } + + [Fact] + public void Extract_CsharpChainedIndexerCalls_StayCall() + { + // issue #293 follow-up: `arr[Compute()][Compute()]` — the second `[` is preceded by + // an indexer-closing `]`, not an attribute-section `]`. Walking back to the matching + // `[` must find an expression-position bracket so both inner calls remain `call`. + // issue #293 補足: `arr[Compute()][Compute()]` の 2 個目の `[` は indexer の `]` に + // 続くだけで attribute section の終端ではないため、対応する `[` まで戻って宣言位置で + // ないことを確認し、両方の呼び出しを `call` のまま残す。 + const string content = """ + public class C + { + public int Compute() => 0; + public int Read(int[][] arr) => arr[Compute()][Compute()]; + } + """; + + var symbols = SymbolExtractor.Extract(1, "csharp", content); + var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); + + var computeRefs = references.Where(r => r.SymbolName == "Compute").ToList(); + Assert.Equal(2, computeRefs.Count); + Assert.All(computeRefs, r => Assert.Equal("call", r.ReferenceKind)); + } + + [Fact] + public void Extract_CsharpMatrixIndexerCalls_StayCall() + { + // Two consecutive indexer accesses on a matrix — `matrix[Row()][Col()]` — must keep + // both inner calls as `call`. + // 連続 indexer `matrix[Row()][Col()]` でも、両方の呼び出しが `call` のまま残ること。 + const string content = """ + public class M + { + public int Row() => 0; + public int Col() => 0; + public int Read(int[,] matrix) => matrix[Row(), Col()]; + public int Read2(int[][] grid) => grid[Row()][Col()]; + } + """; + + var symbols = SymbolExtractor.Extract(1, "csharp", content); + var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); + + var row = references.Where(r => r.SymbolName == "Row").ToList(); + var col = references.Where(r => r.SymbolName == "Col").ToList(); + Assert.Equal(2, row.Count); + Assert.Equal(2, col.Count); + Assert.All(row, r => Assert.Equal("call", r.ReferenceKind)); + Assert.All(col, r => Assert.Equal("call", r.ReferenceKind)); + } + + [Fact] + public void Extract_CsharpChainedAttributeLists_StayAttribute() + { + // `[A(...)][B(...)]` on a declaration — the chained attribute-list form must still + // classify both entries as `attribute` after the indexer-safety walk-back. + // `[A(...)][B(...)]` の連続 attribute list は、indexer との区別が入ったあとも両方 `attribute`。 + const string content = """ + [A("x")][B("y")] + public class C { } + """; + + var symbols = SymbolExtractor.Extract(1, "csharp", content); + var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); + + var a = Assert.Single(references.Where(r => r.SymbolName == "A")); + var b = Assert.Single(references.Where(r => r.SymbolName == "B")); + Assert.Equal("attribute", a.ReferenceKind); + Assert.Equal("attribute", b.ReferenceKind); + } + + [Fact] + public void Extract_KotlinQualifiedFieldTargetAnnotation_ClassifiedAsAnnotation() + { + // issue #293 follow-up: Kotlin use-site target with a fully-qualified annotation + // name, e.g. `@field:com.example.Deprecated("msg")`, must be classified as + // `annotation` — the dotted qualifier chain plus the `target:` prefix must both + // resolve back to `@`. + // issue #293 補足: Kotlin の `@field:com.example.Deprecated("msg")` のように use-site + // target と修飾付き注釈名が組み合わさった場合も `annotation` 判定になること。 + const string content = """ + class Example { + @field:com.example.Deprecated("msg") + val value: Int = 0 + } + """; + + var symbols = SymbolExtractor.Extract(1, "kotlin", content); + var references = ReferenceExtractor.Extract(1, "kotlin", content, symbols); + + var deprecated = Assert.Single(references.Where(r => r.SymbolName == "Deprecated")); + Assert.Equal("annotation", deprecated.ReferenceKind); + } + + [Fact] + public void Extract_KotlinQualifiedGetTargetAnnotation_ClassifiedAsAnnotation() + { + // Kotlin `@get:com.fasterxml.jackson.annotation.JsonProperty("x")` — use-site target + // combined with a long qualifier chain must still be `annotation`. + // Kotlin の `@get:com.fasterxml.jackson.annotation.JsonProperty("x")` も `annotation`。 + const string content = """ + class K { + @get:com.fasterxml.jackson.annotation.JsonProperty("x") + val x: Int = 0 + } + """; + + var symbols = SymbolExtractor.Extract(1, "kotlin", content); + var references = ReferenceExtractor.Extract(1, "kotlin", content, symbols); + + var jsonProperty = Assert.Single(references.Where(r => r.SymbolName == "JsonProperty")); + Assert.Equal("annotation", jsonProperty.ReferenceKind); + } + + [Fact] + public void Extract_KotlinQualifiedAnnotationWithoutTarget_ClassifiedAsAnnotation() + { + // Regression guard: fully-qualified annotation name without a use-site target, e.g. + // `@org.junit.Test(...)`, must still be `annotation`. + // 退行防止: use-site target のない `@org.junit.Test(...)` も引き続き `annotation`。 + const string content = """ + class K { + @org.junit.Test(expected = Exception::class) + fun run() {} + } + """; + + var symbols = SymbolExtractor.Extract(1, "kotlin", content); + var references = ReferenceExtractor.Extract(1, "kotlin", content, symbols); + + var test = Assert.Single(references.Where(r => r.SymbolName == "Test")); + Assert.Equal("annotation", test.ReferenceKind); + } } From 78f9d427aae8095ab17aaae8cf77ada21b16c505 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 17 Apr 2026 20:19:28 +0900 Subject: [PATCH 04/30] Fix #293 follow-up: impact subscribe, C# param/multi-line attrs Address adversarial review findings on top of the #293 fix: 1. impact BFS now follows subscribe edges. DbReader.GetCallersExact (the sole caller used by impact / impact_analysis) was still using InvokeReferenceKindsSql ('call', 'instantiate'), so event subscriptions like `Changed += OnChanged` quietly dropped out of the transitive caller chain even though callers / callees / hotspots kept them. Switched to the shared CallGraphReferenceKindsSql ('call', 'instantiate', 'subscribe') and added a regression test. 2. C# parameter attributes and multi-line attribute lists are now classified as `attribute` instead of `call`. ReferenceExtractor now runs a whole-file pre-pass that tokenizes every C# `[...]` section, disambiguating the `(` / `,` entry point (`void M( [Attr] T x)`) against C# 12 collection expressions in argument position (`Consume([Make()])`) via forward lookahead from `[` to the matching `]`. Multi-line forms such as `[\n Foo("x")\n]` and cross-line parameter attributes are also covered. Targeted `[return: NotNullWhen(true)]` flows through the same bracket-section path, so the previous `target:` heuristic is no longer needed. Regression coverage: - DbReaderTests.GetTransitiveCallers_FollowsSubscribeEdges - Extract_CsharpParameterAttributes_ClassifiedAsAttribute - Extract_CsharpMultiLineAttribute_ClassifiedAsAttribute - Extract_CsharpMultiLineParameterAttribute_ClassifiedAsAttribute - Extract_CsharpTargetedAttribute_StaysAttribute - Extract_CsharpCollectionExpressionInArgument_StaysCallAfterParen --- CHANGELOG.md | 4 + src/CodeIndex/Database/DbReader.cs | 8 +- src/CodeIndex/Indexer/ReferenceExtractor.cs | 324 +++++++++++------- tests/CodeIndex.Tests/DbReaderTests.cs | 40 +++ .../ReferenceExtractorTests.cs | 119 +++++++ 5 files changed, 376 insertions(+), 119 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7f5a3ce5c..4da5f189f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **Developer Guide now distinguishes SQLite's role from "zero dependencies" and labels release numbers as examples** — Clarified that cdidx remains a zero-configuration, single-file CLI with exactly one production dependency (`Microsoft.Data.Sqlite`), and made the release-workflow checklist explicitly state that `1.9.0` is only an example version to substitute during an actual release. Affected: `DEVELOPER_GUIDE.md`. #### Fixed +- **`impact` BFS now follows C# event subscribe edges (#293 follow-up)** — `DbReader.GetCallersExact`, the only caller site used by the `impact` / `impact_analysis` BFS, now filters references through the shared `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` set instead of `InvokeReferenceKindsSql = ('call', 'instantiate')`. Previously `impact` quietly dropped `Changed += OnChanged` subscribe rows while `callers` / `callees` / `hotspots` kept them, so an event-driven transitive caller chain appeared shorter under `impact` than under `callers`. A new `DbReaderTests.GetTransitiveCallers_FollowsSubscribeEdges` regression pins the subscribe edge into the BFS output. Metadata edges (`attribute`, `annotation`) remain excluded. Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. +- **C# parameter attributes and multi-line `[...]` attribute lists are now classified as `attribute` instead of `call` (#293 follow-up)** — `ReferenceExtractor` now runs a pre-pass that tokenizes every C# `[...]` section across the whole file, so parameter attributes such as `void M([FromServices] IService s)` (where `[` is preceded by `(` or `,` rather than a declaration boundary) and multi-line forms such as `[\n Foo("x")\n]` or `public void M(\n [Attr("x")]\n int a)` are recognized alongside the existing same-line cases. The pre-pass disambiguates the `(` / `,` entry point against C# 12 collection expressions in argument position (`Consume([Make()])`) by scanning forward from `[` to the matching `]` and checking whether the next meaningful character begins an identifier. Targeted forms (`[return: NotNullWhen(true)]`) are now recognized directly from their `[...]` section instead of via the previous `target:` heuristic. Regression coverage pins `[Attr("x")] int a, [Other("y")] int b`, `[\n Foo("x")\n]`, the cross-line parameter attribute shape, the targeted `[return: ...]` shape, and the defense-in-depth collection-expression-in-argument case. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. - **C# attribute and Java/Kotlin/Scala/TypeScript annotation usages no longer pollute the call-graph as phantom `call` rows (#293)** — `ReferenceExtractor` now reclassifies identifiers that appear inside C# `[...]` attribute lists (including direct `[Attr(args)]`, targeted `[return: Attr]` / `[assembly: Attr]` forms, and comma-separated `[Foo("a"), Bar("b")]` lists) as `attribute` references, and identifiers preceded by a Java-family `@` marker (optionally through a dotted qualifier chain such as `@org.junit.Test(args)`, or through a Kotlin use-site target such as `@field:Deprecated("msg")` / `@get:JsonName("x")` / `@file:JvmName("Foo")`) as `annotation` references. Attribute `[` detection is gated by a declaration-position guard so C# 12 collection expressions (`var xs = [Make(), Make()]`, `Consume([Make(), Make()])`, `return [Make(), Make()]`) and indexer accesses (`arr[Compute()]`, chained `arr[Compute()][Compute()]`, `matrix[Row()][Col()]`) keep their inner calls as `call` instead of being misclassified as metadata — when the `[` is preceded by another `]`, the walk-back now finds the matching `[` (skipping balanced parens) and re-checks that opening bracket's declaration position so `[A("x")][B("y")]` chained attribute lists still classify as `attribute` while `arr[i][Compute()]` stays `call`. Kotlin use-site target detection also handles the qualifier-chain combination `@field:com.example.Deprecated("msg")` / `@get:com.fasterxml.jackson.annotation.JsonProperty("x")` by unwinding the dotted qualifier first and then matching either `@` or a Kotlin `target:` + `@`. The shared graph filter `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` is applied to default-kind `callers`, `callees`, count queries, hotspot CTEs, and symbol-hotspot grouping, so `callers Obsolete`, `callers Deprecated`, `impact Conditional`, and `hotspots` no longer surface phantom caller edges or inflate hotspot counts for metadata-only references. `references` defaults keep every indexed reference kind visible so metadata usages remain inspectable; `references --kind attribute` / `--kind annotation` (and the MCP tool equivalents) explicitly surface metadata rows, and the CLI help / `--kind` validator / README now list the new reference kinds and document the new default contract for `callers` / `callees` / `hotspots` / `impact`. Regression coverage pins the new classification for `[Obsolete("msg")]`, `[return: NotNull("x")]`, `[Foo("a"), Bar("b")]`, `[AttributeUsage(...)]`, Java `@Deprecated(since="1.0")`, qualified `@org.junit.Test(...)`, Kotlin `@Deprecated("msg")`, Kotlin `@field:Deprecated` / `@get:JsonName` / `@file:JvmName`, ordinary method bodies still emitting `call`, and C# collection-expression / indexer call sites staying `call`. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Database/DbReader.cs`, `src/CodeIndex/Database/DbSymbolReader.cs`, `src/CodeIndex/Cli/QueryCommandRunner.cs`, `src/CodeIndex/Cli/ConsoleUi.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`, `tests/CodeIndex.Tests/ConsoleUiTests.cs`, `README.md`, `DEVELOPER_GUIDE.md`. Closes #293. - **C# expression-bodied and block-bodied property callers attribute to the individual method/property instead of the enclosing class (#233)** — `SymbolExtractor` now detects `=> expr;` at top level inside `FindCSharpBraceRange` and assigns a body range covering the declaration line through the terminating `;` (including the multi-line form where `=>` is placed on a following indented line, both for methods and for expression-bodied properties). The block-bodied property scanner now also merges the common Microsoft-style `public int Wrap {` + next-line `get { ... }` shape with its accessor line so that form is recognized alongside the existing K&R (`public int Wrap { get { ... } }`) and Allman (`public int Wrap` then `{` on the next line) shapes, without misclassifying `public class X {` or other non-property headers. Block comments spanning several lines between the header and the body are handled correctly by the same line-scanning path. `ReferenceExtractor.FindInnermostContainer` now accepts `property` as a container kind alongside `function` / `class` / `namespace`. As a result `callers `, `impact_analysis`, and the caller rows surfaced by `inspect` / `analyze_symbol` no longer collapse `public int Wrap() => Compute();`, `public int Wrap => Compute();`, the block-bodied `public int Wrap { get { return Compute(); } }` (K&R), `public int Wrap {` / ` get { return Compute(); }` / `}` (same-line brace with next-line accessor), the Allman-style `public int Wrap` / `{` / ` get { return Compute(); }` / `}`, the multi-line expression-bodied `public int Wrap` / ` => Compute();`, or the same shapes with multi-line block comments between header and body into a single enclosing-class row — each caller now appears on its own row with the correct `caller_kind` (`function` or `property`) and `caller_name`. Regression coverage spans symbol body ranges, all three block-bodied property shapes plus a misclassification guard for bare-brace lines whose body is not an accessor, accessor-attribute and visibility-prefixed accessor lines, reference attribution for expression-bodied methods / properties / multi-line `=> expr;` / K&R block-bodied accessor / bare-brace-same-line accessor-next-line / Allman-style / multi-line expression-bodied property forms, the block-comment-between-header-and-body variants for both Allman and multi-line expression-bodied properties, the previously-encoded ternary-continuation caller test, and end-to-end `callers` assertions through the CLI for the K&R shape (covered by existing property tests), the bare-brace-same-line shape, the Allman block-body shape, the multi-line expression-bodied shape, and both block-comment-separated variants. Affected: `src/CodeIndex/Indexer/SymbolExtractor.cs`, `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/SymbolExtractorTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`, `tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`. Closes #233. - **C# 13 partial properties now participate in symbol extraction (#228)** — The C# extractor now accepts the `partial` modifier on properties and looks ahead across multiline headers, so brace-on-next-line declarations, block-bodied implementations, multiline expression-bodied properties, and `partial override` forms no longer disappear from `symbols`, `definition`, `outline`, `hotspots`, and related symbol-driven commands. Added regression coverage for multiline declaration-style, block-bodied implementation, and multiline `partial override` properties. Affected: `src/CodeIndex/Indexer/SymbolExtractor.cs`, `tests/CodeIndex.Tests/SymbolExtractorTests.cs`. Closes #228. @@ -678,6 +680,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **DEVELOPER_GUIDE が SQLite の位置付けとリリース番号の例示を明確化** — cdidx が「設定ゼロ・単一ファイル・本番依存1個」の CLI であり、その唯一の本番依存が `Microsoft.Data.Sqlite` であることを明記した。あわせて、リリース手順のチェックリスト中に出てくる `1.9.0` は固定値ではなく例示であり、実際のリリース番号へ読み替える前提だと明示した。対象: `DEVELOPER_GUIDE.md`。 #### 修正 +- **`impact` BFS が C# の event subscribe edge を辿るよう修正 (#293 追加対応)** — `impact` / `impact_analysis` BFS の唯一の呼び出し箇所である `DbReader.GetCallersExact` が、共有 filter `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` で参照を絞り込むようになった(以前は `InvokeReferenceKindsSql = ('call', 'instantiate')` を使っていたため、`Changed += OnChanged` のような subscribe 行を静かに捨てていた)。これにより `callers` / `callees` / `hotspots` と同じ call-graph 契約を `impact` も共有し、イベント駆動の transitive caller chain が `impact` でだけ短くなる不整合が解消する。`DbReaderTests.GetTransitiveCallers_FollowsSubscribeEdges` の回帰テストで subscribe edge が BFS 出力に含まれることを固定。Metadata edge (`attribute` / `annotation`) の除外は従来通り。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 +- **C# のパラメータ属性と複数行 `[...]` 属性が `call` ではなく `attribute` として分類されるよう修正 (#293 追加対応)** — `ReferenceExtractor` がファイル全体の C# `[...]` セクションを事前パスでトークン化するようになり、`void M([FromServices] IService s)` のように `[` の直前が宣言境界ではなく `(` や `,` で始まるパラメータ属性と、`[\n Foo("x")\n]` や `public void M(\n [Attr("x")]\n int a)` のような複数行形式も、既存の同一行ケースと同じように属性として扱われるようになった。`(` / `,` 直後の曖昧さは、`[` から対応する `]` まで前方走査し、`]` の直後の非空白文字が識別子を始めるかで判定するため、引数位置の C# 12 collection expression (`Consume([Make()])`) は `call` のまま残る。target 付き形 (`[return: NotNullWhen(true)]`) も、以前のような `target:` 個別ヒューリスティクスではなく `[...]` セクションそのものから判定する形に統一された。`[Attr("x")] int a, [Other("y")] int b`、`[\n Foo("x")\n]`、改行を挟んだパラメータ属性、`[return: NotNullWhen(true)]`、そして防御的に `Consume([Make()])` の collection expression-in-argument を押さえる回帰テストを追加。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 - **C# 属性と Java/Kotlin/Scala/TypeScript の annotation 使用が phantom な `call` 行として call-graph を汚染しないよう修正 (#293)** — `ReferenceExtractor` が C# の `[...]` 属性リスト内(直接形 `[Attr(args)]`、target 付き `[return: Attr]` / `[assembly: Attr]`、カンマ区切り `[Foo("a"), Bar("b")]` を含む)に現れる識別子を `attribute` 参照、Java 系 `@` マーカーが先行する識別子(`@org.junit.Test(args)` のようなドット修飾チェーン越し、および Kotlin の use-site target `@field:Deprecated("msg")` / `@get:JsonName("x")` / `@file:JvmName("Foo")` 越しを含む)を `annotation` 参照として分類するようになった。属性 `[` の判定には宣言位置ガードを入れ、C# 12 の collection expression(`var xs = [Make(), Make()]`、`Consume([Make(), Make()])`、`return [Make(), Make()]`)や indexer アクセス(`arr[Compute()]`、連続 `arr[Compute()][Compute()]`、`matrix[Row()][Col()]`)の内側の呼び出しは `call` のまま残り、metadata に誤分類されないようにした。`[` の直前が別の `]` だった場合は、対応する `[` まで左方向に(balanced paren を跨いで)巻き戻してその開き bracket 自体が宣言位置かを再判定するため、`[A("x")][B("y")]` のような連続 attribute list は引き続き `attribute` として認識される一方、`arr[i][Compute()]` のような連続 indexer は `call` のまま残る。Kotlin の use-site target 判定も、ドット修飾子チェーンを先に剥がしたうえで `@` または Kotlin の `target:` + `@` を判定する構成に変え、`@field:com.example.Deprecated("msg")` / `@get:com.fasterxml.jackson.annotation.JsonProperty("x")` のような修飾付き注釈名と use-site target の組み合わせにも対応するようにした。共有 filter `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` を default-kind の `callers`・`callees`・count クエリ・hotspot CTE・symbol-hotspot グルーピングに適用し、`callers Obsolete`・`callers Deprecated`・`impact Conditional`・`hotspots` で phantom caller edge や metadata のみの参照による件数膨張が発生しないようにした。`references` の既定は全 reference kind を表示して metadata 使用も確認できる状態を保ちつつ、`references --kind attribute` / `--kind annotation`(と MCP ツール)で metadata 行を明示取得できる。CLI help・`--kind` バリデータ・README も新しい reference kind と `callers` / `callees` / `hotspots` / `impact` の新しい既定契約を反映する。`[Obsolete("msg")]`・`[return: NotNull("x")]`・`[Foo("a"), Bar("b")]`・`[AttributeUsage(...)]`、Java `@Deprecated(since="1.0")`・修飾付き `@org.junit.Test(...)`、Kotlin `@Deprecated("msg")`、Kotlin `@field:Deprecated` / `@get:JsonName` / `@file:JvmName` の分類、通常のメソッド本体が引き続き `call` を返すこと、C# collection expression や indexer アクセス内の呼び出しが `call` のままであることを押さえる回帰テストを追加。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Database/DbReader.cs`、`src/CodeIndex/Database/DbSymbolReader.cs`、`src/CodeIndex/Cli/QueryCommandRunner.cs`、`src/CodeIndex/Cli/ConsoleUi.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`、`tests/CodeIndex.Tests/ConsoleUiTests.cs`、`README.md`、`DEVELOPER_GUIDE.md`。Closes #293。 - **C# 式本体メンバーと block-bodied プロパティの caller が外側クラスではなく個別メソッド/プロパティに帰属するよう修正 (#233)** — `SymbolExtractor` の `FindCSharpBraceRange` がトップレベルの `=> expr;` を検出し、宣言行から終端 `;` までを本体範囲として扱うようになった(宣言行の次行にインデントして `=>` を置く multi-line 形にも、メソッドと式本体プロパティの双方で対応)。block-bodied プロパティの行結合ロジックも、`public int Wrap {` の次行に ` get { ... }` を置く Microsoft 標準形を accessor 行と結合するよう拡張し、既存の K&R (`public int Wrap { get { ... } }`) や Allman (`public int Wrap` の次行に `{`) と並ぶ 3 形を抽出対象にしつつ、`public class X {` のような非プロパティ header を property と誤分類しないようにした。ヘッダと本体の間に複数行またがるブロックコメントが挟まる場合も同じ行スキャン経路で正しく処理される。`ReferenceExtractor.FindInnermostContainer` は `property` も `function` / `class` / `namespace` と並ぶ container kind として受け入れるようにした。これにより `callers ` / `impact_analysis` と `inspect` / `analyze_symbol` に現れる caller 行が、`public int Wrap() => Compute();`、`public int Wrap => Compute();`、K&R の `public int Wrap { get { return Compute(); } }`、`public int Wrap {` / ` get { return Compute(); }` / `}`(同一行 bare `{` + 次行 accessor)、Allman の `public int Wrap` / `{` / ` get { return Compute(); }` / `}`、次行に ` => Compute();` を置く multi-line 式本体プロパティ、さらにヘッダと本体の間に複数行のブロックコメントを挟む同種の形まで、外側クラスの 1 行にまとめてしまわず、各 caller が固有の `caller_kind`(`function` または `property`)と `caller_name` を持つ別行として現れる。シンボル本体範囲、3 形すべての block-bodied property 抽出と、本体が accessor でない bare `{` の誤分類防止ガード、accessor attribute / visibility 修飾子付き accessor 行、式本体メソッド / プロパティ / multi-line `=> expr;` / K&R block-bodied accessor / 同一行 bare `{` + 次行 accessor / Allman / multi-line 式本体プロパティの参照属性、Allman と multi-line 式本体プロパティのブロックコメント挟み込み版、既存の ternary-continuation caller テスト、K&R 形(既存テストでカバー)、bare `{` + 次行 accessor 形、Allman block-body 形、multi-line 式本体プロパティ形、そしてブロックコメント挟み込みの両形の CLI `callers` end-to-end 検証まで押さえる回帰テストを追加。対象: `src/CodeIndex/Indexer/SymbolExtractor.cs`、`src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/SymbolExtractorTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`、`tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`。Closes #233。 - **C# 13 の partial property がシンボル抽出から欠落しないよう修正 (#228)** — C# extractor が property の `partial` 修飾子を受け付け、さらに複数行 header を先読みするようになった。これにより brace-on-next-line の宣言、block-bodied な実装、複数行の expression-bodied property、`partial override` 形が `symbols`、`definition`、`outline`、`hotspots` などのシンボル系コマンドから消えなくなった。複数行 declaration-style、block-bodied implementation、複数行 `partial override` property を押さえる回帰テストも追加。対象: `src/CodeIndex/Indexer/SymbolExtractor.cs`、`tests/CodeIndex.Tests/SymbolExtractorTests.cs`。Closes #228。 diff --git a/src/CodeIndex/Database/DbReader.cs b/src/CodeIndex/Database/DbReader.cs index 058af308d9..34201ab1a7 100644 --- a/src/CodeIndex/Database/DbReader.cs +++ b/src/CodeIndex/Database/DbReader.cs @@ -1306,13 +1306,19 @@ private List GetCallersExact(string symbolName, int limit, int off : @" AND r.symbol_name = @symbolName COLLATE NOCASE"; + // impact BFS must share the call-graph contract with `callers`/`callees`/`hotspots`, + // so event subscriptions (`Click += OnClick`) also participate in the transitive + // caller chain. Metadata edges (`attribute`, `annotation`) stay excluded. + // impact の BFS は `callers`/`callees`/`hotspots` と同じ call-graph 契約を共有し、 + // `subscribe` エッジ(`Click += OnClick` 等)も推移 caller に含める。`attribute` / + // `annotation` のような metadata エッジは引き続き除外する。 var sql = $@" WITH logical_references AS ( SELECT f.path, f.lang, r.container_kind, r.container_name, r.symbol_name, r.line FROM symbol_references r JOIN files f ON r.file_id = f.id WHERE r.container_name IS NOT NULL - AND r.reference_kind IN {InvokeReferenceKindsSql} + AND r.reference_kind IN {CallGraphReferenceKindsSql} AND {supportedLangFilter} {nameCondition}"; if (lang != null) diff --git a/src/CodeIndex/Indexer/ReferenceExtractor.cs b/src/CodeIndex/Indexer/ReferenceExtractor.cs index 358297fbd5..ac543f844e 100644 --- a/src/CodeIndex/Indexer/ReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/ReferenceExtractor.cs @@ -132,13 +132,6 @@ public static class ReferenceExtractor // C# イベント購読・解除: Click += OnClick — LHS と RHS の両方が PascalCase 識別子のみ private static readonly Regex EventSubscriptionRegex = new(@"(?[A-Z]\w*)\s*[+-]=\s*(?:new\s+)?[A-Z]\w*", RegexOptions.Compiled); - // C# targeted-attribute prefixes (e.g. [return: NotNull], [assembly: InternalsVisibleTo(...)]). - // C# のターゲット付き属性の prefix。 - private static readonly HashSet CSharpAttributeTargets = new(StringComparer.Ordinal) - { - "return", "param", "method", "type", "field", "property", "event", "assembly", "module", - }; - // Languages whose `@Decorator(args)` / `@Annotation(args)` syntax should produce // `annotation` reference rows rather than `call` rows (issue #293). // `@Decorator(args)` / `@Annotation(args)` 構文を `call` ではなく `annotation` として @@ -190,6 +183,16 @@ public static List Extract(long fileId, string? lang, string co var lines = content.Split('\n'); var structuralLines = StructuralLineMasker.MaskLines(language, lines); + var preparedLines = new string[lines.Length]; + for (var pi = 0; pi < lines.Length; pi++) + preparedLines[pi] = PrepareLine(language, structuralLines[pi]); + // Pre-pass C# attribute analysis so cross-line `[\n Foo("x")\n]` and parameter + // attributes `void M([Attr] T x)` are classified consistently with same-line `[Foo]`. + // 行を跨いだ `[\n Foo("x")\n]` やパラメータ属性 `void M([Attr] T x)` も、同一行の `[Foo]` と + // 同じ判定で属性として扱えるように、事前パスで C# 属性セクションの範囲を構築する。 + var csharpAttrRanges = language == "csharp" + ? BuildCSharpAttributeRanges(preparedLines) + : null; var definitionNamesByLine = symbols .GroupBy(symbol => symbol.Line) .ToDictionary(group => group.Key, group => group.Select(symbol => symbol.Name).ToHashSet(StringComparer.Ordinal)); @@ -212,9 +215,10 @@ public static List Extract(long fileId, string? lang, string co { var lineNumber = i + 1; var originalLine = lines[i]; - var preparedLine = PrepareLine(language, structuralLines[i]); + var preparedLine = preparedLines[i]; if (string.IsNullOrWhiteSpace(preparedLine)) continue; + var csharpAttrRangesOnLine = csharpAttrRanges?[i]; var context = originalLine.Trim(); if (context.Length == 0) @@ -250,7 +254,9 @@ public static List Extract(long fileId, string? lang, string co // usages with arguments so they do not pollute the call-graph as phantom `call` rows. // issue #293: 引数付きの C# attribute と Java/Kotlin/Scala/TypeScript annotation 使用を // `call` ではなく専用の種別に分類し、call-graph の phantom エッジを防ぐ。 - var metadataKind = TryClassifyMetadataReference(language, preparedLine, nameIndex); + var insideCSharpAttributeRange = csharpAttrRangesOnLine != null + && IsInsideCSharpAttributeRange(csharpAttrRangesOnLine, nameIndex); + var metadataKind = TryClassifyMetadataReference(language, preparedLine, nameIndex, insideCSharpAttributeRange); AddReference(references, seen, fileId, match, metadataKind ?? "call", context, lineNumber, container); } } @@ -417,160 +423,242 @@ private static bool IsIdentifierChar(char ch) => /// 呼び出しに見える識別子を、C# の `[...]` 属性リスト内や Java 系 `@` 付き注釈に該当する /// 場合に専用の reference kind へ分類する。通常の呼び出しは null を返して既定の `call` を維持する。 /// - private static string? TryClassifyMetadataReference(string language, string preparedLine, int nameIndex) + private static string? TryClassifyMetadataReference( + string language, + string preparedLine, + int nameIndex, + bool insideCSharpAttributeRange) { + if (language == "csharp") + return insideCSharpAttributeRange ? "attribute" : null; + var probe = nameIndex - 1; while (probe >= 0 && char.IsWhiteSpace(preparedLine[probe])) probe--; if (probe < 0) return null; - if (language == "csharp") - return IsCSharpAttributeContext(preparedLine, probe) ? "attribute" : null; - if (AnnotationLanguages.Contains(language)) return IsAnnotationContext(preparedLine, probe) ? "annotation" : null; return null; } - private static bool IsCSharpAttributeContext(string line, int probe) + /// + /// Build per-line column ranges that identify C# `[...]` attribute sections. Handles + /// declaration-position detection (including parameter attributes preceded by `(` / `,` + /// via forward look-ahead) and multi-line `[\n ... \n]` sections. Each inner list holds + /// ordered `(startColumn, endColumnExclusive)` ranges that are inside an attribute section + /// on that line. Call sites whose name column falls inside one of these ranges are + /// reclassified as `attribute` instead of `call`. + /// C# の `[...]` 属性セクションを行ごとの列範囲で表すテーブルを構築する。 + /// `(` / `,` の直後に置かれるパラメータ属性を forward lookahead で、複数行にわたる + /// `[\n ... \n]` 属性を跨行トラッキングで検出する。各行のリストは属性セクションに含まれる + /// `(開始列, 終端列 (exclusive))` のレンジを保持し、呼び出し名の列がどれかのレンジに含まれる場合に + /// `call` ではなく `attribute` へ再分類する。 + /// + private static List> BuildCSharpAttributeRanges(string[] preparedLines) { - // Direct form: `[Foo(...)`. 直接形 `[Foo(...)`。 - // Must verify that this `[` sits at a declaration position, not inside a C# 12 - // collection expression such as `var xs = [Make(), Make()]`. - // この `[` が宣言位置(`var xs = [...]` のような C# 12 collection expression ではない) - // であることを必ず確認する必要がある。 - if (line[probe] == '[') - return IsCSharpAttributeOpenBracket(line, probe); - - // Targeted form: `[target: Foo(...)]` — peek past the `target:` prefix. - // ターゲット指定形式 `[target: Foo(...)]` — `target:` 部分をスキップして `[` を探す。 - if (line[probe] == ':') + var perLine = new List>(preparedLines.Length); + for (var i = 0; i < preparedLines.Length; i++) + perLine.Add(new List<(int, int)>()); + + // Stack entries capture the opening `[` position and whether that bracket was at + // a C# declaration (attribute) position. Parens inside a section are tracked so the + // stack only pops when the bracket actually closes, not when an inner `)` appears. + // スタックは `[` の位置と、その bracket が属性位置だったかを保持する。セクション内の + // 括弧は独立して追跡し、内部の `)` によってスタックが誤って pop されないようにする。 + var bracketStack = new Stack<(int li, int ci, bool isAttr)>(); + char lastMeaningful = '\0'; + int parenDepth = 0; + bool lastClosedBracketWasAttribute = false; + + for (var li = 0; li < preparedLines.Length; li++) { - var j = probe - 1; - var idEnd = j; - while (j >= 0 && IsIdentifierChar(line[j])) - j--; - if (j + 1 <= idEnd) + var line = preparedLines[li]; + for (var ci = 0; ci < line.Length; ci++) { - var target = line[(j + 1)..(idEnd + 1)]; - if (CSharpAttributeTargets.Contains(target)) + var c = line[ci]; + if (char.IsWhiteSpace(c)) + continue; + + if (c == '(') { - var k = j; - while (k >= 0 && char.IsWhiteSpace(line[k])) - k--; - if (k >= 0 && line[k] == '[' && IsCSharpAttributeOpenBracket(line, k)) - return true; + parenDepth++; + lastMeaningful = c; + continue; + } + if (c == ')') + { + if (parenDepth > 0) + parenDepth--; + lastMeaningful = c; + continue; } + + if (c == '[') + { + bool isAttr = EvaluateCSharpAttributePosition( + lastMeaningful, lastClosedBracketWasAttribute, preparedLines, li, ci); + bracketStack.Push((li, ci, isAttr)); + lastMeaningful = c; + continue; + } + + if (c == ']') + { + if (bracketStack.Count > 0) + { + var opened = bracketStack.Pop(); + lastClosedBracketWasAttribute = opened.isAttr; + if (opened.isAttr) + { + // Record the attribute section span for every line it covers so + // cross-line `[\n Foo("x")\n]` also classifies `Foo` as attribute. + // 属性セクションがまたぐ全ての行に対して範囲を記録し、 + // `[\n Foo("x")\n]` のような跨行ケースでも `Foo` が属性として分類されるようにする。 + for (var l = opened.li; l <= li; l++) + { + int s = (l == opened.li) ? opened.ci : 0; + int e = (l == li) ? ci + 1 : preparedLines[l].Length; + perLine[l].Add((s, e)); + } + } + } + else + { + lastClosedBracketWasAttribute = false; + } + lastMeaningful = c; + continue; + } + + lastMeaningful = c; } - return false; } - // Comma-separated list: `[Foo("a"), Bar("b")]` — walk past prior attribute entries - // (and any balanced parens they contain) until an unbalanced opening `[` appears. - // 一行に並んだ複数属性 `[Foo("a"), Bar("b")]` では、先行する属性と対応する括弧を - // 読み飛ばして未対応の `[` に到達した場合にのみ属性として扱う。 - if (line[probe] == ',') - return ScanLeftForAttributeOpen(line, probe - 1); - - return false; + return perLine; } /// - /// Return true when the `[` at is at a C# declaration - /// position rather than an expression position. Attribute `[` must be preceded on the - /// same line only by whitespace, another attribute list's closing `]`, or a scope/ - /// statement boundary (`{`, `}`, `;`). Any expression-context token (identifier, digit, - /// `=`, `,`, `(`, `:`, etc.) means this is a collection expression or indexer. - /// `[` が C# の宣言位置にあるか(collection expression や indexer ではないか)を判定する。 + /// Decide whether a `[` token sits at a C# attribute position based on the immediately + /// preceding meaningful character. `(` / `,` (parameter attributes) are disambiguated via + /// forward look-ahead because both attributes and C# 12 collection expressions can follow. + /// `[` が C# の属性位置にあるかを、直前の非空白文字から判定する。`(` / `,` の直後は + /// パラメータ属性にも collection expression にもなりうるため、forward lookahead で区別する。 /// - private static bool IsCSharpAttributeOpenBracket(string line, int bracketIndex) + private static bool EvaluateCSharpAttributePosition( + char lastMeaningful, + bool lastClosedBracketWasAttribute, + string[] preparedLines, + int startLi, + int startCi) { - var i = bracketIndex - 1; - while (i >= 0 && char.IsWhiteSpace(line[i])) - i--; - if (i < 0) + // Start of file or after a scope/statement boundary — attribute position. + // ファイル先頭、あるいはスコープ・文境界の直後は属性位置。 + if (lastMeaningful is '\0' or '{' or '}' or ';') return true; - var c = line[i]; - if (c == '{' || c == '}' || c == ';') - return true; - // A preceding `]` only indicates attribute-list chaining (`[A][B]`) when that `]` - // actually closed an attribute section. For `arr[i][Compute()]` the preceding `]` - // closes an indexer, so we must walk back to the matching `[` and re-check that - // opening bracket's declaration position. - // 直前の `]` が attribute list のチェーン (`[A][B]`) を意味するのは、その `]` が - // 実際に attribute section を閉じていたときだけ。`arr[i][Compute()]` のように - // indexer を閉じた `]` の場合は、対応する `[` まで戻ってそこが宣言位置かを再判定する。 - if (c == ']') - return IsCSharpAttributeSectionClose(line, i); + + // Chained attribute list `[A][B]`: the prior `]` must have closed an attribute section. + // `arr[i][Compute()]` → the prior `]` closed an indexer, so stays `call`. + // 連続した属性リスト `[A][B]` は、直前の `]` が属性セクションを閉じていたときのみ属性扱い。 + // `arr[i][Compute()]` の `]` は indexer を閉じているため `call` のまま。 + if (lastMeaningful == ']') + return lastClosedBracketWasAttribute; + + // Parameter attribute candidates (`(` or `,`): could be `void M([Attr] T x)` or + // `Consume([Make()])`. Disambiguate by scanning forward to the matching `]` and + // checking the next meaningful character; identifier-like → attribute, otherwise expression. + // パラメータ属性候補 (`(` / `,`) は `void M([Attr] T x)` にも `Consume([Make()])` にもなりうる。 + // 対応する `]` まで進んで直後の文字を確認し、識別子なら属性、それ以外は式として扱う。 + if (lastMeaningful is '(' or ',') + return IsCSharpAttributeFollowedByDeclaration(preparedLines, startLi, startCi); + return false; } /// - /// Walk left from a `]` to its matching `[`, skipping balanced parens, and return true - /// when that opening bracket itself was at a C# declaration position. - /// `]` から対応する `[` まで左方向に遡り、その開き bracket 自体が宣言位置だった場合のみ true を返す。 + /// Scan forward from a `[` to its matching `]` (skipping balanced parens) and return true + /// when the next meaningful character begins an identifier-like token. Works across lines so + /// `void M(\n [Attr]\n T x\n)` is recognized as a parameter attribute. + /// `[` から対応する `]` まで進んで、`]` の次の非空白文字が識別子を始める場合に true を返す。 + /// 行を跨ぐ走査にも対応しているため `void M(\n [Attr]\n T x\n)` も属性として認識される。 /// - private static bool IsCSharpAttributeSectionClose(string line, int closeBracketIndex) + private static bool IsCSharpAttributeFollowedByDeclaration(string[] preparedLines, int startLi, int startCi) { var bracketDepth = 1; var parenDepth = 0; - for (var i = closeBracketIndex - 1; i >= 0; i--) + var li = startLi; + var ci = startCi + 1; + while (li < preparedLines.Length) { - var c = line[i]; - if (c == ')') - { - parenDepth++; - continue; - } - if (c == '(') + var line = preparedLines[li]; + while (ci < line.Length) { + var c = line[ci]; + if (c == '(') + { + parenDepth++; + ci++; + continue; + } + if (c == ')') + { + if (parenDepth > 0) + parenDepth--; + ci++; + continue; + } if (parenDepth > 0) - parenDepth--; - continue; - } - if (parenDepth > 0) - continue; - if (c == ']') - { - bracketDepth++; - continue; - } - if (c == '[') - { - bracketDepth--; - if (bracketDepth == 0) - return IsCSharpAttributeOpenBracket(line, i); + { + ci++; + continue; + } + if (c == '[') + { + bracketDepth++; + ci++; + continue; + } + if (c == ']') + { + bracketDepth--; + if (bracketDepth == 0) + { + ci++; + while (true) + { + while (ci < line.Length && char.IsWhiteSpace(line[ci])) + ci++; + if (ci < line.Length) + { + var next = line[ci]; + return IsIdentifierChar(next) || next == '@'; + } + li++; + if (li >= preparedLines.Length) + return false; + line = preparedLines[li]; + ci = 0; + } + } + ci++; + continue; + } + ci++; } + li++; + ci = 0; } return false; } - private static bool ScanLeftForAttributeOpen(string line, int start) + private static bool IsInsideCSharpAttributeRange(List<(int start, int end)> ranges, int index) { - var parenDepth = 0; - for (var i = start; i >= 0; i--) + foreach (var (start, end) in ranges) { - var c = line[i]; - if (c == ')') - { - parenDepth++; - continue; - } - if (c == '(') - { - if (parenDepth == 0) - return false; - parenDepth--; - continue; - } - if (parenDepth > 0) - continue; - if (c == '[') - return IsCSharpAttributeOpenBracket(line, i); - if (c == ']' || c == ';' || c == '{' || c == '}') - return false; + if (index >= start && index < end) + return true; } return false; } diff --git a/tests/CodeIndex.Tests/DbReaderTests.cs b/tests/CodeIndex.Tests/DbReaderTests.cs index 138c0c7ed7..1348fa8fb6 100644 --- a/tests/CodeIndex.Tests/DbReaderTests.cs +++ b/tests/CodeIndex.Tests/DbReaderTests.cs @@ -2398,6 +2398,46 @@ private void OnChanged(object? sender, EventArgs e) { } Assert.Equal("subscribe", bundledCallee.ReferenceKind); } + [Fact] + public void GetTransitiveCallers_FollowsSubscribeEdges() + { + // Regression: impact BFS must share the call-graph contract with callers/callees, + // so event subscriptions (`Changed += OnChanged`) also participate in the transitive + // caller chain rather than being stripped like metadata edges. + // リグレッション: impact BFS も callers/callees と同じ call-graph 契約を共有し、 + // イベント購読 (`Changed += OnChanged`) が transitive caller chain に含まれること。 + InsertIndexedFile("src/impact_subscribe_publisher.cs", "csharp", + """ + using System; + + public class SubPublisher + { + public event EventHandler? Changed; + } + """); + InsertIndexedFile("src/impact_subscribe_subscriber.cs", "csharp", + """ + using System; + + public class SubSubscriber + { + public void Hook(SubPublisher publisher) + { + publisher.Changed += OnChanged; + } + + private void OnChanged(object? sender, EventArgs e) { } + } + """); + + var (impact, truncated) = _reader.GetTransitiveCallers( + "Changed", maxDepth: 2, limit: 10, lang: "csharp", pathPatterns: ["impact_subscribe_"]); + + Assert.False(truncated); + var caller = Assert.Single(impact); + Assert.Equal("Hook", caller.CallerName); + } + [Fact] public void GetTransitiveCallers_ReturnsAllDirectCallersAcrossPages() { diff --git a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs index d31acd240e..f2f0816c28 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs @@ -1417,6 +1417,125 @@ public class C { } Assert.Equal("attribute", b.ReferenceKind); } + [Fact] + public void Extract_CsharpParameterAttributes_ClassifiedAsAttribute() + { + // Parameter attributes are introduced by `(` or `,` rather than a scope boundary, so + // the classifier must use forward lookahead from `[` to disambiguate against C# 12 + // collection expressions in argument position like `Consume([Make()])`. + // パラメータ属性は `(` や `,` に続くため、collection expression と区別するには `[` から + // 対応する `]` まで前方を走査して、直後が識別子かを確認する必要がある。 + const string content = """ + public class C + { + public void M([Attr("x")] int a, [Other("y")] int b) { } + } + """; + + var symbols = SymbolExtractor.Extract(1, "csharp", content); + var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); + + var attr = Assert.Single(references.Where(r => r.SymbolName == "Attr")); + var other = Assert.Single(references.Where(r => r.SymbolName == "Other")); + Assert.Equal("attribute", attr.ReferenceKind); + Assert.Equal("attribute", other.ReferenceKind); + } + + [Fact] + public void Extract_CsharpMultiLineAttribute_ClassifiedAsAttribute() + { + // A multi-line attribute list `[\n Foo("x")\n ]` must still classify `Foo` as + // attribute even though the opening `[` is not on the same line as the identifier. + // `[` と `Foo("x")` が別行にある場合でも `Foo` を属性として判定すること。 + const string content = """ + [ + Foo("x") + ] + public class C { } + """; + + var symbols = SymbolExtractor.Extract(1, "csharp", content); + var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); + + var foo = Assert.Single(references.Where(r => r.SymbolName == "Foo")); + Assert.Equal("attribute", foo.ReferenceKind); + } + + [Fact] + public void Extract_CsharpMultiLineParameterAttribute_ClassifiedAsAttribute() + { + // Parameter attribute split across lines — `(` ends one line, `[Attr]` sits on the + // next, and the declaration continues after. Cross-line lookahead must still find + // the identifier after the matching `]`. + // 改行を挟んだパラメータ属性でも、跨行 lookahead で `]` の直後に続く識別子まで到達し、 + // 属性として判定できること。 + const string content = """ + public class C + { + public void M( + [Attr("x")] + int a) + { + } + } + """; + + var symbols = SymbolExtractor.Extract(1, "csharp", content); + var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); + + var attr = Assert.Single(references.Where(r => r.SymbolName == "Attr")); + Assert.Equal("attribute", attr.ReferenceKind); + } + + [Fact] + public void Extract_CsharpTargetedAttribute_StaysAttribute() + { + // Regression: `[return: NotNullWhen(true)]` is recognised as an attribute section by + // the pre-pass (bracket position, not `target:` heuristics). Keep the case covered. + // リグレッション: `[return: NotNullWhen(true)]` も属性セクションとして判定されること。 + const string content = """ + public class C + { + [return: NotNullWhen(true)] + public bool Try() => true; + } + """; + + var symbols = SymbolExtractor.Extract(1, "csharp", content); + var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); + + var notNullWhen = Assert.Single(references.Where(r => r.SymbolName == "NotNullWhen")); + Assert.Equal("attribute", notNullWhen.ReferenceKind); + } + + [Fact] + public void Extract_CsharpCollectionExpressionInArgument_StaysCallAfterParen() + { + // Defense-in-depth: `Consume([Make()])` has `[` immediately after `(`, matching the + // parameter-attribute entry point, but forward lookahead sees `)` after the matching + // `]` and correctly keeps `Make` as `call`. + // `Consume([Make()])` のように `(` 直後に `[` が続くケースでも、`]` の直後が `)` であれば + // collection expression として `call` のままであること。 + const string content = """ + public class C + { + public void M() + { + Consume([Make(), Make()]); + } + private static int Make() => 0; + private void Consume(int[] xs) { } + } + """; + + var symbols = SymbolExtractor.Extract(1, "csharp", content); + var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); + + var makeRefs = references.Where(r => r.SymbolName == "Make").ToList(); + Assert.Equal(2, makeRefs.Count); + Assert.All(makeRefs, r => Assert.Equal("call", r.ReferenceKind)); + } + [Fact] public void Extract_KotlinQualifiedFieldTargetAnnotation_ClassifiedAsAnnotation() { From fdfd147fd739da916860e8be1cea0691f4dcda95 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 17 Apr 2026 20:53:48 +0900 Subject: [PATCH 05/30] Fix #293 handle pattern/cast/switch after C# collection expr and type-parameter attrs Follow-up to the #293 attribute classification work: codex adversarial review caught two gaps in the parameter-attribute pre-pass. 1. `Consume([Make()] is int[] xs)` and `[Make()] as int[]` / `[Make()] switch { _ => true }` were wrongly classified as attribute because the lookahead only checked that the next char was an identifier. `is` / `as` / `switch` / `with` / `when` are expression-continuation keywords, so the enclosing `[...]` is a collection / pattern expression, not an attribute section. `Make` vanished from `callers` / `impact` as a result. 2. `class C<[Attr("x")] T>` and `void M([Attr("x")] (int, int) value)` were still `call`: the first because `<` was not an accepted attribute-position lead, the second because the token after `]` was `(` rather than an identifier. The lookahead now: - Accepts `<` as a parameter-list / type-parameter entry point. - Treats `is` / `as` / `switch` / `with` / `when` as expression continuation (rejects the attribute interpretation). - Accepts `(` for tuple-typed parameter declarations. - Accepts `@` for verbatim identifiers. - Recurses through chained `[A][B]` via the inner bracket. Co-Authored-By: Claude Opus 4 --- CHANGELOG.md | 4 +- src/CodeIndex/Indexer/ReferenceExtractor.cs | 78 ++++++++---- .../ReferenceExtractorTests.cs | 112 ++++++++++++++++++ 3 files changed, 171 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4da5f189f3..bf19cc18e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), #### Fixed - **`impact` BFS now follows C# event subscribe edges (#293 follow-up)** — `DbReader.GetCallersExact`, the only caller site used by the `impact` / `impact_analysis` BFS, now filters references through the shared `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` set instead of `InvokeReferenceKindsSql = ('call', 'instantiate')`. Previously `impact` quietly dropped `Changed += OnChanged` subscribe rows while `callers` / `callees` / `hotspots` kept them, so an event-driven transitive caller chain appeared shorter under `impact` than under `callers`. A new `DbReaderTests.GetTransitiveCallers_FollowsSubscribeEdges` regression pins the subscribe edge into the BFS output. Metadata edges (`attribute`, `annotation`) remain excluded. Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. -- **C# parameter attributes and multi-line `[...]` attribute lists are now classified as `attribute` instead of `call` (#293 follow-up)** — `ReferenceExtractor` now runs a pre-pass that tokenizes every C# `[...]` section across the whole file, so parameter attributes such as `void M([FromServices] IService s)` (where `[` is preceded by `(` or `,` rather than a declaration boundary) and multi-line forms such as `[\n Foo("x")\n]` or `public void M(\n [Attr("x")]\n int a)` are recognized alongside the existing same-line cases. The pre-pass disambiguates the `(` / `,` entry point against C# 12 collection expressions in argument position (`Consume([Make()])`) by scanning forward from `[` to the matching `]` and checking whether the next meaningful character begins an identifier. Targeted forms (`[return: NotNullWhen(true)]`) are now recognized directly from their `[...]` section instead of via the previous `target:` heuristic. Regression coverage pins `[Attr("x")] int a, [Other("y")] int b`, `[\n Foo("x")\n]`, the cross-line parameter attribute shape, the targeted `[return: ...]` shape, and the defense-in-depth collection-expression-in-argument case. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. +- **C# parameter, type-parameter, and multi-line `[...]` attribute lists are now classified as `attribute` instead of `call`, and collection-expression pattern / `as` / `switch` forms stay `call` (#293 follow-up)** — `ReferenceExtractor` now runs a pre-pass that tokenizes every C# `[...]` section across the whole file, so parameter attributes such as `void M([FromServices] IService s)` (where `[` is preceded by `(` or `,` rather than a declaration boundary), type-parameter attributes such as `class C<[Attr("x")] T>` (preceded by `<`), tuple-typed parameter attributes such as `void M([Attr("x")] (int a, int b) value)` (where the next token after `]` is `(`), and multi-line forms such as `[\n Foo("x")\n]` or `public void M(\n [Attr("x")]\n int a)` are recognized alongside the existing same-line cases. The pre-pass disambiguates the `(` / `,` / `<` entry point against C# 12 collection expressions in argument position (`Consume([Make()])`), pattern expressions (`[Make()] is int[] xs`), `as` casts (`[Make()] as int[]`), and switch expressions (`[Make()] switch { _ => true }`) by scanning forward from `[` to the matching `]` and checking whether the next meaningful token begins a declaration — identifiers are accepted unless they are the expression-continuation keywords `is` / `as` / `switch` / `with` / `when`, `@` is accepted as a verbatim-identifier lead, `(` is accepted for tuple-typed parameters, and chained `[A][B]` recurses through the inner bracket. Targeted forms (`[return: NotNullWhen(true)]`) are now recognized directly from their `[...]` section instead of via the previous `target:` heuristic. Regression coverage pins `[Attr("x")] int a, [Other("y")] int b`, type-parameter `class C<[Attr("x")] T>`, tuple-typed parameter `void M([Attr("x")] (int a, int b) value)`, `[\n Foo("x")\n]`, the cross-line parameter attribute shape, the targeted `[return: ...]` shape, the defense-in-depth collection-expression-in-argument case, and the three expression-continuation cases `[Make()] is ...`, `[Make()] as int[]`, and `[Make()] switch { ... }`. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. - **C# attribute and Java/Kotlin/Scala/TypeScript annotation usages no longer pollute the call-graph as phantom `call` rows (#293)** — `ReferenceExtractor` now reclassifies identifiers that appear inside C# `[...]` attribute lists (including direct `[Attr(args)]`, targeted `[return: Attr]` / `[assembly: Attr]` forms, and comma-separated `[Foo("a"), Bar("b")]` lists) as `attribute` references, and identifiers preceded by a Java-family `@` marker (optionally through a dotted qualifier chain such as `@org.junit.Test(args)`, or through a Kotlin use-site target such as `@field:Deprecated("msg")` / `@get:JsonName("x")` / `@file:JvmName("Foo")`) as `annotation` references. Attribute `[` detection is gated by a declaration-position guard so C# 12 collection expressions (`var xs = [Make(), Make()]`, `Consume([Make(), Make()])`, `return [Make(), Make()]`) and indexer accesses (`arr[Compute()]`, chained `arr[Compute()][Compute()]`, `matrix[Row()][Col()]`) keep their inner calls as `call` instead of being misclassified as metadata — when the `[` is preceded by another `]`, the walk-back now finds the matching `[` (skipping balanced parens) and re-checks that opening bracket's declaration position so `[A("x")][B("y")]` chained attribute lists still classify as `attribute` while `arr[i][Compute()]` stays `call`. Kotlin use-site target detection also handles the qualifier-chain combination `@field:com.example.Deprecated("msg")` / `@get:com.fasterxml.jackson.annotation.JsonProperty("x")` by unwinding the dotted qualifier first and then matching either `@` or a Kotlin `target:` + `@`. The shared graph filter `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` is applied to default-kind `callers`, `callees`, count queries, hotspot CTEs, and symbol-hotspot grouping, so `callers Obsolete`, `callers Deprecated`, `impact Conditional`, and `hotspots` no longer surface phantom caller edges or inflate hotspot counts for metadata-only references. `references` defaults keep every indexed reference kind visible so metadata usages remain inspectable; `references --kind attribute` / `--kind annotation` (and the MCP tool equivalents) explicitly surface metadata rows, and the CLI help / `--kind` validator / README now list the new reference kinds and document the new default contract for `callers` / `callees` / `hotspots` / `impact`. Regression coverage pins the new classification for `[Obsolete("msg")]`, `[return: NotNull("x")]`, `[Foo("a"), Bar("b")]`, `[AttributeUsage(...)]`, Java `@Deprecated(since="1.0")`, qualified `@org.junit.Test(...)`, Kotlin `@Deprecated("msg")`, Kotlin `@field:Deprecated` / `@get:JsonName` / `@file:JvmName`, ordinary method bodies still emitting `call`, and C# collection-expression / indexer call sites staying `call`. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Database/DbReader.cs`, `src/CodeIndex/Database/DbSymbolReader.cs`, `src/CodeIndex/Cli/QueryCommandRunner.cs`, `src/CodeIndex/Cli/ConsoleUi.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`, `tests/CodeIndex.Tests/ConsoleUiTests.cs`, `README.md`, `DEVELOPER_GUIDE.md`. Closes #293. - **C# expression-bodied and block-bodied property callers attribute to the individual method/property instead of the enclosing class (#233)** — `SymbolExtractor` now detects `=> expr;` at top level inside `FindCSharpBraceRange` and assigns a body range covering the declaration line through the terminating `;` (including the multi-line form where `=>` is placed on a following indented line, both for methods and for expression-bodied properties). The block-bodied property scanner now also merges the common Microsoft-style `public int Wrap {` + next-line `get { ... }` shape with its accessor line so that form is recognized alongside the existing K&R (`public int Wrap { get { ... } }`) and Allman (`public int Wrap` then `{` on the next line) shapes, without misclassifying `public class X {` or other non-property headers. Block comments spanning several lines between the header and the body are handled correctly by the same line-scanning path. `ReferenceExtractor.FindInnermostContainer` now accepts `property` as a container kind alongside `function` / `class` / `namespace`. As a result `callers `, `impact_analysis`, and the caller rows surfaced by `inspect` / `analyze_symbol` no longer collapse `public int Wrap() => Compute();`, `public int Wrap => Compute();`, the block-bodied `public int Wrap { get { return Compute(); } }` (K&R), `public int Wrap {` / ` get { return Compute(); }` / `}` (same-line brace with next-line accessor), the Allman-style `public int Wrap` / `{` / ` get { return Compute(); }` / `}`, the multi-line expression-bodied `public int Wrap` / ` => Compute();`, or the same shapes with multi-line block comments between header and body into a single enclosing-class row — each caller now appears on its own row with the correct `caller_kind` (`function` or `property`) and `caller_name`. Regression coverage spans symbol body ranges, all three block-bodied property shapes plus a misclassification guard for bare-brace lines whose body is not an accessor, accessor-attribute and visibility-prefixed accessor lines, reference attribution for expression-bodied methods / properties / multi-line `=> expr;` / K&R block-bodied accessor / bare-brace-same-line accessor-next-line / Allman-style / multi-line expression-bodied property forms, the block-comment-between-header-and-body variants for both Allman and multi-line expression-bodied properties, the previously-encoded ternary-continuation caller test, and end-to-end `callers` assertions through the CLI for the K&R shape (covered by existing property tests), the bare-brace-same-line shape, the Allman block-body shape, the multi-line expression-bodied shape, and both block-comment-separated variants. Affected: `src/CodeIndex/Indexer/SymbolExtractor.cs`, `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/SymbolExtractorTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`, `tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`. Closes #233. - **C# 13 partial properties now participate in symbol extraction (#228)** — The C# extractor now accepts the `partial` modifier on properties and looks ahead across multiline headers, so brace-on-next-line declarations, block-bodied implementations, multiline expression-bodied properties, and `partial override` forms no longer disappear from `symbols`, `definition`, `outline`, `hotspots`, and related symbol-driven commands. Added regression coverage for multiline declaration-style, block-bodied implementation, and multiline `partial override` properties. Affected: `src/CodeIndex/Indexer/SymbolExtractor.cs`, `tests/CodeIndex.Tests/SymbolExtractorTests.cs`. Closes #228. @@ -681,7 +681,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), #### 修正 - **`impact` BFS が C# の event subscribe edge を辿るよう修正 (#293 追加対応)** — `impact` / `impact_analysis` BFS の唯一の呼び出し箇所である `DbReader.GetCallersExact` が、共有 filter `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` で参照を絞り込むようになった(以前は `InvokeReferenceKindsSql = ('call', 'instantiate')` を使っていたため、`Changed += OnChanged` のような subscribe 行を静かに捨てていた)。これにより `callers` / `callees` / `hotspots` と同じ call-graph 契約を `impact` も共有し、イベント駆動の transitive caller chain が `impact` でだけ短くなる不整合が解消する。`DbReaderTests.GetTransitiveCallers_FollowsSubscribeEdges` の回帰テストで subscribe edge が BFS 出力に含まれることを固定。Metadata edge (`attribute` / `annotation`) の除外は従来通り。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 -- **C# のパラメータ属性と複数行 `[...]` 属性が `call` ではなく `attribute` として分類されるよう修正 (#293 追加対応)** — `ReferenceExtractor` がファイル全体の C# `[...]` セクションを事前パスでトークン化するようになり、`void M([FromServices] IService s)` のように `[` の直前が宣言境界ではなく `(` や `,` で始まるパラメータ属性と、`[\n Foo("x")\n]` や `public void M(\n [Attr("x")]\n int a)` のような複数行形式も、既存の同一行ケースと同じように属性として扱われるようになった。`(` / `,` 直後の曖昧さは、`[` から対応する `]` まで前方走査し、`]` の直後の非空白文字が識別子を始めるかで判定するため、引数位置の C# 12 collection expression (`Consume([Make()])`) は `call` のまま残る。target 付き形 (`[return: NotNullWhen(true)]`) も、以前のような `target:` 個別ヒューリスティクスではなく `[...]` セクションそのものから判定する形に統一された。`[Attr("x")] int a, [Other("y")] int b`、`[\n Foo("x")\n]`、改行を挟んだパラメータ属性、`[return: NotNullWhen(true)]`、そして防御的に `Consume([Make()])` の collection expression-in-argument を押さえる回帰テストを追加。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 +- **C# のパラメータ属性・型パラメータ属性・複数行 `[...]` 属性が `call` ではなく `attribute` として分類され、collection expression のパターン / `as` / `switch` は `call` のまま残るよう修正 (#293 追加対応)** — `ReferenceExtractor` がファイル全体の C# `[...]` セクションを事前パスでトークン化するようになり、`void M([FromServices] IService s)` のように `[` の直前が宣言境界ではなく `(` / `,` で始まるパラメータ属性、`class C<[Attr("x")] T>` のように `<` で始まる型パラメータ属性、`void M([Attr("x")] (int a, int b) value)` のように `]` の直後が `(` のまま続く tuple 型パラメータ属性、さらに `[\n Foo("x")\n]` や `public void M(\n [Attr("x")]\n int a)` のような複数行形式も、既存の同一行ケースと同じように属性として扱われるようになった。`(` / `,` / `<` 直後の曖昧さは、`[` から対応する `]` まで前方走査し、`]` の直後の非空白トークンが宣言を開始するかで判定する — 識別子は原則受理するが、式継続キーワード `is` / `as` / `switch` / `with` / `when` は宣言扱いしないので `Consume([Make()] is int[] xs)` / `([Make()] as int[])` / `Consume([Make()] switch { _ => true })` のような collection expression は `call` のまま残り、`@` は verbatim 識別子の先頭として、`(` は tuple 型パラメータの先頭として受理し、`[A][B]` のような連続 bracket は内側 `[` に再帰して判定する。target 付き形 (`[return: NotNullWhen(true)]`) も、以前のような `target:` 個別ヒューリスティクスではなく `[...]` セクションそのものから判定する形に統一された。`[Attr("x")] int a, [Other("y")] int b`、`class C<[Attr("x")] T>` 型パラメータ属性、`void M([Attr("x")] (int a, int b) value)` tuple 型パラメータ属性、`[\n Foo("x")\n]`、改行を挟んだパラメータ属性、`[return: NotNullWhen(true)]`、防御的に `Consume([Make()])` の collection expression-in-argument、および `[Make()] is ...` / `[Make()] as int[]` / `[Make()] switch { ... }` の 3 つの式継続ケースを押さえる回帰テストを追加。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 - **C# 属性と Java/Kotlin/Scala/TypeScript の annotation 使用が phantom な `call` 行として call-graph を汚染しないよう修正 (#293)** — `ReferenceExtractor` が C# の `[...]` 属性リスト内(直接形 `[Attr(args)]`、target 付き `[return: Attr]` / `[assembly: Attr]`、カンマ区切り `[Foo("a"), Bar("b")]` を含む)に現れる識別子を `attribute` 参照、Java 系 `@` マーカーが先行する識別子(`@org.junit.Test(args)` のようなドット修飾チェーン越し、および Kotlin の use-site target `@field:Deprecated("msg")` / `@get:JsonName("x")` / `@file:JvmName("Foo")` 越しを含む)を `annotation` 参照として分類するようになった。属性 `[` の判定には宣言位置ガードを入れ、C# 12 の collection expression(`var xs = [Make(), Make()]`、`Consume([Make(), Make()])`、`return [Make(), Make()]`)や indexer アクセス(`arr[Compute()]`、連続 `arr[Compute()][Compute()]`、`matrix[Row()][Col()]`)の内側の呼び出しは `call` のまま残り、metadata に誤分類されないようにした。`[` の直前が別の `]` だった場合は、対応する `[` まで左方向に(balanced paren を跨いで)巻き戻してその開き bracket 自体が宣言位置かを再判定するため、`[A("x")][B("y")]` のような連続 attribute list は引き続き `attribute` として認識される一方、`arr[i][Compute()]` のような連続 indexer は `call` のまま残る。Kotlin の use-site target 判定も、ドット修飾子チェーンを先に剥がしたうえで `@` または Kotlin の `target:` + `@` を判定する構成に変え、`@field:com.example.Deprecated("msg")` / `@get:com.fasterxml.jackson.annotation.JsonProperty("x")` のような修飾付き注釈名と use-site target の組み合わせにも対応するようにした。共有 filter `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` を default-kind の `callers`・`callees`・count クエリ・hotspot CTE・symbol-hotspot グルーピングに適用し、`callers Obsolete`・`callers Deprecated`・`impact Conditional`・`hotspots` で phantom caller edge や metadata のみの参照による件数膨張が発生しないようにした。`references` の既定は全 reference kind を表示して metadata 使用も確認できる状態を保ちつつ、`references --kind attribute` / `--kind annotation`(と MCP ツール)で metadata 行を明示取得できる。CLI help・`--kind` バリデータ・README も新しい reference kind と `callers` / `callees` / `hotspots` / `impact` の新しい既定契約を反映する。`[Obsolete("msg")]`・`[return: NotNull("x")]`・`[Foo("a"), Bar("b")]`・`[AttributeUsage(...)]`、Java `@Deprecated(since="1.0")`・修飾付き `@org.junit.Test(...)`、Kotlin `@Deprecated("msg")`、Kotlin `@field:Deprecated` / `@get:JsonName` / `@file:JvmName` の分類、通常のメソッド本体が引き続き `call` を返すこと、C# collection expression や indexer アクセス内の呼び出しが `call` のままであることを押さえる回帰テストを追加。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Database/DbReader.cs`、`src/CodeIndex/Database/DbSymbolReader.cs`、`src/CodeIndex/Cli/QueryCommandRunner.cs`、`src/CodeIndex/Cli/ConsoleUi.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`、`tests/CodeIndex.Tests/ConsoleUiTests.cs`、`README.md`、`DEVELOPER_GUIDE.md`。Closes #293。 - **C# 式本体メンバーと block-bodied プロパティの caller が外側クラスではなく個別メソッド/プロパティに帰属するよう修正 (#233)** — `SymbolExtractor` の `FindCSharpBraceRange` がトップレベルの `=> expr;` を検出し、宣言行から終端 `;` までを本体範囲として扱うようになった(宣言行の次行にインデントして `=>` を置く multi-line 形にも、メソッドと式本体プロパティの双方で対応)。block-bodied プロパティの行結合ロジックも、`public int Wrap {` の次行に ` get { ... }` を置く Microsoft 標準形を accessor 行と結合するよう拡張し、既存の K&R (`public int Wrap { get { ... } }`) や Allman (`public int Wrap` の次行に `{`) と並ぶ 3 形を抽出対象にしつつ、`public class X {` のような非プロパティ header を property と誤分類しないようにした。ヘッダと本体の間に複数行またがるブロックコメントが挟まる場合も同じ行スキャン経路で正しく処理される。`ReferenceExtractor.FindInnermostContainer` は `property` も `function` / `class` / `namespace` と並ぶ container kind として受け入れるようにした。これにより `callers ` / `impact_analysis` と `inspect` / `analyze_symbol` に現れる caller 行が、`public int Wrap() => Compute();`、`public int Wrap => Compute();`、K&R の `public int Wrap { get { return Compute(); } }`、`public int Wrap {` / ` get { return Compute(); }` / `}`(同一行 bare `{` + 次行 accessor)、Allman の `public int Wrap` / `{` / ` get { return Compute(); }` / `}`、次行に ` => Compute();` を置く multi-line 式本体プロパティ、さらにヘッダと本体の間に複数行のブロックコメントを挟む同種の形まで、外側クラスの 1 行にまとめてしまわず、各 caller が固有の `caller_kind`(`function` または `property`)と `caller_name` を持つ別行として現れる。シンボル本体範囲、3 形すべての block-bodied property 抽出と、本体が accessor でない bare `{` の誤分類防止ガード、accessor attribute / visibility 修飾子付き accessor 行、式本体メソッド / プロパティ / multi-line `=> expr;` / K&R block-bodied accessor / 同一行 bare `{` + 次行 accessor / Allman / multi-line 式本体プロパティの参照属性、Allman と multi-line 式本体プロパティのブロックコメント挟み込み版、既存の ternary-continuation caller テスト、K&R 形(既存テストでカバー)、bare `{` + 次行 accessor 形、Allman block-body 形、multi-line 式本体プロパティ形、そしてブロックコメント挟み込みの両形の CLI `callers` end-to-end 検証まで押さえる回帰テストを追加。対象: `src/CodeIndex/Indexer/SymbolExtractor.cs`、`src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/SymbolExtractorTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`、`tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`。Closes #233。 - **C# 13 の partial property がシンボル抽出から欠落しないよう修正 (#228)** — C# extractor が property の `partial` 修飾子を受け付け、さらに複数行 header を先読みするようになった。これにより brace-on-next-line の宣言、block-bodied な実装、複数行の expression-bodied property、`partial override` 形が `symbols`、`definition`、`outline`、`hotspots` などのシンボル系コマンドから消えなくなった。複数行 declaration-style、block-bodied implementation、複数行 `partial override` property を押さえる回帰テストも追加。対象: `src/CodeIndex/Indexer/SymbolExtractor.cs`、`tests/CodeIndex.Tests/SymbolExtractorTests.cs`。Closes #228。 diff --git a/src/CodeIndex/Indexer/ReferenceExtractor.cs b/src/CodeIndex/Indexer/ReferenceExtractor.cs index ac543f844e..4a1c1c7303 100644 --- a/src/CodeIndex/Indexer/ReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/ReferenceExtractor.cs @@ -566,17 +566,30 @@ private static bool EvaluateCSharpAttributePosition( if (lastMeaningful == ']') return lastClosedBracketWasAttribute; - // Parameter attribute candidates (`(` or `,`): could be `void M([Attr] T x)` or - // `Consume([Make()])`. Disambiguate by scanning forward to the matching `]` and - // checking the next meaningful character; identifier-like → attribute, otherwise expression. - // パラメータ属性候補 (`(` / `,`) は `void M([Attr] T x)` にも `Consume([Make()])` にもなりうる。 - // 対応する `]` まで進んで直後の文字を確認し、識別子なら属性、それ以外は式として扱う。 - if (lastMeaningful is '(' or ',') + // Parameter / type-parameter attribute candidates (`(`, `,`, `<`): could be + // `void M([Attr] T x)`, `class C<[Attr] T>`, or `Consume([Make()])`. Disambiguate by + // scanning forward to the matching `]` and checking whether the next meaningful + // token begins a declaration (identifier / `@` / `(` for tuple types / `[` chained). + // パラメータ / 型パラメータ属性候補 (`(`, `,`, `<`) は `void M([Attr] T x)` にも + // `class C<[Attr] T>` にも `Consume([Make()])` にもなりうる。対応する `]` まで進んで + // 次トークンが宣言を開始するか(識別子 / `@` / tuple の `(` / chained `[`)で区別する。 + if (lastMeaningful is '(' or ',' or '<') return IsCSharpAttributeFollowedByDeclaration(preparedLines, startLi, startCi); return false; } + /// + /// Keywords that indicate the preceding `[...]` is an expression (collection / pattern / + /// switch target) rather than an attribute section when they appear after `]`. + /// `]` の直後に現れると、直前の `[...]` が属性ではなく式(collection / pattern / switch 対象) + /// であることを示す C# のキーワード集合。 + /// + private static readonly HashSet CSharpExpressionContinuationKeywords = new(StringComparer.Ordinal) + { + "is", "as", "switch", "with", "when", + }; + /// /// Scan forward from a `[` to its matching `]` (skipping balanced parens) and return true /// when the next meaningful character begins an identifier-like token. Works across lines so @@ -626,21 +639,7 @@ private static bool IsCSharpAttributeFollowedByDeclaration(string[] preparedLine if (bracketDepth == 0) { ci++; - while (true) - { - while (ci < line.Length && char.IsWhiteSpace(line[ci])) - ci++; - if (ci < line.Length) - { - var next = line[ci]; - return IsIdentifierChar(next) || next == '@'; - } - li++; - if (li >= preparedLines.Length) - return false; - line = preparedLines[li]; - ci = 0; - } + return NextTokenStartsDeclaration(preparedLines, li, ci); } ci++; continue; @@ -653,6 +652,43 @@ private static bool IsCSharpAttributeFollowedByDeclaration(string[] preparedLine return false; } + /// + /// After the closing `]` of a candidate `[...]`, inspect the next meaningful token to decide + /// whether it begins a declaration. Accepts identifiers (except expression-continuation + /// keywords like `is` / `as` / `switch` / `with` / `when`), leading `@` (verbatim identifier), + /// `(` (tuple-typed parameter), and chained `[` (recurse for `[A][B]`). + /// 閉じ `]` の直後のトークンで宣言が始まるかを判定する。識別子(式継続の `is` / `as` / + /// `switch` / `with` / `when` は除外)、`@`(verbatim 識別子)、`(`(tuple パラメータ型)、 + /// `[`(`[A][B]` の連結)を受け入れる。 + /// + private static bool NextTokenStartsDeclaration(string[] preparedLines, int li, int ci) + { + while (li < preparedLines.Length) + { + var line = preparedLines[li]; + while (ci < line.Length && char.IsWhiteSpace(line[ci])) + ci++; + if (ci < line.Length) + { + var first = line[ci]; + if (first == '@' || first == '(') + return true; + if (first == '[') + return IsCSharpAttributeFollowedByDeclaration(preparedLines, li, ci); + if (!IsIdentifierChar(first)) + return false; + var start = ci; + while (ci < line.Length && IsIdentifierChar(line[ci])) + ci++; + var token = line.Substring(start, ci - start); + return !CSharpExpressionContinuationKeywords.Contains(token); + } + li++; + ci = 0; + } + return false; + } + private static bool IsInsideCSharpAttributeRange(List<(int start, int end)> ranges, int index) { foreach (var (start, end) in ranges) diff --git a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs index f2f0816c28..13f413fcb8 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs @@ -1536,6 +1536,118 @@ private void Consume(int[] xs) { } Assert.All(makeRefs, r => Assert.Equal("call", r.ReferenceKind)); } + [Fact] + public void Extract_CsharpCollectionExpressionPatternMatch_StaysCall() + { + // Regression: `[Make()] is int[] xs` is a pattern expression, not an attribute. The + // next token after `]` is the contextual keyword `is`, so `Make` must stay `call`. + // リグレッション: `[Make()] is int[] xs` はパターン式なので、`]` の次の `is` を属性の続きと誤認せず `Make` は `call`。 + const string content = """ + public class C + { + public bool M() + { + return Consume([Make()] is int[] xs && xs.Length > 0); + } + private static int Make() => 0; + private bool Consume(bool b) => b; + } + """; + + var symbols = SymbolExtractor.Extract(1, "csharp", content); + var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); + + var make = Assert.Single(references.Where(r => r.SymbolName == "Make")); + Assert.Equal("call", make.ReferenceKind); + } + + [Fact] + public void Extract_CsharpCollectionExpressionAsCast_StaysCall() + { + // Regression: `[Make()] as int[]` is an `as` cast, not an attribute. The classifier + // must treat `as` as expression continuation and keep `Make` as `call`. + // リグレッション: `[Make()] as int[]` は `as` キャストなので `Make` は `call` のまま。 + const string content = """ + public class C + { + public void M() + { + var arr = ([Make()] as int[]); + } + private static int Make() => 0; + } + """; + + var symbols = SymbolExtractor.Extract(1, "csharp", content); + var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); + + var make = Assert.Single(references.Where(r => r.SymbolName == "Make")); + Assert.Equal("call", make.ReferenceKind); + } + + [Fact] + public void Extract_CsharpCollectionExpressionSwitchExpression_StaysCall() + { + // Regression: `[Make()] switch { ... }` is a switch expression over a collection, + // not an attribute. The classifier must treat `switch` as expression continuation. + // リグレッション: `[Make()] switch { ... }` は collection に対する switch 式のため `Make` は `call`。 + const string content = """ + public class C + { + public bool M() => Consume([Make()] switch { _ => true }); + private static int Make() => 0; + private bool Consume(bool b) => b; + } + """; + + var symbols = SymbolExtractor.Extract(1, "csharp", content); + var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); + + var make = Assert.Single(references.Where(r => r.SymbolName == "Make")); + Assert.Equal("call", make.ReferenceKind); + } + + [Fact] + public void Extract_CsharpTupleTypedParameterAttribute_ClassifiedAsAttribute() + { + // Regression: `void M([Attr("x")] (int, int) value)` — the token after `]` is `(` + // (tuple type syntax), which must still be treated as a declaration start so the + // preceding `[...]` is classified as an attribute. + // リグレッション: `void M([Attr("x")] (int, int) value)` のように `]` の直後が tuple 型の `(` でも属性扱い。 + const string content = """ + public class C + { + public void M([Attr("x")] (int a, int b) value) { } + } + """; + + var symbols = SymbolExtractor.Extract(1, "csharp", content); + var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); + + var attr = Assert.Single(references.Where(r => r.SymbolName == "Attr")); + Assert.Equal("attribute", attr.ReferenceKind); + } + + [Fact] + public void Extract_CsharpTypeParameterAttribute_ClassifiedAsAttribute() + { + // Regression: `class C<[Attr("x")] T>` — the `[` is preceded by `<`, which is a valid + // attribute position for type parameters. The classifier must accept `<` alongside + // `(` and `,` as parameter-list entry points. + // リグレッション: `class C<[Attr("x")] T>` のように `<` の直後にある型パラメータ属性も検出できること。 + const string content = """ + public class C<[Attr("x")] T> + { + } + """; + + var symbols = SymbolExtractor.Extract(1, "csharp", content); + var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); + + var attr = Assert.Single(references.Where(r => r.SymbolName == "Attr")); + Assert.Equal("attribute", attr.ReferenceKind); + } + [Fact] public void Extract_KotlinQualifiedFieldTargetAnnotation_ClassifiedAsAnnotation() { From 37464981bb58ce3c969c4adebbd94099ff26b6db Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 17 Apr 2026 21:29:04 +0900 Subject: [PATCH 06/30] Fix #293 handle lambda attributes, no-arg attrs/annotations, MCP docs - EvaluateCSharpAttributePosition now accepts `=` so `var f = [Attr("x")] () => 0;` is classified as `attribute` instead of the `(` token after `]` being misread as non-attribute context. - Two dedicated regexes emit no-arg forms that `CallRegex` could not reach: CSharpNoArgAttributeRegex for `[Serializable]`, `[Obsolete]`, `[System.Obsolete]`, `[assembly: CLSCompliant]`, `[Required, Key]` (gated on the attribute-range pre-pass so `arr[i]` stays unclassified), and NoArgAnnotationRegex for `@Deprecated`, `@Override`, `@org.junit.Test`, `@field:Deprecated`, with a `(?` (preceded by `<`), tuple-typed parameter attributes such as `void M([Attr("x")] (int a, int b) value)` (where the next token after `]` is `(`), and multi-line forms such as `[\n Foo("x")\n]` or `public void M(\n [Attr("x")]\n int a)` are recognized alongside the existing same-line cases. The pre-pass disambiguates the `(` / `,` / `<` entry point against C# 12 collection expressions in argument position (`Consume([Make()])`), pattern expressions (`[Make()] is int[] xs`), `as` casts (`[Make()] as int[]`), and switch expressions (`[Make()] switch { _ => true }`) by scanning forward from `[` to the matching `]` and checking whether the next meaningful token begins a declaration — identifiers are accepted unless they are the expression-continuation keywords `is` / `as` / `switch` / `with` / `when`, `@` is accepted as a verbatim-identifier lead, `(` is accepted for tuple-typed parameters, and chained `[A][B]` recurses through the inner bracket. Targeted forms (`[return: NotNullWhen(true)]`) are now recognized directly from their `[...]` section instead of via the previous `target:` heuristic. Regression coverage pins `[Attr("x")] int a, [Other("y")] int b`, type-parameter `class C<[Attr("x")] T>`, tuple-typed parameter `void M([Attr("x")] (int a, int b) value)`, `[\n Foo("x")\n]`, the cross-line parameter attribute shape, the targeted `[return: ...]` shape, the defense-in-depth collection-expression-in-argument case, and the three expression-continuation cases `[Make()] is ...`, `[Make()] as int[]`, and `[Make()] switch { ... }`. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. +- **C# parameter, type-parameter, lambda, and multi-line `[...]` attribute lists are now classified as `attribute`, no-arg attributes and annotations are indexed, and MCP tool descriptions document the default metadata filter (#293 follow-up)** — `ReferenceExtractor` now runs a pre-pass that tokenizes every C# `[...]` section across the whole file, so parameter attributes such as `void M([FromServices] IService s)` (where `[` is preceded by `(` or `,` rather than a declaration boundary), type-parameter attributes such as `class C<[Attr("x")] T>` (preceded by `<`), tuple-typed parameter attributes such as `void M([Attr("x")] (int a, int b) value)` (where the next token after `]` is `(`), lambda attributes such as `var f = [Attr("x")] () => 0;` (where `[` is preceded by `=`), and multi-line forms such as `[\n Foo("x")\n]` or `public void M(\n [Attr("x")]\n int a)` are recognized alongside the existing same-line cases. The pre-pass disambiguates the `(` / `,` / `<` / `=` entry point against C# 12 collection expressions in argument position (`Consume([Make()])`), pattern expressions (`[Make()] is int[] xs`), `as` casts (`[Make()] as int[]`), and switch expressions (`[Make()] switch { _ => true }`) by scanning forward from `[` to the matching `]` and checking whether the next meaningful token begins a declaration — identifiers are accepted unless they are the expression-continuation keywords `is` / `as` / `switch` / `with` / `when`, `@` is accepted as a verbatim-identifier lead, `(` is accepted for tuple-typed parameters and lambda parameter lists, and chained `[A][B]` recurses through the inner bracket. Bare no-arg C# attributes such as `[Serializable]`, `[Obsolete]`, `[System.Obsolete]`, `[assembly: CLSCompliant]`, and `[Required, Key]`, plus bare no-arg Java-family annotations such as `@Deprecated`, `@Override`, `@org.junit.Test`, and `@field:Deprecated`, are now emitted through dedicated no-arg regexes since the original `CallRegex` requires `(` — the C# path still gates on the attribute-range pre-pass so indexer access such as `arr[i]` stays unclassified, and the annotation path uses a lookbehind `(?`, tuple-typed parameter `void M([Attr("x")] (int a, int b) value)`, lambda `var f = [Attr("x")] () => 0;`, the no-arg C# attribute set above, the no-arg Java `@Deprecated` / `@Override` / `@org.junit.Test` set, Kotlin `@field:Deprecated` plus the `return@foo` negative, indexer access staying unclassified, `[\n Foo("x")\n]`, the cross-line parameter attribute shape, the targeted `[return: ...]` shape, the defense-in-depth collection-expression-in-argument case, and the three expression-continuation cases `[Make()] is ...`, `[Make()] as int[]`, and `[Make()] switch { ... }`. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. - **C# attribute and Java/Kotlin/Scala/TypeScript annotation usages no longer pollute the call-graph as phantom `call` rows (#293)** — `ReferenceExtractor` now reclassifies identifiers that appear inside C# `[...]` attribute lists (including direct `[Attr(args)]`, targeted `[return: Attr]` / `[assembly: Attr]` forms, and comma-separated `[Foo("a"), Bar("b")]` lists) as `attribute` references, and identifiers preceded by a Java-family `@` marker (optionally through a dotted qualifier chain such as `@org.junit.Test(args)`, or through a Kotlin use-site target such as `@field:Deprecated("msg")` / `@get:JsonName("x")` / `@file:JvmName("Foo")`) as `annotation` references. Attribute `[` detection is gated by a declaration-position guard so C# 12 collection expressions (`var xs = [Make(), Make()]`, `Consume([Make(), Make()])`, `return [Make(), Make()]`) and indexer accesses (`arr[Compute()]`, chained `arr[Compute()][Compute()]`, `matrix[Row()][Col()]`) keep their inner calls as `call` instead of being misclassified as metadata — when the `[` is preceded by another `]`, the walk-back now finds the matching `[` (skipping balanced parens) and re-checks that opening bracket's declaration position so `[A("x")][B("y")]` chained attribute lists still classify as `attribute` while `arr[i][Compute()]` stays `call`. Kotlin use-site target detection also handles the qualifier-chain combination `@field:com.example.Deprecated("msg")` / `@get:com.fasterxml.jackson.annotation.JsonProperty("x")` by unwinding the dotted qualifier first and then matching either `@` or a Kotlin `target:` + `@`. The shared graph filter `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` is applied to default-kind `callers`, `callees`, count queries, hotspot CTEs, and symbol-hotspot grouping, so `callers Obsolete`, `callers Deprecated`, `impact Conditional`, and `hotspots` no longer surface phantom caller edges or inflate hotspot counts for metadata-only references. `references` defaults keep every indexed reference kind visible so metadata usages remain inspectable; `references --kind attribute` / `--kind annotation` (and the MCP tool equivalents) explicitly surface metadata rows, and the CLI help / `--kind` validator / README now list the new reference kinds and document the new default contract for `callers` / `callees` / `hotspots` / `impact`. Regression coverage pins the new classification for `[Obsolete("msg")]`, `[return: NotNull("x")]`, `[Foo("a"), Bar("b")]`, `[AttributeUsage(...)]`, Java `@Deprecated(since="1.0")`, qualified `@org.junit.Test(...)`, Kotlin `@Deprecated("msg")`, Kotlin `@field:Deprecated` / `@get:JsonName` / `@file:JvmName`, ordinary method bodies still emitting `call`, and C# collection-expression / indexer call sites staying `call`. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Database/DbReader.cs`, `src/CodeIndex/Database/DbSymbolReader.cs`, `src/CodeIndex/Cli/QueryCommandRunner.cs`, `src/CodeIndex/Cli/ConsoleUi.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`, `tests/CodeIndex.Tests/ConsoleUiTests.cs`, `README.md`, `DEVELOPER_GUIDE.md`. Closes #293. - **C# expression-bodied and block-bodied property callers attribute to the individual method/property instead of the enclosing class (#233)** — `SymbolExtractor` now detects `=> expr;` at top level inside `FindCSharpBraceRange` and assigns a body range covering the declaration line through the terminating `;` (including the multi-line form where `=>` is placed on a following indented line, both for methods and for expression-bodied properties). The block-bodied property scanner now also merges the common Microsoft-style `public int Wrap {` + next-line `get { ... }` shape with its accessor line so that form is recognized alongside the existing K&R (`public int Wrap { get { ... } }`) and Allman (`public int Wrap` then `{` on the next line) shapes, without misclassifying `public class X {` or other non-property headers. Block comments spanning several lines between the header and the body are handled correctly by the same line-scanning path. `ReferenceExtractor.FindInnermostContainer` now accepts `property` as a container kind alongside `function` / `class` / `namespace`. As a result `callers `, `impact_analysis`, and the caller rows surfaced by `inspect` / `analyze_symbol` no longer collapse `public int Wrap() => Compute();`, `public int Wrap => Compute();`, the block-bodied `public int Wrap { get { return Compute(); } }` (K&R), `public int Wrap {` / ` get { return Compute(); }` / `}` (same-line brace with next-line accessor), the Allman-style `public int Wrap` / `{` / ` get { return Compute(); }` / `}`, the multi-line expression-bodied `public int Wrap` / ` => Compute();`, or the same shapes with multi-line block comments between header and body into a single enclosing-class row — each caller now appears on its own row with the correct `caller_kind` (`function` or `property`) and `caller_name`. Regression coverage spans symbol body ranges, all three block-bodied property shapes plus a misclassification guard for bare-brace lines whose body is not an accessor, accessor-attribute and visibility-prefixed accessor lines, reference attribution for expression-bodied methods / properties / multi-line `=> expr;` / K&R block-bodied accessor / bare-brace-same-line accessor-next-line / Allman-style / multi-line expression-bodied property forms, the block-comment-between-header-and-body variants for both Allman and multi-line expression-bodied properties, the previously-encoded ternary-continuation caller test, and end-to-end `callers` assertions through the CLI for the K&R shape (covered by existing property tests), the bare-brace-same-line shape, the Allman block-body shape, the multi-line expression-bodied shape, and both block-comment-separated variants. Affected: `src/CodeIndex/Indexer/SymbolExtractor.cs`, `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/SymbolExtractorTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`, `tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`. Closes #233. - **C# 13 partial properties now participate in symbol extraction (#228)** — The C# extractor now accepts the `partial` modifier on properties and looks ahead across multiline headers, so brace-on-next-line declarations, block-bodied implementations, multiline expression-bodied properties, and `partial override` forms no longer disappear from `symbols`, `definition`, `outline`, `hotspots`, and related symbol-driven commands. Added regression coverage for multiline declaration-style, block-bodied implementation, and multiline `partial override` properties. Affected: `src/CodeIndex/Indexer/SymbolExtractor.cs`, `tests/CodeIndex.Tests/SymbolExtractorTests.cs`. Closes #228. @@ -681,7 +681,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), #### 修正 - **`impact` BFS が C# の event subscribe edge を辿るよう修正 (#293 追加対応)** — `impact` / `impact_analysis` BFS の唯一の呼び出し箇所である `DbReader.GetCallersExact` が、共有 filter `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` で参照を絞り込むようになった(以前は `InvokeReferenceKindsSql = ('call', 'instantiate')` を使っていたため、`Changed += OnChanged` のような subscribe 行を静かに捨てていた)。これにより `callers` / `callees` / `hotspots` と同じ call-graph 契約を `impact` も共有し、イベント駆動の transitive caller chain が `impact` でだけ短くなる不整合が解消する。`DbReaderTests.GetTransitiveCallers_FollowsSubscribeEdges` の回帰テストで subscribe edge が BFS 出力に含まれることを固定。Metadata edge (`attribute` / `annotation`) の除外は従来通り。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 -- **C# のパラメータ属性・型パラメータ属性・複数行 `[...]` 属性が `call` ではなく `attribute` として分類され、collection expression のパターン / `as` / `switch` は `call` のまま残るよう修正 (#293 追加対応)** — `ReferenceExtractor` がファイル全体の C# `[...]` セクションを事前パスでトークン化するようになり、`void M([FromServices] IService s)` のように `[` の直前が宣言境界ではなく `(` / `,` で始まるパラメータ属性、`class C<[Attr("x")] T>` のように `<` で始まる型パラメータ属性、`void M([Attr("x")] (int a, int b) value)` のように `]` の直後が `(` のまま続く tuple 型パラメータ属性、さらに `[\n Foo("x")\n]` や `public void M(\n [Attr("x")]\n int a)` のような複数行形式も、既存の同一行ケースと同じように属性として扱われるようになった。`(` / `,` / `<` 直後の曖昧さは、`[` から対応する `]` まで前方走査し、`]` の直後の非空白トークンが宣言を開始するかで判定する — 識別子は原則受理するが、式継続キーワード `is` / `as` / `switch` / `with` / `when` は宣言扱いしないので `Consume([Make()] is int[] xs)` / `([Make()] as int[])` / `Consume([Make()] switch { _ => true })` のような collection expression は `call` のまま残り、`@` は verbatim 識別子の先頭として、`(` は tuple 型パラメータの先頭として受理し、`[A][B]` のような連続 bracket は内側 `[` に再帰して判定する。target 付き形 (`[return: NotNullWhen(true)]`) も、以前のような `target:` 個別ヒューリスティクスではなく `[...]` セクションそのものから判定する形に統一された。`[Attr("x")] int a, [Other("y")] int b`、`class C<[Attr("x")] T>` 型パラメータ属性、`void M([Attr("x")] (int a, int b) value)` tuple 型パラメータ属性、`[\n Foo("x")\n]`、改行を挟んだパラメータ属性、`[return: NotNullWhen(true)]`、防御的に `Consume([Make()])` の collection expression-in-argument、および `[Make()] is ...` / `[Make()] as int[]` / `[Make()] switch { ... }` の 3 つの式継続ケースを押さえる回帰テストを追加。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 +- **C# のパラメータ属性・型パラメータ属性・ラムダ属性・複数行 `[...]` 属性を `attribute` として分類し、引数なし属性とアノテーションも索引化、MCP ツール説明に既定の metadata フィルタを反映 (#293 追加対応)** — `ReferenceExtractor` がファイル全体の C# `[...]` セクションを事前パスでトークン化するようになり、`void M([FromServices] IService s)` のように `[` の直前が宣言境界ではなく `(` / `,` で始まるパラメータ属性、`class C<[Attr("x")] T>` のように `<` で始まる型パラメータ属性、`void M([Attr("x")] (int a, int b) value)` のように `]` の直後が `(` のまま続く tuple 型パラメータ属性、`var f = [Attr("x")] () => 0;` のように `=` の直後にあるラムダ属性、さらに `[\n Foo("x")\n]` や `public void M(\n [Attr("x")]\n int a)` のような複数行形式も、既存の同一行ケースと同じように属性として扱われるようになった。`(` / `,` / `<` / `=` 直後の曖昧さは、`[` から対応する `]` まで前方走査し、`]` の直後の非空白トークンが宣言を開始するかで判定する — 識別子は原則受理するが、式継続キーワード `is` / `as` / `switch` / `with` / `when` は宣言扱いしないので `Consume([Make()] is int[] xs)` / `([Make()] as int[])` / `Consume([Make()] switch { _ => true })` のような collection expression は `call` のまま残り、`@` は verbatim 識別子の先頭として、`(` は tuple 型パラメータとラムダ仮引数リストの先頭として受理し、`[A][B]` のような連続 bracket は内側 `[` に再帰して判定する。さらに `[Serializable]`、`[Obsolete]`、`[System.Obsolete]`、`[assembly: CLSCompliant]`、`[Required, Key]` のような引数なし C# 属性と、`@Deprecated`・`@Override`・`@org.junit.Test`・`@field:Deprecated` のような引数なし Java 系アノテーションは、`(` を要求する既存の `CallRegex` では拾えないため専用の no-arg 正規表現から emit するようになった。C# 側では引き続き `[...]` 事前パスのレンジに含まれるかで gate するので `arr[i]` のような indexer アクセスは属性化されず、アノテーション側は lookbehind `(?` 型パラメータ属性、`void M([Attr("x")] (int a, int b) value)` tuple 型パラメータ属性、`var f = [Attr("x")] () => 0;` ラムダ属性、上記 no-arg C# 属性群、Java `@Deprecated` / `@Override` / `@org.junit.Test` の no-arg アノテーション群、Kotlin `@field:Deprecated` と `return@foo` の negative ケース、indexer アクセスが属性化されないこと、`[\n Foo("x")\n]`、改行を挟んだパラメータ属性、`[return: NotNullWhen(true)]`、防御的に `Consume([Make()])` の collection expression-in-argument、および `[Make()] is ...` / `[Make()] as int[]` / `[Make()] switch { ... }` の 3 つの式継続ケースを押さえる回帰テストを追加。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Mcp/McpToolDefinitions.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 - **C# 属性と Java/Kotlin/Scala/TypeScript の annotation 使用が phantom な `call` 行として call-graph を汚染しないよう修正 (#293)** — `ReferenceExtractor` が C# の `[...]` 属性リスト内(直接形 `[Attr(args)]`、target 付き `[return: Attr]` / `[assembly: Attr]`、カンマ区切り `[Foo("a"), Bar("b")]` を含む)に現れる識別子を `attribute` 参照、Java 系 `@` マーカーが先行する識別子(`@org.junit.Test(args)` のようなドット修飾チェーン越し、および Kotlin の use-site target `@field:Deprecated("msg")` / `@get:JsonName("x")` / `@file:JvmName("Foo")` 越しを含む)を `annotation` 参照として分類するようになった。属性 `[` の判定には宣言位置ガードを入れ、C# 12 の collection expression(`var xs = [Make(), Make()]`、`Consume([Make(), Make()])`、`return [Make(), Make()]`)や indexer アクセス(`arr[Compute()]`、連続 `arr[Compute()][Compute()]`、`matrix[Row()][Col()]`)の内側の呼び出しは `call` のまま残り、metadata に誤分類されないようにした。`[` の直前が別の `]` だった場合は、対応する `[` まで左方向に(balanced paren を跨いで)巻き戻してその開き bracket 自体が宣言位置かを再判定するため、`[A("x")][B("y")]` のような連続 attribute list は引き続き `attribute` として認識される一方、`arr[i][Compute()]` のような連続 indexer は `call` のまま残る。Kotlin の use-site target 判定も、ドット修飾子チェーンを先に剥がしたうえで `@` または Kotlin の `target:` + `@` を判定する構成に変え、`@field:com.example.Deprecated("msg")` / `@get:com.fasterxml.jackson.annotation.JsonProperty("x")` のような修飾付き注釈名と use-site target の組み合わせにも対応するようにした。共有 filter `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` を default-kind の `callers`・`callees`・count クエリ・hotspot CTE・symbol-hotspot グルーピングに適用し、`callers Obsolete`・`callers Deprecated`・`impact Conditional`・`hotspots` で phantom caller edge や metadata のみの参照による件数膨張が発生しないようにした。`references` の既定は全 reference kind を表示して metadata 使用も確認できる状態を保ちつつ、`references --kind attribute` / `--kind annotation`(と MCP ツール)で metadata 行を明示取得できる。CLI help・`--kind` バリデータ・README も新しい reference kind と `callers` / `callees` / `hotspots` / `impact` の新しい既定契約を反映する。`[Obsolete("msg")]`・`[return: NotNull("x")]`・`[Foo("a"), Bar("b")]`・`[AttributeUsage(...)]`、Java `@Deprecated(since="1.0")`・修飾付き `@org.junit.Test(...)`、Kotlin `@Deprecated("msg")`、Kotlin `@field:Deprecated` / `@get:JsonName` / `@file:JvmName` の分類、通常のメソッド本体が引き続き `call` を返すこと、C# collection expression や indexer アクセス内の呼び出しが `call` のままであることを押さえる回帰テストを追加。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Database/DbReader.cs`、`src/CodeIndex/Database/DbSymbolReader.cs`、`src/CodeIndex/Cli/QueryCommandRunner.cs`、`src/CodeIndex/Cli/ConsoleUi.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`、`tests/CodeIndex.Tests/ConsoleUiTests.cs`、`README.md`、`DEVELOPER_GUIDE.md`。Closes #293。 - **C# 式本体メンバーと block-bodied プロパティの caller が外側クラスではなく個別メソッド/プロパティに帰属するよう修正 (#233)** — `SymbolExtractor` の `FindCSharpBraceRange` がトップレベルの `=> expr;` を検出し、宣言行から終端 `;` までを本体範囲として扱うようになった(宣言行の次行にインデントして `=>` を置く multi-line 形にも、メソッドと式本体プロパティの双方で対応)。block-bodied プロパティの行結合ロジックも、`public int Wrap {` の次行に ` get { ... }` を置く Microsoft 標準形を accessor 行と結合するよう拡張し、既存の K&R (`public int Wrap { get { ... } }`) や Allman (`public int Wrap` の次行に `{`) と並ぶ 3 形を抽出対象にしつつ、`public class X {` のような非プロパティ header を property と誤分類しないようにした。ヘッダと本体の間に複数行またがるブロックコメントが挟まる場合も同じ行スキャン経路で正しく処理される。`ReferenceExtractor.FindInnermostContainer` は `property` も `function` / `class` / `namespace` と並ぶ container kind として受け入れるようにした。これにより `callers ` / `impact_analysis` と `inspect` / `analyze_symbol` に現れる caller 行が、`public int Wrap() => Compute();`、`public int Wrap => Compute();`、K&R の `public int Wrap { get { return Compute(); } }`、`public int Wrap {` / ` get { return Compute(); }` / `}`(同一行 bare `{` + 次行 accessor)、Allman の `public int Wrap` / `{` / ` get { return Compute(); }` / `}`、次行に ` => Compute();` を置く multi-line 式本体プロパティ、さらにヘッダと本体の間に複数行のブロックコメントを挟む同種の形まで、外側クラスの 1 行にまとめてしまわず、各 caller が固有の `caller_kind`(`function` または `property`)と `caller_name` を持つ別行として現れる。シンボル本体範囲、3 形すべての block-bodied property 抽出と、本体が accessor でない bare `{` の誤分類防止ガード、accessor attribute / visibility 修飾子付き accessor 行、式本体メソッド / プロパティ / multi-line `=> expr;` / K&R block-bodied accessor / 同一行 bare `{` + 次行 accessor / Allman / multi-line 式本体プロパティの参照属性、Allman と multi-line 式本体プロパティのブロックコメント挟み込み版、既存の ternary-continuation caller テスト、K&R 形(既存テストでカバー)、bare `{` + 次行 accessor 形、Allman block-body 形、multi-line 式本体プロパティ形、そしてブロックコメント挟み込みの両形の CLI `callers` end-to-end 検証まで押さえる回帰テストを追加。対象: `src/CodeIndex/Indexer/SymbolExtractor.cs`、`src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/SymbolExtractorTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`、`tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`。Closes #233。 - **C# 13 の partial property がシンボル抽出から欠落しないよう修正 (#228)** — C# extractor が property の `partial` 修飾子を受け付け、さらに複数行 header を先読みするようになった。これにより brace-on-next-line の宣言、block-bodied な実装、複数行の expression-bodied property、`partial override` 形が `symbols`、`definition`、`outline`、`hotspots` などのシンボル系コマンドから消えなくなった。複数行 declaration-style、block-bodied implementation、複数行 `partial override` property を押さえる回帰テストも追加。対象: `src/CodeIndex/Indexer/SymbolExtractor.cs`、`tests/CodeIndex.Tests/SymbolExtractorTests.cs`。Closes #228。 diff --git a/src/CodeIndex/Indexer/ReferenceExtractor.cs b/src/CodeIndex/Indexer/ReferenceExtractor.cs index 4a1c1c7303..e684ea6b35 100644 --- a/src/CodeIndex/Indexer/ReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/ReferenceExtractor.cs @@ -132,6 +132,30 @@ public static class ReferenceExtractor // C# イベント購読・解除: Click += OnClick — LHS と RHS の両方が PascalCase 識別子のみ private static readonly Regex EventSubscriptionRegex = new(@"(?[A-Z]\w*)\s*[+-]=\s*(?:new\s+)?[A-Z]\w*", RegexOptions.Compiled); + // No-arg C# attribute name (`[Serializable]`, `[assembly: CLSCompliant]`, `[System.Obsolete]`, + // `[Required, Key]`). CallRegex only matches identifiers followed by `(`, so no-arg attributes + // would otherwise never be indexed. The pattern anchors to `[` / `,` boundaries and refuses to + // match when the identifier is followed by `(` (handled by CallRegex + TryClassifyMetadataReference) + // or `.` (qualifier continues). A subsequent `IsInsideCSharpAttributeRange` filter keeps the + // match from firing on indexer access like `arr[i]`. + // 引数なしの C# attribute 名用 regex。`[Serializable]` などは CallRegex では拾えないため専用の + // 入口で捕捉する。`[` / `,` 境界にアンカーし、後続が `(`(CallRegex 経路で処理)や `.`(qualifier 継続) + // なら採用しない。`arr[i]` のような indexer を排除するため、マッチ後に + // `IsInsideCSharpAttributeRange` で範囲内かも確認する。 + private static readonly Regex CSharpNoArgAttributeRegex = new( + @"[\[,]\s*(?:[A-Za-z_]\w*\s*:\s*)?(?:[A-Za-z_]\w*\s*\.\s*)*(?[A-Za-z_]\w*)\s*(?=[\],])", + RegexOptions.Compiled); + + // No-arg Java-family annotation (`@Deprecated`, `@Override`, `@org.junit.Test`, `@field:Deprecated`). + // CallRegex only catches `@Name(` forms; this pattern fills the bare `@Name` gap. The leading + // lookbehind `(?[A-Za-z_]\w*)\b(?!\s*[.(])", + RegexOptions.Compiled); + // Languages whose `@Decorator(args)` / `@Annotation(args)` syntax should produce // `annotation` reference rows rather than `call` rows (issue #293). // `@Decorator(args)` / `@Annotation(args)` 構文を `call` ではなく `annotation` として @@ -259,6 +283,39 @@ public static List Extract(long fileId, string? lang, string co var metadataKind = TryClassifyMetadataReference(language, preparedLine, nameIndex, insideCSharpAttributeRange); AddReference(references, seen, fileId, match, metadataKind ?? "call", context, lineNumber, container); } + + // issue #293: bare no-arg attributes / annotations are invisible to CallRegex because + // it requires `(`. Emit them from dedicated regexes so `[Serializable]` / `@Deprecated` + // and their siblings still populate the reference table. + // issue #293: 引数なしの属性・アノテーションは `(` が必須な CallRegex では拾えないため、 + // 専用 regex から `[Serializable]` / `@Deprecated` などの素形を reference テーブルへ反映する。 + if (language == "csharp" && csharpAttrRangesOnLine != null && csharpAttrRangesOnLine.Count > 0) + { + foreach (Match match in CSharpNoArgAttributeRegex.Matches(preparedLine)) + { + var name = match.Groups["name"].Value; + var nameIndex = match.Groups["name"].Index; + if (!IsInsideCSharpAttributeRange(csharpAttrRangesOnLine, nameIndex)) + continue; + if (IsIgnoredCallName(language, name)) + continue; + if (definitionNames != null && definitionNames.Contains(name)) + continue; + AddReference(references, seen, fileId, match, "attribute", context, lineNumber, container); + } + } + else if (AnnotationLanguages.Contains(language)) + { + foreach (Match match in NoArgAnnotationRegex.Matches(preparedLine)) + { + var name = match.Groups["name"].Value; + if (IsIgnoredCallName(language, name)) + continue; + if (definitionNames != null && definitionNames.Contains(name)) + continue; + AddReference(references, seen, fileId, match, "annotation", context, lineNumber, container); + } + } } return references; @@ -566,14 +623,16 @@ private static bool EvaluateCSharpAttributePosition( if (lastMeaningful == ']') return lastClosedBracketWasAttribute; - // Parameter / type-parameter attribute candidates (`(`, `,`, `<`): could be - // `void M([Attr] T x)`, `class C<[Attr] T>`, or `Consume([Make()])`. Disambiguate by - // scanning forward to the matching `]` and checking whether the next meaningful - // token begins a declaration (identifier / `@` / `(` for tuple types / `[` chained). - // パラメータ / 型パラメータ属性候補 (`(`, `,`, `<`) は `void M([Attr] T x)` にも - // `class C<[Attr] T>` にも `Consume([Make()])` にもなりうる。対応する `]` まで進んで - // 次トークンが宣言を開始するか(識別子 / `@` / tuple の `(` / chained `[`)で区別する。 - if (lastMeaningful is '(' or ',' or '<') + // Parameter / type-parameter / lambda attribute candidates (`(`, `,`, `<`, `=`): + // `void M([Attr] T x)`, `class C<[Attr] T>`, `var f = [Attr] () => body`, or + // `Consume([Make()])`. Disambiguate by scanning forward to the matching `]` and + // checking whether the next meaningful token begins a declaration (identifier / + // `@` / `(` for tuple types or lambda parameter lists / `[` chained). + // パラメータ / 型パラメータ / ラムダ属性候補 (`(`, `,`, `<`, `=`) は + // `void M([Attr] T x)`・`class C<[Attr] T>`・`var f = [Attr] () => body`・ + // `Consume([Make()])` いずれにもなりうる。対応する `]` まで進んで次トークンが + // 宣言やラムダを開始するか(識別子 / `@` / tuple・ラムダ仮引数の `(` / chained `[`)で区別する。 + if (lastMeaningful is '(' or ',' or '<' or '=') return IsCSharpAttributeFollowedByDeclaration(preparedLines, startLi, startCi); return false; diff --git a/src/CodeIndex/Mcp/McpToolDefinitions.cs b/src/CodeIndex/Mcp/McpToolDefinitions.cs index ee06f3c1ce..ea3807ec95 100644 --- a/src/CodeIndex/Mcp/McpToolDefinitions.cs +++ b/src/CodeIndex/Mcp/McpToolDefinitions.cs @@ -67,14 +67,14 @@ private JsonNode HandleToolsList(JsonNode? id) ReadOnlyAnnotations()), CreateToolDefinition( "references", - "Search indexed symbol references such as call sites. When `kind` is omitted, identical constructor `call` + `instantiate` rows at one physical site are collapsed. / 呼び出し箇所などのインデックス済みシンボル参照を検索。`kind` 未指定時は、同じ物理位置にある constructor の `call` + `instantiate` 重複行を集約する。", + "Search indexed symbol references such as call sites. When `kind` is omitted, all indexed reference kinds including metadata uses (`attribute` / `annotation`) stay visible, and identical constructor `call` + `instantiate` rows at one physical site are collapsed. / 呼び出し箇所などのインデックス済みシンボル参照を検索。`kind` 未指定時は metadata (`attribute` / `annotation`) も含む全 reference kind を表示したうえで、同じ物理位置にある constructor の `call` + `instantiate` 重複行を集約する。", new JsonObject { ["type"] = "object", ["properties"] = new JsonObject { ["query"] = new JsonObject { ["type"] = "string", ["description"] = "Referenced symbol name pattern to search for" }, - ["kind"] = new JsonObject { ["type"] = "string", ["description"] = "Filter by reference kind (for example: call, instantiate, subscribe)" }, + ["kind"] = new JsonObject { ["type"] = "string", ["description"] = "Filter by reference kind (call, instantiate, subscribe, attribute, annotation)" }, ["lang"] = new JsonObject { ["type"] = "string", ["description"] = "Filter by language" }, ["limit"] = new JsonObject { ["type"] = "integer", ["description"] = "Max results (default: 20)", ["default"] = 20 }, ["maxLineWidth"] = new JsonObject { ["type"] = "integer", ["description"] = "Clamp very long single-line context payloads per result (default: 512)", ["default"] = LineWidthFormatter.DefaultMaxLineWidth, ["minimum"] = 1, ["maximum"] = LineWidthFormatter.MaxAllowedLineWidth }, @@ -89,14 +89,14 @@ private JsonNode HandleToolsList(JsonNode? id) ReadOnlyAnnotations()), CreateToolDefinition( "callers", - "Find caller symbols that reference a callee. When `kind` is omitted, all indexed reference kinds stay visible while identical constructor `call` + `instantiate` rows at one physical site collapse. / 指定シンボルを参照している呼び出し元シンボルを探す。`kind` 未指定時は全 reference kind を表示したまま、同じ物理位置にある constructor の `call` + `instantiate` 重複行を集約する。", + "Find caller symbols that reference a callee. When `kind` is omitted, only call-graph kinds (`call`, `instantiate`, `subscribe`) are returned so metadata uses (`attribute` / `annotation`) do not pollute caller edges; identical constructor `call` + `instantiate` rows at one physical site also collapse. Pass `kind: \"attribute\"` or `kind: \"annotation\"` to inspect metadata references explicitly. / 指定シンボルを参照している呼び出し元シンボルを探す。`kind` 未指定時は call-graph 種別 (`call` / `instantiate` / `subscribe`) のみを返し、metadata 使用 (`attribute` / `annotation`) が phantom caller edge として混入しないようにする。同じ物理位置にある constructor の `call` + `instantiate` 重複行も集約する。Metadata の参照を明示的に確認したい場合は `kind: \"attribute\"` / `kind: \"annotation\"` を指定する。", new JsonObject { ["type"] = "object", ["properties"] = new JsonObject { ["query"] = new JsonObject { ["type"] = "string", ["description"] = "Callee symbol name pattern to search for" }, - ["kind"] = new JsonObject { ["type"] = "string", ["description"] = "Filter by reference kind (for example: call, instantiate, subscribe)" }, + ["kind"] = new JsonObject { ["type"] = "string", ["description"] = "Filter by reference kind (call, instantiate, subscribe, attribute, annotation)" }, ["lang"] = new JsonObject { ["type"] = "string", ["description"] = "Filter by language" }, ["limit"] = new JsonObject { ["type"] = "integer", ["description"] = "Max results (default: 20)", ["default"] = 20 }, ["path"] = new JsonObject { ["oneOf"] = new JsonArray { new JsonObject { ["type"] = "string" }, new JsonObject { ["type"] = "array", ["items"] = new JsonObject { ["type"] = "string" } } }, ["description"] = "Prefer or restrict matches to paths containing this text. Accepts a single string or an array; multiple values are OR'd together." }, @@ -110,14 +110,14 @@ private JsonNode HandleToolsList(JsonNode? id) ReadOnlyAnnotations()), CreateToolDefinition( "callees", - "Find callees used by a caller/container symbol. When `kind` is omitted, all indexed reference kinds stay visible while identical constructor `call` + `instantiate` rows at one physical site collapse. / 呼び出し元シンボルが使っている呼び出し先を探す。`kind` 未指定時は全 reference kind を表示したまま、同じ物理位置にある constructor の `call` + `instantiate` 重複行を集約する。", + "Find callees used by a caller/container symbol. When `kind` is omitted, only call-graph kinds (`call`, `instantiate`, `subscribe`) are returned so metadata uses (`attribute` / `annotation`) do not pollute callee edges; identical constructor `call` + `instantiate` rows at one physical site also collapse. Pass `kind: \"attribute\"` or `kind: \"annotation\"` to inspect metadata references explicitly. / 呼び出し元シンボルが使っている呼び出し先を探す。`kind` 未指定時は call-graph 種別 (`call` / `instantiate` / `subscribe`) のみを返し、metadata 使用 (`attribute` / `annotation`) が phantom callee edge として混入しないようにする。同じ物理位置にある constructor の `call` + `instantiate` 重複行も集約する。Metadata を明示的に見たい場合は `kind: \"attribute\"` / `kind: \"annotation\"` を指定する。", new JsonObject { ["type"] = "object", ["properties"] = new JsonObject { ["query"] = new JsonObject { ["type"] = "string", ["description"] = "Caller/container symbol name pattern to search for" }, - ["kind"] = new JsonObject { ["type"] = "string", ["description"] = "Filter by reference kind (for example: call, instantiate, subscribe)" }, + ["kind"] = new JsonObject { ["type"] = "string", ["description"] = "Filter by reference kind (call, instantiate, subscribe, attribute, annotation)" }, ["lang"] = new JsonObject { ["type"] = "string", ["description"] = "Filter by language" }, ["limit"] = new JsonObject { ["type"] = "integer", ["description"] = "Max results (default: 20)", ["default"] = 20 }, ["path"] = new JsonObject { ["oneOf"] = new JsonArray { new JsonObject { ["type"] = "string" }, new JsonObject { ["type"] = "array", ["items"] = new JsonObject { ["type"] = "string" } } }, ["description"] = "Prefer or restrict matches to paths containing this text. Accepts a single string or an array; multiple values are OR'd together." }, diff --git a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs index 13f413fcb8..a1ff525f95 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs @@ -1648,6 +1648,149 @@ public class C<[Attr("x")] T> Assert.Equal("attribute", attr.ReferenceKind); } + [Fact] + public void Extract_CsharpLambdaAttribute_ClassifiedAsAttribute() + { + // Regression: `var f = [Attr("x")] () => 0;` — the `[` is preceded by `=`, and the token + // after `]` is `(` (lambda parameter list). The classifier must accept `=` as a valid + // attribute-entry context alongside `(`, `,`, `<`. + // リグレッション: `var f = [Attr("x")] () => 0;` のように `=` の直後にあるラムダ属性も検出できること。 + const string content = """ + public class C + { + public void M() + { + var f = [Attr("x")] () => 0; + } + } + """; + + var symbols = SymbolExtractor.Extract(1, "csharp", content); + var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); + + var attr = Assert.Single(references.Where(r => r.SymbolName == "Attr")); + Assert.Equal("attribute", attr.ReferenceKind); + } + + [Fact] + public void Extract_CsharpNoArgAttribute_ClassifiedAsAttribute() + { + // Regression (issue #293): `[Serializable]`, `[Obsolete]`, `[System.Obsolete]`, + // `[assembly: CLSCompliant]`, `[Required, Key]` — bare no-arg attributes were not + // indexed at all because CallRegex requires `(`. A dedicated no-arg entry path + // must emit them with kind `attribute`. + // リグレッション (issue #293): `[Serializable]` などの引数なし属性も `attribute` として + // インデックスされること。CallRegex は `(` を要求するため専用の取り込み経路が必要。 + const string content = """ + [assembly: CLSCompliant] + [Serializable] + [Obsolete] + [System.Obsolete] + [Required, Key] + public class C + { + } + """; + + var references = ReferenceExtractor.Extract(1, "csharp", content, []); + + Assert.Single(references.Where(r => r.SymbolName == "CLSCompliant" && r.ReferenceKind == "attribute")); + Assert.Single(references.Where(r => r.SymbolName == "Serializable" && r.ReferenceKind == "attribute")); + // `[System.Obsolete]` — the qualifier chain is part of the attribute, and the emitted + // reference should carry the final segment (`Obsolete`). There are two `Obsolete` rows + // (the plain `[Obsolete]` above and the qualified `[System.Obsolete]`), both attribute. + Assert.Equal(2, references.Count(r => r.SymbolName == "Obsolete" && r.ReferenceKind == "attribute")); + Assert.Single(references.Where(r => r.SymbolName == "Required" && r.ReferenceKind == "attribute")); + Assert.Single(references.Where(r => r.SymbolName == "Key" && r.ReferenceKind == "attribute")); + Assert.DoesNotContain(references, r => r.ReferenceKind == "call"); + } + + [Fact] + public void Extract_CsharpIndexerAccess_NotClassifiedAsAttribute() + { + // Regression (issue #293): `arr[i]` looks like a bare `[name]` token, but it is an + // indexer expression, not an attribute. The no-arg attribute path must defer to the + // attribute-range pre-pass so indexer access is not misclassified as `attribute`. + // リグレッション (issue #293): `arr[i]` のような indexer アクセスは `[name]` 形だが + // 属性ではない。属性レンジ pre-pass を経由することで attribute への誤分類を防ぐ。 + const string content = """ + public class C + { + public int M(int[] arr, int i) => arr[i]; + } + """; + + var symbols = SymbolExtractor.Extract(1, "csharp", content); + var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); + + Assert.DoesNotContain(references, r => r.SymbolName == "i" && r.ReferenceKind == "attribute"); + } + + [Fact] + public void Extract_JavaNoArgAnnotation_ClassifiedAsAnnotation() + { + // Regression (issue #293): `@Deprecated`, `@Override`, `@org.junit.Test` — bare no-arg + // annotations were not indexed because CallRegex requires `(`. A dedicated no-arg + // regex must emit them with kind `annotation`. + // リグレッション (issue #293): `@Deprecated` などの引数なし Java annotation も + // `annotation` として認識されること。 + const string content = """ + public class C { + @Deprecated + @Override + @org.junit.Test + public void m() {} + } + """; + + var symbols = SymbolExtractor.Extract(1, "java", content); + var references = ReferenceExtractor.Extract(1, "java", content, symbols); + + Assert.Single(references.Where(r => r.SymbolName == "Deprecated" && r.ReferenceKind == "annotation")); + Assert.Single(references.Where(r => r.SymbolName == "Override" && r.ReferenceKind == "annotation")); + Assert.Single(references.Where(r => r.SymbolName == "Test" && r.ReferenceKind == "annotation")); + } + + [Fact] + public void Extract_KotlinNoArgTargetAnnotation_ClassifiedAsAnnotation() + { + // Regression (issue #293): `@field:Deprecated` — use-site target without parentheses. + // リグレッション (issue #293): `@field:Deprecated` のような use-site target 付き + // 引数なしアノテーションも `annotation` 判定になること。 + const string content = """ + class C { + @field:Deprecated + val x: Int = 0 + } + """; + + var references = ReferenceExtractor.Extract(1, "kotlin", content, []); + + var deprecated = Assert.Single(references.Where(r => r.SymbolName == "Deprecated")); + Assert.Equal("annotation", deprecated.ReferenceKind); + } + + [Fact] + public void Extract_KotlinReturnAtLabel_NotClassifiedAsAnnotation() + { + // Regression (issue #293): `return@foo` is a Kotlin label reference, not an annotation. + // The leading lookbehind `(? r.SymbolName == "foo" && r.ReferenceKind == "annotation"); + } + [Fact] public void Extract_KotlinQualifiedFieldTargetAnnotation_ClassifiedAsAnnotation() { From 931ac16ba3cbd0a23a610164c7452941837f79a7 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 18 Apr 2026 00:35:09 +0900 Subject: [PATCH 07/30] Fix #293 accept `::` qualifier in no-arg C# attrs, classify JS decorators - CSharpNoArgAttributeRegex now accepts `::` as qualifier separator so `[global::System.Obsolete]` and `[Alias::MyAttr]` reach references instead of being silently dropped from `references --kind attribute`. - AnnotationLanguages includes `javascript`, so `@sealed` / `@injectable()` are reclassified to `annotation` instead of leaking as phantom `call`. - Pull back earlier over-promise in MCP callers/callees descriptions and README callers row (EN+JP): metadata rows attribute to the enclosing body-range symbol (or drop for file-level targets), so the supported path for metadata is `references --kind attribute|annotation`. - Regression tests pin `[global::System.Obsolete]`, `[Alias::MyAttr]`, and JavaScript `@sealed` / `@injectable()`. --- CHANGELOG.md | 6 +- README.md | 4 +- src/CodeIndex/Indexer/ReferenceExtractor.cs | 20 +++--- src/CodeIndex/Mcp/McpToolDefinitions.cs | 4 +- .../ReferenceExtractorTests.cs | 69 +++++++++++++++++++ 5 files changed, 88 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d80ff26b62..85390daeda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,8 +13,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **Developer Guide now distinguishes SQLite's role from "zero dependencies" and labels release numbers as examples** — Clarified that cdidx remains a zero-configuration, single-file CLI with exactly one production dependency (`Microsoft.Data.Sqlite`), and made the release-workflow checklist explicitly state that `1.9.0` is only an example version to substitute during an actual release. Affected: `DEVELOPER_GUIDE.md`. #### Fixed +- **`::`-qualified C# no-arg attributes and JavaScript `@Decorator` usages are now classified correctly (#293 follow-up)** — The no-arg C# attribute regex now accepts `::` alongside `.` as a qualifier separator, so `[global::System.Obsolete]` and `[Alias::MyAttr]` reach `references --kind attribute` instead of being silently dropped. `AnnotationLanguages` additionally includes `javascript`, so JS decorators such as `@sealed` (no-arg) and `@injectable()` (with args) are reclassified to `annotation` rather than leaking into the call-graph as phantom `call` rows — matching the existing TypeScript contract since JavaScript is already a graph-supported language. Added regression tests pinning `[global::System.Obsolete]`, `[Alias::MyAttr]`, and JavaScript `@sealed` / `@injectable()`. Also rewrote the README `callers` row (EN + JP) and the MCP tool descriptions for `callers` / `callees` to pull back the earlier over-promise that `kind: "attribute"` / `kind: "annotation"` on `callers` / `callees` is a supported metadata-inspection path: a metadata row is attributed to its enclosing body-range symbol (for a class-level declaration, that is the class itself) and drops entirely for file-level targets such as `[assembly: ...]` where `container_name` is `null`, so the docs now direct users to `references --kind attribute|annotation` for metadata enumeration. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`, `README.md`. - **`impact` BFS now follows C# event subscribe edges (#293 follow-up)** — `DbReader.GetCallersExact`, the only caller site used by the `impact` / `impact_analysis` BFS, now filters references through the shared `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` set instead of `InvokeReferenceKindsSql = ('call', 'instantiate')`. Previously `impact` quietly dropped `Changed += OnChanged` subscribe rows while `callers` / `callees` / `hotspots` kept them, so an event-driven transitive caller chain appeared shorter under `impact` than under `callers`. A new `DbReaderTests.GetTransitiveCallers_FollowsSubscribeEdges` regression pins the subscribe edge into the BFS output. Metadata edges (`attribute`, `annotation`) remain excluded. Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. -- **C# parameter, type-parameter, lambda, and multi-line `[...]` attribute lists are now classified as `attribute`, no-arg attributes and annotations are indexed, and MCP tool descriptions document the default metadata filter (#293 follow-up)** — `ReferenceExtractor` now runs a pre-pass that tokenizes every C# `[...]` section across the whole file, so parameter attributes such as `void M([FromServices] IService s)` (where `[` is preceded by `(` or `,` rather than a declaration boundary), type-parameter attributes such as `class C<[Attr("x")] T>` (preceded by `<`), tuple-typed parameter attributes such as `void M([Attr("x")] (int a, int b) value)` (where the next token after `]` is `(`), lambda attributes such as `var f = [Attr("x")] () => 0;` (where `[` is preceded by `=`), and multi-line forms such as `[\n Foo("x")\n]` or `public void M(\n [Attr("x")]\n int a)` are recognized alongside the existing same-line cases. The pre-pass disambiguates the `(` / `,` / `<` / `=` entry point against C# 12 collection expressions in argument position (`Consume([Make()])`), pattern expressions (`[Make()] is int[] xs`), `as` casts (`[Make()] as int[]`), and switch expressions (`[Make()] switch { _ => true }`) by scanning forward from `[` to the matching `]` and checking whether the next meaningful token begins a declaration — identifiers are accepted unless they are the expression-continuation keywords `is` / `as` / `switch` / `with` / `when`, `@` is accepted as a verbatim-identifier lead, `(` is accepted for tuple-typed parameters and lambda parameter lists, and chained `[A][B]` recurses through the inner bracket. Bare no-arg C# attributes such as `[Serializable]`, `[Obsolete]`, `[System.Obsolete]`, `[assembly: CLSCompliant]`, and `[Required, Key]`, plus bare no-arg Java-family annotations such as `@Deprecated`, `@Override`, `@org.junit.Test`, and `@field:Deprecated`, are now emitted through dedicated no-arg regexes since the original `CallRegex` requires `(` — the C# path still gates on the attribute-range pre-pass so indexer access such as `arr[i]` stays unclassified, and the annotation path uses a lookbehind `(?`, tuple-typed parameter `void M([Attr("x")] (int a, int b) value)`, lambda `var f = [Attr("x")] () => 0;`, the no-arg C# attribute set above, the no-arg Java `@Deprecated` / `@Override` / `@org.junit.Test` set, Kotlin `@field:Deprecated` plus the `return@foo` negative, indexer access staying unclassified, `[\n Foo("x")\n]`, the cross-line parameter attribute shape, the targeted `[return: ...]` shape, the defense-in-depth collection-expression-in-argument case, and the three expression-continuation cases `[Make()] is ...`, `[Make()] as int[]`, and `[Make()] switch { ... }`. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. +- **C# parameter, type-parameter, lambda, and multi-line `[...]` attribute lists are now classified as `attribute`, no-arg attributes and annotations are indexed, and MCP tool descriptions document the default metadata filter (#293 follow-up)** — `ReferenceExtractor` now runs a pre-pass that tokenizes every C# `[...]` section across the whole file, so parameter attributes such as `void M([FromServices] IService s)` (where `[` is preceded by `(` or `,` rather than a declaration boundary), type-parameter attributes such as `class C<[Attr("x")] T>` (preceded by `<`), tuple-typed parameter attributes such as `void M([Attr("x")] (int a, int b) value)` (where the next token after `]` is `(`), lambda attributes such as `var f = [Attr("x")] () => 0;` (where `[` is preceded by `=`), and multi-line forms such as `[\n Foo("x")\n]` or `public void M(\n [Attr("x")]\n int a)` are recognized alongside the existing same-line cases. The pre-pass disambiguates the `(` / `,` / `<` / `=` entry point against C# 12 collection expressions in argument position (`Consume([Make()])`), pattern expressions (`[Make()] is int[] xs`), `as` casts (`[Make()] as int[]`), and switch expressions (`[Make()] switch { _ => true }`) by scanning forward from `[` to the matching `]` and checking whether the next meaningful token begins a declaration — identifiers are accepted unless they are the expression-continuation keywords `is` / `as` / `switch` / `with` / `when`, `@` is accepted as a verbatim-identifier lead, `(` is accepted for tuple-typed parameters and lambda parameter lists, and chained `[A][B]` recurses through the inner bracket. Bare no-arg C# attributes such as `[Serializable]`, `[Obsolete]`, `[System.Obsolete]`, `[assembly: CLSCompliant]`, and `[Required, Key]`, plus bare no-arg Java-family annotations such as `@Deprecated`, `@Override`, `@org.junit.Test`, and `@field:Deprecated`, are now emitted through dedicated no-arg regexes since the original `CallRegex` requires `(` — the C# path still gates on the attribute-range pre-pass so indexer access such as `arr[i]` stays unclassified, and the annotation path uses a lookbehind `(?`, tuple-typed parameter `void M([Attr("x")] (int a, int b) value)`, lambda `var f = [Attr("x")] () => 0;`, the no-arg C# attribute set above, the no-arg Java `@Deprecated` / `@Override` / `@org.junit.Test` set, Kotlin `@field:Deprecated` plus the `return@foo` negative, indexer access staying unclassified, `[\n Foo("x")\n]`, the cross-line parameter attribute shape, the targeted `[return: ...]` shape, the defense-in-depth collection-expression-in-argument case, and the three expression-continuation cases `[Make()] is ...`, `[Make()] as int[]`, and `[Make()] switch { ... }`. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. - **C# attribute and Java/Kotlin/Scala/TypeScript annotation usages no longer pollute the call-graph as phantom `call` rows (#293)** — `ReferenceExtractor` now reclassifies identifiers that appear inside C# `[...]` attribute lists (including direct `[Attr(args)]`, targeted `[return: Attr]` / `[assembly: Attr]` forms, and comma-separated `[Foo("a"), Bar("b")]` lists) as `attribute` references, and identifiers preceded by a Java-family `@` marker (optionally through a dotted qualifier chain such as `@org.junit.Test(args)`, or through a Kotlin use-site target such as `@field:Deprecated("msg")` / `@get:JsonName("x")` / `@file:JvmName("Foo")`) as `annotation` references. Attribute `[` detection is gated by a declaration-position guard so C# 12 collection expressions (`var xs = [Make(), Make()]`, `Consume([Make(), Make()])`, `return [Make(), Make()]`) and indexer accesses (`arr[Compute()]`, chained `arr[Compute()][Compute()]`, `matrix[Row()][Col()]`) keep their inner calls as `call` instead of being misclassified as metadata — when the `[` is preceded by another `]`, the walk-back now finds the matching `[` (skipping balanced parens) and re-checks that opening bracket's declaration position so `[A("x")][B("y")]` chained attribute lists still classify as `attribute` while `arr[i][Compute()]` stays `call`. Kotlin use-site target detection also handles the qualifier-chain combination `@field:com.example.Deprecated("msg")` / `@get:com.fasterxml.jackson.annotation.JsonProperty("x")` by unwinding the dotted qualifier first and then matching either `@` or a Kotlin `target:` + `@`. The shared graph filter `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` is applied to default-kind `callers`, `callees`, count queries, hotspot CTEs, and symbol-hotspot grouping, so `callers Obsolete`, `callers Deprecated`, `impact Conditional`, and `hotspots` no longer surface phantom caller edges or inflate hotspot counts for metadata-only references. `references` defaults keep every indexed reference kind visible so metadata usages remain inspectable; `references --kind attribute` / `--kind annotation` (and the MCP tool equivalents) explicitly surface metadata rows, and the CLI help / `--kind` validator / README now list the new reference kinds and document the new default contract for `callers` / `callees` / `hotspots` / `impact`. Regression coverage pins the new classification for `[Obsolete("msg")]`, `[return: NotNull("x")]`, `[Foo("a"), Bar("b")]`, `[AttributeUsage(...)]`, Java `@Deprecated(since="1.0")`, qualified `@org.junit.Test(...)`, Kotlin `@Deprecated("msg")`, Kotlin `@field:Deprecated` / `@get:JsonName` / `@file:JvmName`, ordinary method bodies still emitting `call`, and C# collection-expression / indexer call sites staying `call`. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Database/DbReader.cs`, `src/CodeIndex/Database/DbSymbolReader.cs`, `src/CodeIndex/Cli/QueryCommandRunner.cs`, `src/CodeIndex/Cli/ConsoleUi.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`, `tests/CodeIndex.Tests/ConsoleUiTests.cs`, `README.md`, `DEVELOPER_GUIDE.md`. Closes #293. - **C# expression-bodied and block-bodied property callers attribute to the individual method/property instead of the enclosing class (#233)** — `SymbolExtractor` now detects `=> expr;` at top level inside `FindCSharpBraceRange` and assigns a body range covering the declaration line through the terminating `;` (including the multi-line form where `=>` is placed on a following indented line, both for methods and for expression-bodied properties). The block-bodied property scanner now also merges the common Microsoft-style `public int Wrap {` + next-line `get { ... }` shape with its accessor line so that form is recognized alongside the existing K&R (`public int Wrap { get { ... } }`) and Allman (`public int Wrap` then `{` on the next line) shapes, without misclassifying `public class X {` or other non-property headers. Block comments spanning several lines between the header and the body are handled correctly by the same line-scanning path. `ReferenceExtractor.FindInnermostContainer` now accepts `property` as a container kind alongside `function` / `class` / `namespace`. As a result `callers `, `impact_analysis`, and the caller rows surfaced by `inspect` / `analyze_symbol` no longer collapse `public int Wrap() => Compute();`, `public int Wrap => Compute();`, the block-bodied `public int Wrap { get { return Compute(); } }` (K&R), `public int Wrap {` / ` get { return Compute(); }` / `}` (same-line brace with next-line accessor), the Allman-style `public int Wrap` / `{` / ` get { return Compute(); }` / `}`, the multi-line expression-bodied `public int Wrap` / ` => Compute();`, or the same shapes with multi-line block comments between header and body into a single enclosing-class row — each caller now appears on its own row with the correct `caller_kind` (`function` or `property`) and `caller_name`. Regression coverage spans symbol body ranges, all three block-bodied property shapes plus a misclassification guard for bare-brace lines whose body is not an accessor, accessor-attribute and visibility-prefixed accessor lines, reference attribution for expression-bodied methods / properties / multi-line `=> expr;` / K&R block-bodied accessor / bare-brace-same-line accessor-next-line / Allman-style / multi-line expression-bodied property forms, the block-comment-between-header-and-body variants for both Allman and multi-line expression-bodied properties, the previously-encoded ternary-continuation caller test, and end-to-end `callers` assertions through the CLI for the K&R shape (covered by existing property tests), the bare-brace-same-line shape, the Allman block-body shape, the multi-line expression-bodied shape, and both block-comment-separated variants. Affected: `src/CodeIndex/Indexer/SymbolExtractor.cs`, `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/SymbolExtractorTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`, `tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`. Closes #233. - **C# 13 partial properties now participate in symbol extraction (#228)** — The C# extractor now accepts the `partial` modifier on properties and looks ahead across multiline headers, so brace-on-next-line declarations, block-bodied implementations, multiline expression-bodied properties, and `partial override` forms no longer disappear from `symbols`, `definition`, `outline`, `hotspots`, and related symbol-driven commands. Added regression coverage for multiline declaration-style, block-bodied implementation, and multiline `partial override` properties. Affected: `src/CodeIndex/Indexer/SymbolExtractor.cs`, `tests/CodeIndex.Tests/SymbolExtractorTests.cs`. Closes #228. @@ -680,8 +681,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **DEVELOPER_GUIDE が SQLite の位置付けとリリース番号の例示を明確化** — cdidx が「設定ゼロ・単一ファイル・本番依存1個」の CLI であり、その唯一の本番依存が `Microsoft.Data.Sqlite` であることを明記した。あわせて、リリース手順のチェックリスト中に出てくる `1.9.0` は固定値ではなく例示であり、実際のリリース番号へ読み替える前提だと明示した。対象: `DEVELOPER_GUIDE.md`。 #### 修正 +- **`::` 修飾の C# 引数なし属性と JavaScript の `@Decorator` を正しく分類 (#293 追加対応)** — 引数なし C# 属性の正規表現が修飾子セパレータとして `.` に加えて `::` も受け付けるようになり、`[global::System.Obsolete]` や `[Alias::MyAttr]` のような形が `references --kind attribute` から黙って脱落しなくなった。`AnnotationLanguages` にも `javascript` を追加し、JavaScript はもともと graph 対応言語なので、`@sealed`(引数なし)や `@injectable()`(引数付き)のような JS decorator も `annotation` として再分類され、TypeScript と同じ契約で call-graph に phantom `call` 行を残さない。`[global::System.Obsolete]`・`[Alias::MyAttr]`・JavaScript の `@sealed` / `@injectable()` を押さえる回帰テストを追加。あわせて、以前に進めていた `callers` / `callees` の metadata 契約を README(英日)と MCP ツール説明の両方で引き戻し、metadata 行は注釈対象そのものではなく body-range 上の外側シンボル(クラス直下宣言ならクラス、`[assembly: ...]` のようなファイルレベル target では `container_name` が `null`)に帰属するため `kind: "attribute"` / `kind: "annotation"` を `callers` / `callees` に渡すのは信頼できる metadata 調査経路ではなく、metadata 列挙は `references --kind attribute|annotation` を使うよう案内する文言に修正した。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Mcp/McpToolDefinitions.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`、`README.md`。 - **`impact` BFS が C# の event subscribe edge を辿るよう修正 (#293 追加対応)** — `impact` / `impact_analysis` BFS の唯一の呼び出し箇所である `DbReader.GetCallersExact` が、共有 filter `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` で参照を絞り込むようになった(以前は `InvokeReferenceKindsSql = ('call', 'instantiate')` を使っていたため、`Changed += OnChanged` のような subscribe 行を静かに捨てていた)。これにより `callers` / `callees` / `hotspots` と同じ call-graph 契約を `impact` も共有し、イベント駆動の transitive caller chain が `impact` でだけ短くなる不整合が解消する。`DbReaderTests.GetTransitiveCallers_FollowsSubscribeEdges` の回帰テストで subscribe edge が BFS 出力に含まれることを固定。Metadata edge (`attribute` / `annotation`) の除外は従来通り。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 -- **C# のパラメータ属性・型パラメータ属性・ラムダ属性・複数行 `[...]` 属性を `attribute` として分類し、引数なし属性とアノテーションも索引化、MCP ツール説明に既定の metadata フィルタを反映 (#293 追加対応)** — `ReferenceExtractor` がファイル全体の C# `[...]` セクションを事前パスでトークン化するようになり、`void M([FromServices] IService s)` のように `[` の直前が宣言境界ではなく `(` / `,` で始まるパラメータ属性、`class C<[Attr("x")] T>` のように `<` で始まる型パラメータ属性、`void M([Attr("x")] (int a, int b) value)` のように `]` の直後が `(` のまま続く tuple 型パラメータ属性、`var f = [Attr("x")] () => 0;` のように `=` の直後にあるラムダ属性、さらに `[\n Foo("x")\n]` や `public void M(\n [Attr("x")]\n int a)` のような複数行形式も、既存の同一行ケースと同じように属性として扱われるようになった。`(` / `,` / `<` / `=` 直後の曖昧さは、`[` から対応する `]` まで前方走査し、`]` の直後の非空白トークンが宣言を開始するかで判定する — 識別子は原則受理するが、式継続キーワード `is` / `as` / `switch` / `with` / `when` は宣言扱いしないので `Consume([Make()] is int[] xs)` / `([Make()] as int[])` / `Consume([Make()] switch { _ => true })` のような collection expression は `call` のまま残り、`@` は verbatim 識別子の先頭として、`(` は tuple 型パラメータとラムダ仮引数リストの先頭として受理し、`[A][B]` のような連続 bracket は内側 `[` に再帰して判定する。さらに `[Serializable]`、`[Obsolete]`、`[System.Obsolete]`、`[assembly: CLSCompliant]`、`[Required, Key]` のような引数なし C# 属性と、`@Deprecated`・`@Override`・`@org.junit.Test`・`@field:Deprecated` のような引数なし Java 系アノテーションは、`(` を要求する既存の `CallRegex` では拾えないため専用の no-arg 正規表現から emit するようになった。C# 側では引き続き `[...]` 事前パスのレンジに含まれるかで gate するので `arr[i]` のような indexer アクセスは属性化されず、アノテーション側は lookbehind `(?` 型パラメータ属性、`void M([Attr("x")] (int a, int b) value)` tuple 型パラメータ属性、`var f = [Attr("x")] () => 0;` ラムダ属性、上記 no-arg C# 属性群、Java `@Deprecated` / `@Override` / `@org.junit.Test` の no-arg アノテーション群、Kotlin `@field:Deprecated` と `return@foo` の negative ケース、indexer アクセスが属性化されないこと、`[\n Foo("x")\n]`、改行を挟んだパラメータ属性、`[return: NotNullWhen(true)]`、防御的に `Consume([Make()])` の collection expression-in-argument、および `[Make()] is ...` / `[Make()] as int[]` / `[Make()] switch { ... }` の 3 つの式継続ケースを押さえる回帰テストを追加。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Mcp/McpToolDefinitions.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 +- **C# のパラメータ属性・型パラメータ属性・ラムダ属性・複数行 `[...]` 属性を `attribute` として分類し、引数なし属性とアノテーションも索引化、MCP ツール説明に既定の metadata フィルタを反映 (#293 追加対応)** — `ReferenceExtractor` がファイル全体の C# `[...]` セクションを事前パスでトークン化するようになり、`void M([FromServices] IService s)` のように `[` の直前が宣言境界ではなく `(` / `,` で始まるパラメータ属性、`class C<[Attr("x")] T>` のように `<` で始まる型パラメータ属性、`void M([Attr("x")] (int a, int b) value)` のように `]` の直後が `(` のまま続く tuple 型パラメータ属性、`var f = [Attr("x")] () => 0;` のように `=` の直後にあるラムダ属性、さらに `[\n Foo("x")\n]` や `public void M(\n [Attr("x")]\n int a)` のような複数行形式も、既存の同一行ケースと同じように属性として扱われるようになった。`(` / `,` / `<` / `=` 直後の曖昧さは、`[` から対応する `]` まで前方走査し、`]` の直後の非空白トークンが宣言を開始するかで判定する — 識別子は原則受理するが、式継続キーワード `is` / `as` / `switch` / `with` / `when` は宣言扱いしないので `Consume([Make()] is int[] xs)` / `([Make()] as int[])` / `Consume([Make()] switch { _ => true })` のような collection expression は `call` のまま残り、`@` は verbatim 識別子の先頭として、`(` は tuple 型パラメータとラムダ仮引数リストの先頭として受理し、`[A][B]` のような連続 bracket は内側 `[` に再帰して判定する。さらに `[Serializable]`、`[Obsolete]`、`[System.Obsolete]`、`[assembly: CLSCompliant]`、`[Required, Key]` のような引数なし C# 属性と、`@Deprecated`・`@Override`・`@org.junit.Test`・`@field:Deprecated` のような引数なし Java 系アノテーションは、`(` を要求する既存の `CallRegex` では拾えないため専用の no-arg 正規表現から emit するようになった。C# 側では引き続き `[...]` 事前パスのレンジに含まれるかで gate するので `arr[i]` のような indexer アクセスは属性化されず、アノテーション側は lookbehind `(?` 型パラメータ属性、`void M([Attr("x")] (int a, int b) value)` tuple 型パラメータ属性、`var f = [Attr("x")] () => 0;` ラムダ属性、上記 no-arg C# 属性群、Java `@Deprecated` / `@Override` / `@org.junit.Test` の no-arg アノテーション群、Kotlin `@field:Deprecated` と `return@foo` の negative ケース、indexer アクセスが属性化されないこと、`[\n Foo("x")\n]`、改行を挟んだパラメータ属性、`[return: NotNullWhen(true)]`、防御的に `Consume([Make()])` の collection expression-in-argument、および `[Make()] is ...` / `[Make()] as int[]` / `[Make()] switch { ... }` の 3 つの式継続ケースを押さえる回帰テストを追加。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Mcp/McpToolDefinitions.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 - **C# 属性と Java/Kotlin/Scala/TypeScript の annotation 使用が phantom な `call` 行として call-graph を汚染しないよう修正 (#293)** — `ReferenceExtractor` が C# の `[...]` 属性リスト内(直接形 `[Attr(args)]`、target 付き `[return: Attr]` / `[assembly: Attr]`、カンマ区切り `[Foo("a"), Bar("b")]` を含む)に現れる識別子を `attribute` 参照、Java 系 `@` マーカーが先行する識別子(`@org.junit.Test(args)` のようなドット修飾チェーン越し、および Kotlin の use-site target `@field:Deprecated("msg")` / `@get:JsonName("x")` / `@file:JvmName("Foo")` 越しを含む)を `annotation` 参照として分類するようになった。属性 `[` の判定には宣言位置ガードを入れ、C# 12 の collection expression(`var xs = [Make(), Make()]`、`Consume([Make(), Make()])`、`return [Make(), Make()]`)や indexer アクセス(`arr[Compute()]`、連続 `arr[Compute()][Compute()]`、`matrix[Row()][Col()]`)の内側の呼び出しは `call` のまま残り、metadata に誤分類されないようにした。`[` の直前が別の `]` だった場合は、対応する `[` まで左方向に(balanced paren を跨いで)巻き戻してその開き bracket 自体が宣言位置かを再判定するため、`[A("x")][B("y")]` のような連続 attribute list は引き続き `attribute` として認識される一方、`arr[i][Compute()]` のような連続 indexer は `call` のまま残る。Kotlin の use-site target 判定も、ドット修飾子チェーンを先に剥がしたうえで `@` または Kotlin の `target:` + `@` を判定する構成に変え、`@field:com.example.Deprecated("msg")` / `@get:com.fasterxml.jackson.annotation.JsonProperty("x")` のような修飾付き注釈名と use-site target の組み合わせにも対応するようにした。共有 filter `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` を default-kind の `callers`・`callees`・count クエリ・hotspot CTE・symbol-hotspot グルーピングに適用し、`callers Obsolete`・`callers Deprecated`・`impact Conditional`・`hotspots` で phantom caller edge や metadata のみの参照による件数膨張が発生しないようにした。`references` の既定は全 reference kind を表示して metadata 使用も確認できる状態を保ちつつ、`references --kind attribute` / `--kind annotation`(と MCP ツール)で metadata 行を明示取得できる。CLI help・`--kind` バリデータ・README も新しい reference kind と `callers` / `callees` / `hotspots` / `impact` の新しい既定契約を反映する。`[Obsolete("msg")]`・`[return: NotNull("x")]`・`[Foo("a"), Bar("b")]`・`[AttributeUsage(...)]`、Java `@Deprecated(since="1.0")`・修飾付き `@org.junit.Test(...)`、Kotlin `@Deprecated("msg")`、Kotlin `@field:Deprecated` / `@get:JsonName` / `@file:JvmName` の分類、通常のメソッド本体が引き続き `call` を返すこと、C# collection expression や indexer アクセス内の呼び出しが `call` のままであることを押さえる回帰テストを追加。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Database/DbReader.cs`、`src/CodeIndex/Database/DbSymbolReader.cs`、`src/CodeIndex/Cli/QueryCommandRunner.cs`、`src/CodeIndex/Cli/ConsoleUi.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`、`tests/CodeIndex.Tests/ConsoleUiTests.cs`、`README.md`、`DEVELOPER_GUIDE.md`。Closes #293。 - **C# 式本体メンバーと block-bodied プロパティの caller が外側クラスではなく個別メソッド/プロパティに帰属するよう修正 (#233)** — `SymbolExtractor` の `FindCSharpBraceRange` がトップレベルの `=> expr;` を検出し、宣言行から終端 `;` までを本体範囲として扱うようになった(宣言行の次行にインデントして `=>` を置く multi-line 形にも、メソッドと式本体プロパティの双方で対応)。block-bodied プロパティの行結合ロジックも、`public int Wrap {` の次行に ` get { ... }` を置く Microsoft 標準形を accessor 行と結合するよう拡張し、既存の K&R (`public int Wrap { get { ... } }`) や Allman (`public int Wrap` の次行に `{`) と並ぶ 3 形を抽出対象にしつつ、`public class X {` のような非プロパティ header を property と誤分類しないようにした。ヘッダと本体の間に複数行またがるブロックコメントが挟まる場合も同じ行スキャン経路で正しく処理される。`ReferenceExtractor.FindInnermostContainer` は `property` も `function` / `class` / `namespace` と並ぶ container kind として受け入れるようにした。これにより `callers ` / `impact_analysis` と `inspect` / `analyze_symbol` に現れる caller 行が、`public int Wrap() => Compute();`、`public int Wrap => Compute();`、K&R の `public int Wrap { get { return Compute(); } }`、`public int Wrap {` / ` get { return Compute(); }` / `}`(同一行 bare `{` + 次行 accessor)、Allman の `public int Wrap` / `{` / ` get { return Compute(); }` / `}`、次行に ` => Compute();` を置く multi-line 式本体プロパティ、さらにヘッダと本体の間に複数行のブロックコメントを挟む同種の形まで、外側クラスの 1 行にまとめてしまわず、各 caller が固有の `caller_kind`(`function` または `property`)と `caller_name` を持つ別行として現れる。シンボル本体範囲、3 形すべての block-bodied property 抽出と、本体が accessor でない bare `{` の誤分類防止ガード、accessor attribute / visibility 修飾子付き accessor 行、式本体メソッド / プロパティ / multi-line `=> expr;` / K&R block-bodied accessor / 同一行 bare `{` + 次行 accessor / Allman / multi-line 式本体プロパティの参照属性、Allman と multi-line 式本体プロパティのブロックコメント挟み込み版、既存の ternary-continuation caller テスト、K&R 形(既存テストでカバー)、bare `{` + 次行 accessor 形、Allman block-body 形、multi-line 式本体プロパティ形、そしてブロックコメント挟み込みの両形の CLI `callers` end-to-end 検証まで押さえる回帰テストを追加。対象: `src/CodeIndex/Indexer/SymbolExtractor.cs`、`src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/SymbolExtractorTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`、`tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`。Closes #233。 - **C# 13 の partial property がシンボル抽出から欠落しないよう修正 (#228)** — C# extractor が property の `partial` 修飾子を受け付け、さらに複数行 header を先読みするようになった。これにより brace-on-next-line の宣言、block-bodied な実装、複数行の expression-bodied property、`partial override` 形が `symbols`、`definition`、`outline`、`hotspots` などのシンボル系コマンドから消えなくなった。複数行 declaration-style、block-bodied implementation、複数行 `partial override` property を押さえる回帰テストも追加。対象: `src/CodeIndex/Indexer/SymbolExtractor.cs`、`tests/CodeIndex.Tests/SymbolExtractorTests.cs`。Closes #228。 diff --git a/README.md b/README.md index c678b42bf2..ebd5de9abe 100644 --- a/README.md +++ b/README.md @@ -810,7 +810,7 @@ Once configured, the AI can directly call these tools: | `search` | Full-text search across code chunks | | `definition` | Reconstruct a symbol declaration and optional body | | `references` | Find indexed references for supported languages; identical constructor `call` + `instantiate` rows collapse by default | -| `callers` | List callers for a named symbol in supported languages; `kind` filters by reference kind. The default keeps call-graph kinds visible (`call`, `instantiate`, `subscribe`) while excluding metadata edges (`attribute`, `annotation`); pass `kind: "attribute"` or `kind: "annotation"` to surface those explicitly. Identical constructor `call` + `instantiate` rows at one physical site collapse. | +| `callers` | List callers for a named symbol in supported languages; `kind` filters by reference kind. The default keeps call-graph kinds visible (`call`, `instantiate`, `subscribe`) while excluding metadata edges (`attribute`, `annotation`). `callers` is not a reliable path to metadata — an attribute / annotation row is attributed to the enclosing body-range symbol (the class for a member declaration) or drops entirely when the target is file-level (`[assembly: ...]`, where `container_name` is `null`). Use `references` with `kind: "attribute"` / `kind: "annotation"` for metadata enumeration. Identical constructor `call` + `instantiate` rows at one physical site collapse. | | `callees` | List callees for a named symbol in supported languages; the default keeps call-graph kinds visible (`call`, `instantiate`, `subscribe`) while excluding metadata edges (`attribute`, `annotation`). Identical constructor `call` + `instantiate` rows at one physical site collapse. | | `symbols` | Find functions, classes, interfaces, imports, and namespaces by name | | `files` | List indexed files | @@ -1680,7 +1680,7 @@ OpenAI Codex CLI (`codex.json` または `~/.codex/config.json`): | `search` | コードチャンクの全文検索 | | `definition` | シンボルの宣言と必要なら本体を再構成して取得 | | `references` | 対応言語でインデックス済み参照を検索。constructor site の `call` + `instantiate` 重複は既定で集約 | -| `callers` | 対応言語で指定シンボルの caller を列挙。`kind` は reference kind を指し、既定では call-graph kind(`call`、`instantiate`、`subscribe`)のみを表示して `attribute` / `annotation` のような metadata edge は除外する。C# の `[...]` 属性や Java 系 `@Annotation(...)` を取りたいときは `kind: "attribute"` / `kind: "annotation"` を明示する。同じ物理位置にある constructor の `call` + `instantiate` 重複は集約する。 | +| `callers` | 対応言語で指定シンボルの caller を列挙。`kind` は reference kind を指し、既定では call-graph kind(`call`、`instantiate`、`subscribe`)のみを表示して `attribute` / `annotation` のような metadata edge は除外する。metadata 行の container は注釈対象そのものではなく body-range 上の外側シンボル(メンバ宣言ならクラス)に設定され、`[assembly: ...]` のようなファイルレベル target では `container_name` が `null` になって `callers` 結果から脱落する。C# の `[...]` 属性や Java 系 `@Annotation(...)` を列挙したいときは `references --kind attribute|annotation` / MCP `references` を使う。同じ物理位置にある constructor の `call` + `instantiate` 重複は集約する。 | | `callees` | 対応言語で指定シンボルの callee を列挙。既定は call-graph kind(`call`、`instantiate`、`subscribe`)のみで、`attribute` / `annotation` のような metadata edge は除外する。同じ物理位置にある constructor の `call` + `instantiate` 重複は集約する。 | | `symbols` | 関数・クラス・インターフェース・import・namespace を名前で検索 | | `files` | インデックス済みファイル一覧 | diff --git a/src/CodeIndex/Indexer/ReferenceExtractor.cs b/src/CodeIndex/Indexer/ReferenceExtractor.cs index e684ea6b35..8d7ba6c28b 100644 --- a/src/CodeIndex/Indexer/ReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/ReferenceExtractor.cs @@ -133,17 +133,19 @@ public static class ReferenceExtractor private static readonly Regex EventSubscriptionRegex = new(@"(?[A-Z]\w*)\s*[+-]=\s*(?:new\s+)?[A-Z]\w*", RegexOptions.Compiled); // No-arg C# attribute name (`[Serializable]`, `[assembly: CLSCompliant]`, `[System.Obsolete]`, - // `[Required, Key]`). CallRegex only matches identifiers followed by `(`, so no-arg attributes - // would otherwise never be indexed. The pattern anchors to `[` / `,` boundaries and refuses to - // match when the identifier is followed by `(` (handled by CallRegex + TryClassifyMetadataReference) - // or `.` (qualifier continues). A subsequent `IsInsideCSharpAttributeRange` filter keeps the - // match from firing on indexer access like `arr[i]`. + // `[global::System.Obsolete]`, `[Alias::MyAttr]`, `[Required, Key]`). CallRegex only matches + // identifiers followed by `(`, so no-arg attributes would otherwise never be indexed. The + // pattern anchors to `[` / `,` boundaries and refuses to match when the identifier is followed + // by `(` (handled by CallRegex + TryClassifyMetadataReference) or a qualifier continuation + // (`.` / `::`). A subsequent `IsInsideCSharpAttributeRange` filter keeps the match from firing + // on indexer access like `arr[i]`. // 引数なしの C# attribute 名用 regex。`[Serializable]` などは CallRegex では拾えないため専用の - // 入口で捕捉する。`[` / `,` 境界にアンカーし、後続が `(`(CallRegex 経路で処理)や `.`(qualifier 継続) - // なら採用しない。`arr[i]` のような indexer を排除するため、マッチ後に + // 入口で捕捉する。`global::System.Obsolete` や `Alias::MyAttr` のように `::` 修飾子の付く形も + // 許容する。`[` / `,` 境界にアンカーし、後続が `(`(CallRegex 経路で処理)や `.` / `::` + // (qualifier 継続)なら採用しない。`arr[i]` のような indexer を排除するため、マッチ後に // `IsInsideCSharpAttributeRange` で範囲内かも確認する。 private static readonly Regex CSharpNoArgAttributeRegex = new( - @"[\[,]\s*(?:[A-Za-z_]\w*\s*:\s*)?(?:[A-Za-z_]\w*\s*\.\s*)*(?[A-Za-z_]\w*)\s*(?=[\],])", + @"[\[,]\s*(?:[A-Za-z_]\w*\s*:\s*)?(?:[A-Za-z_]\w*\s*(?:\.|::)\s*)*(?[A-Za-z_]\w*)\s*(?=[\],])", RegexOptions.Compiled); // No-arg Java-family annotation (`@Deprecated`, `@Override`, `@org.junit.Test`, `@field:Deprecated`). @@ -162,7 +164,7 @@ public static class ReferenceExtractor // 記録すべき言語 (issue #293)。 private static readonly HashSet AnnotationLanguages = new(StringComparer.Ordinal) { - "java", "kotlin", "scala", "typescript", + "java", "kotlin", "scala", "typescript", "javascript", }; // Kotlin use-site target prefixes for annotations (e.g. `@field:Deprecated("msg")`, diff --git a/src/CodeIndex/Mcp/McpToolDefinitions.cs b/src/CodeIndex/Mcp/McpToolDefinitions.cs index ea3807ec95..96045f2bc4 100644 --- a/src/CodeIndex/Mcp/McpToolDefinitions.cs +++ b/src/CodeIndex/Mcp/McpToolDefinitions.cs @@ -89,7 +89,7 @@ private JsonNode HandleToolsList(JsonNode? id) ReadOnlyAnnotations()), CreateToolDefinition( "callers", - "Find caller symbols that reference a callee. When `kind` is omitted, only call-graph kinds (`call`, `instantiate`, `subscribe`) are returned so metadata uses (`attribute` / `annotation`) do not pollute caller edges; identical constructor `call` + `instantiate` rows at one physical site also collapse. Pass `kind: \"attribute\"` or `kind: \"annotation\"` to inspect metadata references explicitly. / 指定シンボルを参照している呼び出し元シンボルを探す。`kind` 未指定時は call-graph 種別 (`call` / `instantiate` / `subscribe`) のみを返し、metadata 使用 (`attribute` / `annotation`) が phantom caller edge として混入しないようにする。同じ物理位置にある constructor の `call` + `instantiate` 重複行も集約する。Metadata の参照を明示的に確認したい場合は `kind: \"attribute\"` / `kind: \"annotation\"` を指定する。", + "Find caller symbols that reference a callee. When `kind` is omitted, only call-graph kinds (`call`, `instantiate`, `subscribe`) are returned so metadata uses (`attribute` / `annotation`) do not pollute caller edges; identical constructor `call` + `instantiate` rows at one physical site also collapse. `callers` / `callees` are not a reliable path to metadata — an attribute / annotation row is attributed to its enclosing body-range symbol (for a class-level declaration, that is the class itself; for a file-level target such as `[assembly: ...]`, `container_name` is `null` and the row drops from these graph queries entirely). Use `references` with `kind: \"attribute\"` or `kind: \"annotation\"` for metadata enumeration. / 指定シンボルを参照している呼び出し元シンボルを探す。`kind` 未指定時は call-graph 種別 (`call` / `instantiate` / `subscribe`) のみを返し、metadata 使用 (`attribute` / `annotation`) が phantom caller edge として混入しないようにする。同じ物理位置にある constructor の `call` + `instantiate` 重複行も集約する。metadata 行の container は注釈対象そのものではなく body-range 上の外側シンボル(クラス直下宣言ならクラス、ファイルレベル target なら `null`)になるため、`callers` / `callees` は metadata 列挙に向かない。Metadata の参照列挙は `references --kind attribute|annotation` / MCP `references` を使う。", new JsonObject { ["type"] = "object", @@ -110,7 +110,7 @@ private JsonNode HandleToolsList(JsonNode? id) ReadOnlyAnnotations()), CreateToolDefinition( "callees", - "Find callees used by a caller/container symbol. When `kind` is omitted, only call-graph kinds (`call`, `instantiate`, `subscribe`) are returned so metadata uses (`attribute` / `annotation`) do not pollute callee edges; identical constructor `call` + `instantiate` rows at one physical site also collapse. Pass `kind: \"attribute\"` or `kind: \"annotation\"` to inspect metadata references explicitly. / 呼び出し元シンボルが使っている呼び出し先を探す。`kind` 未指定時は call-graph 種別 (`call` / `instantiate` / `subscribe`) のみを返し、metadata 使用 (`attribute` / `annotation`) が phantom callee edge として混入しないようにする。同じ物理位置にある constructor の `call` + `instantiate` 重複行も集約する。Metadata を明示的に見たい場合は `kind: \"attribute\"` / `kind: \"annotation\"` を指定する。", + "Find callees used by a caller/container symbol. When `kind` is omitted, only call-graph kinds (`call`, `instantiate`, `subscribe`) are returned so metadata uses (`attribute` / `annotation`) do not pollute callee edges; identical constructor `call` + `instantiate` rows at one physical site also collapse. `callees` is not a reliable path to metadata — the container assigned to an attribute / annotation row is the enclosing body-range symbol, not the annotated declaration, so `callees Method1 --kind attribute` does not return the attributes on `Method1`. Use `references` with `kind: \"attribute\"` or `kind: \"annotation\"` for metadata enumeration. / 呼び出し元シンボルが使っている呼び出し先を探す。`kind` 未指定時は call-graph 種別 (`call` / `instantiate` / `subscribe`) のみを返し、metadata 使用 (`attribute` / `annotation`) が phantom callee edge として混入しないようにする。同じ物理位置にある constructor の `call` + `instantiate` 重複行も集約する。metadata 行の container は注釈対象自身ではなく body-range 上の外側シンボルになるため、`callees` で `Method1 --kind attribute` を引いても `Method1` に付いた属性は返らない。Metadata の列挙は `references --kind attribute|annotation` / MCP `references` を使う。", new JsonObject { ["type"] = "object", diff --git a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs index a1ff525f95..88da703233 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs @@ -1791,6 +1791,75 @@ fun outer() { Assert.DoesNotContain(references, r => r.SymbolName == "foo" && r.ReferenceKind == "annotation"); } + [Fact] + public void Extract_CsharpGlobalQualifiedNoArgAttribute_ClassifiedAsAttribute() + { + // Regression (issue #293 follow-up): `[global::System.Obsolete]` — fully qualified + // attribute using the `global::` alias. The no-arg attribute regex must accept both + // `.` and `::` as qualifier separators so these references are not silently dropped. + // リグレッション (issue #293 補足): `[global::System.Obsolete]` のように `::` で修飾した + // 引数なし属性も `attribute` として取り込まれること。 + const string content = """ + [global::System.Obsolete] + public class C + { + } + """; + + var references = ReferenceExtractor.Extract(1, "csharp", content, []); + + var obsolete = Assert.Single(references.Where(r => r.SymbolName == "Obsolete")); + Assert.Equal("attribute", obsolete.ReferenceKind); + Assert.DoesNotContain(references, r => r.ReferenceKind == "call"); + } + + [Fact] + public void Extract_CsharpAliasQualifiedNoArgAttribute_ClassifiedAsAttribute() + { + // Regression (issue #293 follow-up): `[Alias::MyAttr]` — alias-qualified attribute. + // The qualifier separator may be `::` (extern alias) as well as `.`; the name segment + // must still be emitted with kind `attribute`. + // リグレッション (issue #293 補足): `[Alias::MyAttr]` のように extern alias 修飾された + // 引数なし属性も `attribute` として取り込まれること。 + const string content = """ + [Alias::MyAttr] + public class C + { + } + """; + + var references = ReferenceExtractor.Extract(1, "csharp", content, []); + + var attr = Assert.Single(references.Where(r => r.SymbolName == "MyAttr")); + Assert.Equal("attribute", attr.ReferenceKind); + Assert.DoesNotContain(references, r => r.ReferenceKind == "call"); + } + + [Fact] + public void Extract_JavaScriptDecorator_ClassifiedAsAnnotation() + { + // Regression (issue #293 follow-up): JavaScript is a graph-supported language, so + // its `@Decorator` / `@Decorator()` forms must be reclassified to `annotation` instead + // of leaking into call-graph edges. Both bare `@sealed` (no-arg, via the dedicated + // regex) and `@injectable()` (via CallRegex + TryClassifyMetadataReference) must end + // up as `annotation`. + // リグレッション (issue #293 補足): JavaScript も graph 対応言語なので、`@Decorator` + // / `@Decorator()` は `annotation` に再分類され、call graph を汚染しないこと。 + const string content = """ + @sealed + @injectable() + class Foo {} + """; + + var references = ReferenceExtractor.Extract(1, "javascript", content, []); + + var sealedRef = Assert.Single(references.Where(r => r.SymbolName == "sealed")); + Assert.Equal("annotation", sealedRef.ReferenceKind); + var injectable = Assert.Single(references.Where(r => r.SymbolName == "injectable")); + Assert.Equal("annotation", injectable.ReferenceKind); + Assert.DoesNotContain(references, r => r.ReferenceKind == "call"); + } + [Fact] public void Extract_KotlinQualifiedFieldTargetAnnotation_ClassifiedAsAnnotation() { From 5f1626f3a32ff8dc1c620e17d8564672bc84a251 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 18 Apr 2026 01:30:10 +0900 Subject: [PATCH 08/30] Fix #293 index multi-line no-arg C# attribute sections - Relax the C# no-arg attribute regex to accept line-end on the right and use a word-boundary lookbehind on the left, so a bare identifier on the interior line of a multi-line `[...]` section (for example ` Serializable` between line-leading `[` and `]`) is still matched. The downstream `IsInsideCSharpAttributeRange` gate keeps indexer access such as `arr[i]` out of the attribute classification. - Track multi-line `[...]` bracket depth across lines in `SymbolExtractor.BuildCSharpMatchLines` via a new `StripMultiLineCSharpAttributeInterior` helper, so interior lines of a multi-line attribute section are blanked out before declaration regexes run. Without this, ` Serializable` was being extracted as a top-level `function` symbol, which then suppressed the `attribute`-reference match through the `definitionNames` guard. - Regression tests add `[\n Serializable\n]`, `[\n global::System.Obsolete\n]`, and `[Required,\n Key]` and drive the fixtures through `SymbolExtractor.Extract` + `ReferenceExtractor.Extract` end-to-end so both sides of the regression stay pinned. Co-Authored-By: Claude Opus 4 --- CHANGELOG.md | 2 + src/CodeIndex/Indexer/ReferenceExtractor.cs | 24 ++++--- src/CodeIndex/Indexer/SymbolExtractor.cs | 67 ++++++++++++++++++- .../ReferenceExtractorTests.cs | 46 +++++++++++++ 4 files changed, 128 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85390daeda..82547b99b7 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/), - **Developer Guide now distinguishes SQLite's role from "zero dependencies" and labels release numbers as examples** — Clarified that cdidx remains a zero-configuration, single-file CLI with exactly one production dependency (`Microsoft.Data.Sqlite`), and made the release-workflow checklist explicitly state that `1.9.0` is only an example version to substitute during an actual release. Affected: `DEVELOPER_GUIDE.md`. #### Fixed +- **Multi-line no-arg C# attribute sections like `[\n Serializable\n]` are now indexed (#293 follow-up)** — The no-arg C# attribute regex previously required the match to start at a `[` / `,` boundary and end at `]` / `,` on the same line, so a bare identifier on the interior line of a multi-line attribute section (e.g. ` Serializable` between a line-leading `[` and a line-leading `]`) was silently dropped from `references --kind attribute`. The left anchor is now a word-boundary lookbehind and the right anchor also accepts end-of-line, while the downstream `IsInsideCSharpAttributeRange` gate still rejects indexer access like `arr[i]`. `SymbolExtractor` also now tracks multi-line `[...]` depth across lines via `StripMultiLineCSharpAttributeInterior` in `BuildCSharpMatchLines`, so interior lines of a multi-line attribute section are blanked out before declaration regexes run — this prevents ` Serializable` from being mis-extracted as a top-level `function` symbol (which then masked the reference through the `definitionNames` guard). Regression coverage adds `[\n Serializable\n]`, `[\n global::System.Obsolete\n]`, and `[Required,\n Key]` and runs through `SymbolExtractor.Extract` + `ReferenceExtractor.Extract` end-to-end so the SymbolExtractor side of the regression stays pinned. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Indexer/SymbolExtractor.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. - **`::`-qualified C# no-arg attributes and JavaScript `@Decorator` usages are now classified correctly (#293 follow-up)** — The no-arg C# attribute regex now accepts `::` alongside `.` as a qualifier separator, so `[global::System.Obsolete]` and `[Alias::MyAttr]` reach `references --kind attribute` instead of being silently dropped. `AnnotationLanguages` additionally includes `javascript`, so JS decorators such as `@sealed` (no-arg) and `@injectable()` (with args) are reclassified to `annotation` rather than leaking into the call-graph as phantom `call` rows — matching the existing TypeScript contract since JavaScript is already a graph-supported language. Added regression tests pinning `[global::System.Obsolete]`, `[Alias::MyAttr]`, and JavaScript `@sealed` / `@injectable()`. Also rewrote the README `callers` row (EN + JP) and the MCP tool descriptions for `callers` / `callees` to pull back the earlier over-promise that `kind: "attribute"` / `kind: "annotation"` on `callers` / `callees` is a supported metadata-inspection path: a metadata row is attributed to its enclosing body-range symbol (for a class-level declaration, that is the class itself) and drops entirely for file-level targets such as `[assembly: ...]` where `container_name` is `null`, so the docs now direct users to `references --kind attribute|annotation` for metadata enumeration. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`, `README.md`. - **`impact` BFS now follows C# event subscribe edges (#293 follow-up)** — `DbReader.GetCallersExact`, the only caller site used by the `impact` / `impact_analysis` BFS, now filters references through the shared `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` set instead of `InvokeReferenceKindsSql = ('call', 'instantiate')`. Previously `impact` quietly dropped `Changed += OnChanged` subscribe rows while `callers` / `callees` / `hotspots` kept them, so an event-driven transitive caller chain appeared shorter under `impact` than under `callers`. A new `DbReaderTests.GetTransitiveCallers_FollowsSubscribeEdges` regression pins the subscribe edge into the BFS output. Metadata edges (`attribute`, `annotation`) remain excluded. Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. - **C# parameter, type-parameter, lambda, and multi-line `[...]` attribute lists are now classified as `attribute`, no-arg attributes and annotations are indexed, and MCP tool descriptions document the default metadata filter (#293 follow-up)** — `ReferenceExtractor` now runs a pre-pass that tokenizes every C# `[...]` section across the whole file, so parameter attributes such as `void M([FromServices] IService s)` (where `[` is preceded by `(` or `,` rather than a declaration boundary), type-parameter attributes such as `class C<[Attr("x")] T>` (preceded by `<`), tuple-typed parameter attributes such as `void M([Attr("x")] (int a, int b) value)` (where the next token after `]` is `(`), lambda attributes such as `var f = [Attr("x")] () => 0;` (where `[` is preceded by `=`), and multi-line forms such as `[\n Foo("x")\n]` or `public void M(\n [Attr("x")]\n int a)` are recognized alongside the existing same-line cases. The pre-pass disambiguates the `(` / `,` / `<` / `=` entry point against C# 12 collection expressions in argument position (`Consume([Make()])`), pattern expressions (`[Make()] is int[] xs`), `as` casts (`[Make()] as int[]`), and switch expressions (`[Make()] switch { _ => true }`) by scanning forward from `[` to the matching `]` and checking whether the next meaningful token begins a declaration — identifiers are accepted unless they are the expression-continuation keywords `is` / `as` / `switch` / `with` / `when`, `@` is accepted as a verbatim-identifier lead, `(` is accepted for tuple-typed parameters and lambda parameter lists, and chained `[A][B]` recurses through the inner bracket. Bare no-arg C# attributes such as `[Serializable]`, `[Obsolete]`, `[System.Obsolete]`, `[assembly: CLSCompliant]`, and `[Required, Key]`, plus bare no-arg Java-family annotations such as `@Deprecated`, `@Override`, `@org.junit.Test`, and `@field:Deprecated`, are now emitted through dedicated no-arg regexes since the original `CallRegex` requires `(` — the C# path still gates on the attribute-range pre-pass so indexer access such as `arr[i]` stays unclassified, and the annotation path uses a lookbehind `(?`, tuple-typed parameter `void M([Attr("x")] (int a, int b) value)`, lambda `var f = [Attr("x")] () => 0;`, the no-arg C# attribute set above, the no-arg Java `@Deprecated` / `@Override` / `@org.junit.Test` set, Kotlin `@field:Deprecated` plus the `return@foo` negative, indexer access staying unclassified, `[\n Foo("x")\n]`, the cross-line parameter attribute shape, the targeted `[return: ...]` shape, the defense-in-depth collection-expression-in-argument case, and the three expression-continuation cases `[Make()] is ...`, `[Make()] as int[]`, and `[Make()] switch { ... }`. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. @@ -681,6 +682,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **DEVELOPER_GUIDE が SQLite の位置付けとリリース番号の例示を明確化** — cdidx が「設定ゼロ・単一ファイル・本番依存1個」の CLI であり、その唯一の本番依存が `Microsoft.Data.Sqlite` であることを明記した。あわせて、リリース手順のチェックリスト中に出てくる `1.9.0` は固定値ではなく例示であり、実際のリリース番号へ読み替える前提だと明示した。対象: `DEVELOPER_GUIDE.md`。 #### 修正 +- **`[\n Serializable\n]` のような複数行にまたがる引数なし C# 属性セクションを索引化 (#293 追加対応)** — 引数なし C# 属性の正規表現は、これまで `[` / `,` を左端、`]` / `,` を右端として同一行内でアンカーしていたため、行頭 `[` と行頭 `]` の間に書かれた ` Serializable` のような裸識別子が `references --kind attribute` から黙って脱落していた。左側は単語境界 lookbehind、右側は行末(`$`)も許容するように緩和し、誤検出を防ぐため引き続き `IsInsideCSharpAttributeRange` の属性レンジ内かどうかでゲートする(`arr[i]` のような indexer アクセスは弾く)。`SymbolExtractor` 側でも `BuildCSharpMatchLines` に `StripMultiLineCSharpAttributeInterior` を追加し、複数行にまたがる `[...]` セクションの深さを跨行で追跡して内部行を宣言 regex 適用前に空白化するようにした。これにより ` Serializable` がトップレベル `function` シンボルとして誤抽出され、`definitionNames` ガード経由で参照分類が詰まってしまうのを防ぐ。`[\n Serializable\n]`・`[\n global::System.Obsolete\n]`・`[Required,\n Key]` を `SymbolExtractor.Extract` + `ReferenceExtractor.Extract` で end-to-end に回して SymbolExtractor 側の回帰も固定する回帰テストを追加。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Indexer/SymbolExtractor.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 - **`::` 修飾の C# 引数なし属性と JavaScript の `@Decorator` を正しく分類 (#293 追加対応)** — 引数なし C# 属性の正規表現が修飾子セパレータとして `.` に加えて `::` も受け付けるようになり、`[global::System.Obsolete]` や `[Alias::MyAttr]` のような形が `references --kind attribute` から黙って脱落しなくなった。`AnnotationLanguages` にも `javascript` を追加し、JavaScript はもともと graph 対応言語なので、`@sealed`(引数なし)や `@injectable()`(引数付き)のような JS decorator も `annotation` として再分類され、TypeScript と同じ契約で call-graph に phantom `call` 行を残さない。`[global::System.Obsolete]`・`[Alias::MyAttr]`・JavaScript の `@sealed` / `@injectable()` を押さえる回帰テストを追加。あわせて、以前に進めていた `callers` / `callees` の metadata 契約を README(英日)と MCP ツール説明の両方で引き戻し、metadata 行は注釈対象そのものではなく body-range 上の外側シンボル(クラス直下宣言ならクラス、`[assembly: ...]` のようなファイルレベル target では `container_name` が `null`)に帰属するため `kind: "attribute"` / `kind: "annotation"` を `callers` / `callees` に渡すのは信頼できる metadata 調査経路ではなく、metadata 列挙は `references --kind attribute|annotation` を使うよう案内する文言に修正した。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Mcp/McpToolDefinitions.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`、`README.md`。 - **`impact` BFS が C# の event subscribe edge を辿るよう修正 (#293 追加対応)** — `impact` / `impact_analysis` BFS の唯一の呼び出し箇所である `DbReader.GetCallersExact` が、共有 filter `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` で参照を絞り込むようになった(以前は `InvokeReferenceKindsSql = ('call', 'instantiate')` を使っていたため、`Changed += OnChanged` のような subscribe 行を静かに捨てていた)。これにより `callers` / `callees` / `hotspots` と同じ call-graph 契約を `impact` も共有し、イベント駆動の transitive caller chain が `impact` でだけ短くなる不整合が解消する。`DbReaderTests.GetTransitiveCallers_FollowsSubscribeEdges` の回帰テストで subscribe edge が BFS 出力に含まれることを固定。Metadata edge (`attribute` / `annotation`) の除外は従来通り。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 - **C# のパラメータ属性・型パラメータ属性・ラムダ属性・複数行 `[...]` 属性を `attribute` として分類し、引数なし属性とアノテーションも索引化、MCP ツール説明に既定の metadata フィルタを反映 (#293 追加対応)** — `ReferenceExtractor` がファイル全体の C# `[...]` セクションを事前パスでトークン化するようになり、`void M([FromServices] IService s)` のように `[` の直前が宣言境界ではなく `(` / `,` で始まるパラメータ属性、`class C<[Attr("x")] T>` のように `<` で始まる型パラメータ属性、`void M([Attr("x")] (int a, int b) value)` のように `]` の直後が `(` のまま続く tuple 型パラメータ属性、`var f = [Attr("x")] () => 0;` のように `=` の直後にあるラムダ属性、さらに `[\n Foo("x")\n]` や `public void M(\n [Attr("x")]\n int a)` のような複数行形式も、既存の同一行ケースと同じように属性として扱われるようになった。`(` / `,` / `<` / `=` 直後の曖昧さは、`[` から対応する `]` まで前方走査し、`]` の直後の非空白トークンが宣言を開始するかで判定する — 識別子は原則受理するが、式継続キーワード `is` / `as` / `switch` / `with` / `when` は宣言扱いしないので `Consume([Make()] is int[] xs)` / `([Make()] as int[])` / `Consume([Make()] switch { _ => true })` のような collection expression は `call` のまま残り、`@` は verbatim 識別子の先頭として、`(` は tuple 型パラメータとラムダ仮引数リストの先頭として受理し、`[A][B]` のような連続 bracket は内側 `[` に再帰して判定する。さらに `[Serializable]`、`[Obsolete]`、`[System.Obsolete]`、`[assembly: CLSCompliant]`、`[Required, Key]` のような引数なし C# 属性と、`@Deprecated`・`@Override`・`@org.junit.Test`・`@field:Deprecated` のような引数なし Java 系アノテーションは、`(` を要求する既存の `CallRegex` では拾えないため専用の no-arg 正規表現から emit するようになった。C# 側では引き続き `[...]` 事前パスのレンジに含まれるかで gate するので `arr[i]` のような indexer アクセスは属性化されず、アノテーション側は lookbehind `(?` 型パラメータ属性、`void M([Attr("x")] (int a, int b) value)` tuple 型パラメータ属性、`var f = [Attr("x")] () => 0;` ラムダ属性、上記 no-arg C# 属性群、Java `@Deprecated` / `@Override` / `@org.junit.Test` の no-arg アノテーション群、Kotlin `@field:Deprecated` と `return@foo` の negative ケース、indexer アクセスが属性化されないこと、`[\n Foo("x")\n]`、改行を挟んだパラメータ属性、`[return: NotNullWhen(true)]`、防御的に `Consume([Make()])` の collection expression-in-argument、および `[Make()] is ...` / `[Make()] as int[]` / `[Make()] switch { ... }` の 3 つの式継続ケースを押さえる回帰テストを追加。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Mcp/McpToolDefinitions.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 diff --git a/src/CodeIndex/Indexer/ReferenceExtractor.cs b/src/CodeIndex/Indexer/ReferenceExtractor.cs index 8d7ba6c28b..05b451ae51 100644 --- a/src/CodeIndex/Indexer/ReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/ReferenceExtractor.cs @@ -133,19 +133,23 @@ public static class ReferenceExtractor private static readonly Regex EventSubscriptionRegex = new(@"(?[A-Z]\w*)\s*[+-]=\s*(?:new\s+)?[A-Z]\w*", RegexOptions.Compiled); // No-arg C# attribute name (`[Serializable]`, `[assembly: CLSCompliant]`, `[System.Obsolete]`, - // `[global::System.Obsolete]`, `[Alias::MyAttr]`, `[Required, Key]`). CallRegex only matches - // identifiers followed by `(`, so no-arg attributes would otherwise never be indexed. The - // pattern anchors to `[` / `,` boundaries and refuses to match when the identifier is followed - // by `(` (handled by CallRegex + TryClassifyMetadataReference) or a qualifier continuation - // (`.` / `::`). A subsequent `IsInsideCSharpAttributeRange` filter keeps the match from firing - // on indexer access like `arr[i]`. + // `[global::System.Obsolete]`, `[Alias::MyAttr]`, `[Required, Key]`, and their multi-line + // variants where `[` / `]` sit on separate lines). CallRegex only matches identifiers followed + // by `(`, so no-arg attributes would otherwise never be indexed. The pattern refuses to match + // when the identifier is followed by `(` (handled by CallRegex + TryClassifyMetadataReference) + // or a qualifier continuation (`.` / `::`). The match is gated downstream by + // `IsInsideCSharpAttributeRange`, so it is safe to relax the `[` / `,` left-anchor in favor of + // a word-boundary lookbehind — that lets a bare identifier on a line like ` Serializable` + // inside a multi-line attribute section still be recognized. // 引数なしの C# attribute 名用 regex。`[Serializable]` などは CallRegex では拾えないため専用の // 入口で捕捉する。`global::System.Obsolete` や `Alias::MyAttr` のように `::` 修飾子の付く形も - // 許容する。`[` / `,` 境界にアンカーし、後続が `(`(CallRegex 経路で処理)や `.` / `::` - // (qualifier 継続)なら採用しない。`arr[i]` のような indexer を排除するため、マッチ後に - // `IsInsideCSharpAttributeRange` で範囲内かも確認する。 + // 許容する。`[` / `,` / `]` が別行にある複数行形(例: `[\n Serializable\n]`)も取り込むため、 + // 左側は `[` / `,` ではなく単語境界だけでアンカーする。属性以外の位置で誤検出しないよう、 + // マッチ後は `IsInsideCSharpAttributeRange` で属性レンジ内かどうかを確認する。後続が `(` + // (CallRegex 経路)や `.` / `::`(qualifier 継続)なら名前を確定させず、行末(`$`)・`]`・`,` + // のいずれかで初めて採用する。 private static readonly Regex CSharpNoArgAttributeRegex = new( - @"[\[,]\s*(?:[A-Za-z_]\w*\s*:\s*)?(?:[A-Za-z_]\w*\s*(?:\.|::)\s*)*(?[A-Za-z_]\w*)\s*(?=[\],])", + @"(?[A-Za-z_]\w*)\s*(?=[\],]|$)", RegexOptions.Compiled); // No-arg Java-family annotation (`@Deprecated`, `@Override`, `@org.junit.Test`, `@field:Deprecated`). diff --git a/src/CodeIndex/Indexer/SymbolExtractor.cs b/src/CodeIndex/Indexer/SymbolExtractor.cs index 01ead9408b..edce0b1d4a 100644 --- a/src/CodeIndex/Indexer/SymbolExtractor.cs +++ b/src/CodeIndex/Indexer/SymbolExtractor.cs @@ -5345,16 +5345,81 @@ private static string[] BuildCSharpMatchLines(string[] structuralLines) { var matchLines = new string[structuralLines.Length]; var lexState = new CSharpLexState(); + // Track multi-line attribute bracket depth so interior lines of a `[\n Serializable\n]` + // section are blanked out before the declaration-pattern regexes run. Otherwise a bare + // identifier on a line like ` Serializable` gets picked up as a top-level function + // and poisons reference classification via the `definitionNames` guard. + // 複数行にまたがる `[...]` 属性セクションの内部行を空行扱いするため、`[` / `]` の + // 深さを跨行で追跡する。そうしないと ` Serializable` のような裸識別子が + // トップレベル関数として抽出されてしまい、`definitionNames` ガードで参照分類が壊れる。 + int multiLineAttrDepth = 0; for (int i = 0; i < structuralLines.Length; i++) { var lexedLine = LexCSharpLine(structuralLines[i], lexState); lexState = lexedLine.EndState; - matchLines[i] = CollapseCSharpGenericTypeWhitespace(StripLeadingCSharpAttributeLists(lexedLine.SanitizedLine)); + var sanitized = StripMultiLineCSharpAttributeInterior(lexedLine.SanitizedLine, ref multiLineAttrDepth); + matchLines[i] = CollapseCSharpGenericTypeWhitespace(StripLeadingCSharpAttributeLists(sanitized)); } return matchLines; } + /// + /// Track multi-line C# `[...]` attribute sections across lines and blank out any text that + /// sits inside those sections, so downstream symbol regexes do not treat interior identifiers + /// as declarations. Only activates when a leading `[` (optionally after whitespace) opens the + /// section without a matching `]` on the same line — single-line attribute lists continue to + /// be handled by `StripLeadingCSharpAttributeLists`. + /// 複数行にまたがる C# `[...]` 属性セクションを跨行で追跡し、内部の文字列を空白化することで + /// 下流のシンボル regex が内部の識別子を宣言として誤解釈しないようにする。行頭(空白の後)の + /// `[` が同一行内で閉じないときだけ起動し、同一行で完結する属性リストは + /// `StripLeadingCSharpAttributeLists` が引き続き担当する。 + /// + private static string StripMultiLineCSharpAttributeInterior(string line, ref int depth) + { + if (depth == 0) + { + // Detect the opening of a multi-line attribute section: line starts with `[`, and the + // `[` is not closed on the same line (single-line cases are handled downstream). + var cursor = 0; + while (cursor < line.Length && char.IsWhiteSpace(line[cursor])) + cursor++; + if (cursor >= line.Length || line[cursor] != '[') + return line; + + // Count `[` / `]` on this line to decide whether we span into following lines. + int localDepth = 0; + for (int i = cursor; i < line.Length; i++) + { + if (line[i] == '[') localDepth++; + else if (line[i] == ']') + { + localDepth--; + if (localDepth == 0) + return line; // single-line section — leave to StripLeadingCSharpAttributeLists + } + } + if (localDepth <= 0) + return line; + + depth = localDepth; + return string.Empty; + } + + // We are inside a multi-line attribute section. Walk the line, closing brackets when we + // see `]`. Once depth returns to zero, the remainder of the line is real code. + int index = 0; + while (index < line.Length && depth > 0) + { + if (line[index] == '[') depth++; + else if (line[index] == ']') depth--; + index++; + } + if (depth > 0) + return string.Empty; + return line[index..]; + } + private static CSharpPropertyMatchCandidate BuildCSharpPropertyMatchLine(string[] lines, string[] csharpMatchLines, int startLineIndex) { var matchLine = csharpMatchLines[startLineIndex]; diff --git a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs index 88da703233..e0d99d8211 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs @@ -1813,6 +1813,52 @@ public class C Assert.DoesNotContain(references, r => r.ReferenceKind == "call"); } + [Fact] + public void Extract_CsharpMultiLineNoArgAttribute_ClassifiedAsAttribute() + { + // Regression (issue #293 follow-up): multi-line no-arg attribute forms such as + // `[\n Serializable\n]`, `[\n global::System.Obsolete\n]`, and `[Serializable,\n Obsolete]` + // must still classify as `attribute`. The attribute range pre-pass already tracks the + // section across line breaks; the no-arg regex must not reject identifiers just because + // the opening `[` or `,` is on a previous line. + // リグレッション (issue #293 補足): `[\n Serializable\n]` のように `[` と識別子が別行に + // ある複数行形、`[\n global::System.Obsolete\n]` のような `::` 修飾複数行形、そして + // `[Serializable,\n Obsolete]` のような行を跨ぐカンマ区切りも `attribute` として取り込まれること。 + const string content = """ + [ + Serializable + ] + [ + global::System.Obsolete + ] + [Required, + Key] + public class C + { + } + """; + + // Use SymbolExtractor to mirror end-to-end indexing: if SymbolExtractor misclassifies + // a bare identifier inside a multi-line attribute section as a top-level symbol, the + // reference would be filtered out via the `definitionNames` guard and this test would + // catch that regression instead of silently passing with `[]` symbols. + // SymbolExtractor を通すことで end-to-end と同じ流れを再現する。複数行属性セクション内の + // 裸識別子を誤ってトップレベルのシンボルとして抽出してしまうと `definitionNames` ガードで + // 参照が脱落してしまうため、本テストがその退行も検出する。 + var symbols = SymbolExtractor.Extract(1, "csharp", content); + var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); + + var serializable = Assert.Single(references.Where(r => r.SymbolName == "Serializable")); + Assert.Equal("attribute", serializable.ReferenceKind); + var obsolete = Assert.Single(references.Where(r => r.SymbolName == "Obsolete")); + Assert.Equal("attribute", obsolete.ReferenceKind); + var required = Assert.Single(references.Where(r => r.SymbolName == "Required")); + Assert.Equal("attribute", required.ReferenceKind); + var key = Assert.Single(references.Where(r => r.SymbolName == "Key")); + Assert.Equal("attribute", key.ReferenceKind); + Assert.DoesNotContain(references, r => r.ReferenceKind == "call"); + } + [Fact] public void Extract_CsharpAliasQualifiedNoArgAttribute_ClassifiedAsAttribute() { From 71552c736c5f23920a7b5dc44e2771bd90834676 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 18 Apr 2026 04:03:47 +0900 Subject: [PATCH 09/30] Fix #293 gate no-arg attribute matches on top-level zones MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous follow-up commit relaxed `CSharpNoArgAttributeRegex` to accept end-of-line on the right and a word-boundary lookbehind on the left so that `[\n Serializable\n]` multi-line forms would match. But that also made enum / qualified-constant identifiers appearing at end-of-line inside attribute argument lists — e.g. `AllowNumbers` in: [ JsonConverter( ConverterStrategy.AllowNumbers ) ] — be misclassified as no-arg `attribute` references, because the downstream `IsInsideCSharpAttributeRange` gate only checks whether the column falls inside the `[...]` span, not whether it is at the attribute-list top level. `BuildCSharpAttributeRanges` now additionally tracks paren-depth inside attribute sections and emits a parallel "top-level zone" table that splits each section by `(` / `)`. The no-arg attribute path gates on the top-level table; the existing with-args metadata classification still uses the full section ranges. Regression test `Extract_CsharpMultiLineAttributeArgumentEnum_NotClassifiedAsAttribute` pins that `ConverterStrategy` / `AllowNumbers` inside a multi-line attribute argument list do NOT surface as `attribute` references while `JsonConverter` still does. Co-Authored-By: Claude Opus 4 --- CHANGELOG.md | 4 +- src/CodeIndex/Indexer/ReferenceExtractor.cs | 91 +++++++++++++++++-- .../ReferenceExtractorTests.cs | 36 ++++++++ 3 files changed, 123 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82547b99b7..eebb13f10f 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/), - **Developer Guide now distinguishes SQLite's role from "zero dependencies" and labels release numbers as examples** — Clarified that cdidx remains a zero-configuration, single-file CLI with exactly one production dependency (`Microsoft.Data.Sqlite`), and made the release-workflow checklist explicitly state that `1.9.0` is only an example version to substitute during an actual release. Affected: `DEVELOPER_GUIDE.md`. #### Fixed -- **Multi-line no-arg C# attribute sections like `[\n Serializable\n]` are now indexed (#293 follow-up)** — The no-arg C# attribute regex previously required the match to start at a `[` / `,` boundary and end at `]` / `,` on the same line, so a bare identifier on the interior line of a multi-line attribute section (e.g. ` Serializable` between a line-leading `[` and a line-leading `]`) was silently dropped from `references --kind attribute`. The left anchor is now a word-boundary lookbehind and the right anchor also accepts end-of-line, while the downstream `IsInsideCSharpAttributeRange` gate still rejects indexer access like `arr[i]`. `SymbolExtractor` also now tracks multi-line `[...]` depth across lines via `StripMultiLineCSharpAttributeInterior` in `BuildCSharpMatchLines`, so interior lines of a multi-line attribute section are blanked out before declaration regexes run — this prevents ` Serializable` from being mis-extracted as a top-level `function` symbol (which then masked the reference through the `definitionNames` guard). Regression coverage adds `[\n Serializable\n]`, `[\n global::System.Obsolete\n]`, and `[Required,\n Key]` and runs through `SymbolExtractor.Extract` + `ReferenceExtractor.Extract` end-to-end so the SymbolExtractor side of the regression stays pinned. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Indexer/SymbolExtractor.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. +- **Multi-line no-arg C# attribute sections like `[\n Serializable\n]` are now indexed without over-matching attribute argument identifiers (#293 follow-up)** — The no-arg C# attribute regex previously required the match to start at a `[` / `,` boundary and end at `]` / `,` on the same line, so a bare identifier on the interior line of a multi-line attribute section (e.g. ` Serializable` between a line-leading `[` and a line-leading `]`) was silently dropped from `references --kind attribute`. The left anchor is now a word-boundary lookbehind and the right anchor also accepts end-of-line. To avoid misclassifying enum / qualified-constant identifiers that happen to sit at end-of-line inside an attribute argument list (e.g. `ConverterStrategy.AllowNumbers` in `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]`) as no-arg attributes, `BuildCSharpAttributeRanges` now also emits a parallel "top-level zone" table that splits each attribute section by `(` / `)` so only paren-depth-0 positions inside the section qualify as no-arg attribute name positions. The no-arg attribute path uses that top-level table instead of the full attribute-section ranges, while the existing with-args metadata classification still uses the full ranges. `SymbolExtractor` also now tracks multi-line `[...]` depth across lines via `StripMultiLineCSharpAttributeInterior` in `BuildCSharpMatchLines`, so interior lines of a multi-line attribute section are blanked out before declaration regexes run — this prevents ` Serializable` from being mis-extracted as a top-level `function` symbol (which then masked the reference through the `definitionNames` guard). Regression coverage adds `[\n Serializable\n]`, `[\n global::System.Obsolete\n]`, `[Required,\n Key]`, and `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]` (pinning that `AllowNumbers` / `ConverterStrategy` are NOT classified as `attribute`), running through `SymbolExtractor.Extract` + `ReferenceExtractor.Extract` end-to-end so both sides of the regression stay pinned. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Indexer/SymbolExtractor.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. - **`::`-qualified C# no-arg attributes and JavaScript `@Decorator` usages are now classified correctly (#293 follow-up)** — The no-arg C# attribute regex now accepts `::` alongside `.` as a qualifier separator, so `[global::System.Obsolete]` and `[Alias::MyAttr]` reach `references --kind attribute` instead of being silently dropped. `AnnotationLanguages` additionally includes `javascript`, so JS decorators such as `@sealed` (no-arg) and `@injectable()` (with args) are reclassified to `annotation` rather than leaking into the call-graph as phantom `call` rows — matching the existing TypeScript contract since JavaScript is already a graph-supported language. Added regression tests pinning `[global::System.Obsolete]`, `[Alias::MyAttr]`, and JavaScript `@sealed` / `@injectable()`. Also rewrote the README `callers` row (EN + JP) and the MCP tool descriptions for `callers` / `callees` to pull back the earlier over-promise that `kind: "attribute"` / `kind: "annotation"` on `callers` / `callees` is a supported metadata-inspection path: a metadata row is attributed to its enclosing body-range symbol (for a class-level declaration, that is the class itself) and drops entirely for file-level targets such as `[assembly: ...]` where `container_name` is `null`, so the docs now direct users to `references --kind attribute|annotation` for metadata enumeration. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`, `README.md`. - **`impact` BFS now follows C# event subscribe edges (#293 follow-up)** — `DbReader.GetCallersExact`, the only caller site used by the `impact` / `impact_analysis` BFS, now filters references through the shared `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` set instead of `InvokeReferenceKindsSql = ('call', 'instantiate')`. Previously `impact` quietly dropped `Changed += OnChanged` subscribe rows while `callers` / `callees` / `hotspots` kept them, so an event-driven transitive caller chain appeared shorter under `impact` than under `callers`. A new `DbReaderTests.GetTransitiveCallers_FollowsSubscribeEdges` regression pins the subscribe edge into the BFS output. Metadata edges (`attribute`, `annotation`) remain excluded. Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. - **C# parameter, type-parameter, lambda, and multi-line `[...]` attribute lists are now classified as `attribute`, no-arg attributes and annotations are indexed, and MCP tool descriptions document the default metadata filter (#293 follow-up)** — `ReferenceExtractor` now runs a pre-pass that tokenizes every C# `[...]` section across the whole file, so parameter attributes such as `void M([FromServices] IService s)` (where `[` is preceded by `(` or `,` rather than a declaration boundary), type-parameter attributes such as `class C<[Attr("x")] T>` (preceded by `<`), tuple-typed parameter attributes such as `void M([Attr("x")] (int a, int b) value)` (where the next token after `]` is `(`), lambda attributes such as `var f = [Attr("x")] () => 0;` (where `[` is preceded by `=`), and multi-line forms such as `[\n Foo("x")\n]` or `public void M(\n [Attr("x")]\n int a)` are recognized alongside the existing same-line cases. The pre-pass disambiguates the `(` / `,` / `<` / `=` entry point against C# 12 collection expressions in argument position (`Consume([Make()])`), pattern expressions (`[Make()] is int[] xs`), `as` casts (`[Make()] as int[]`), and switch expressions (`[Make()] switch { _ => true }`) by scanning forward from `[` to the matching `]` and checking whether the next meaningful token begins a declaration — identifiers are accepted unless they are the expression-continuation keywords `is` / `as` / `switch` / `with` / `when`, `@` is accepted as a verbatim-identifier lead, `(` is accepted for tuple-typed parameters and lambda parameter lists, and chained `[A][B]` recurses through the inner bracket. Bare no-arg C# attributes such as `[Serializable]`, `[Obsolete]`, `[System.Obsolete]`, `[assembly: CLSCompliant]`, and `[Required, Key]`, plus bare no-arg Java-family annotations such as `@Deprecated`, `@Override`, `@org.junit.Test`, and `@field:Deprecated`, are now emitted through dedicated no-arg regexes since the original `CallRegex` requires `(` — the C# path still gates on the attribute-range pre-pass so indexer access such as `arr[i]` stays unclassified, and the annotation path uses a lookbehind `(?`, tuple-typed parameter `void M([Attr("x")] (int a, int b) value)`, lambda `var f = [Attr("x")] () => 0;`, the no-arg C# attribute set above, the no-arg Java `@Deprecated` / `@Override` / `@org.junit.Test` set, Kotlin `@field:Deprecated` plus the `return@foo` negative, indexer access staying unclassified, `[\n Foo("x")\n]`, the cross-line parameter attribute shape, the targeted `[return: ...]` shape, the defense-in-depth collection-expression-in-argument case, and the three expression-continuation cases `[Make()] is ...`, `[Make()] as int[]`, and `[Make()] switch { ... }`. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. @@ -682,7 +682,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **DEVELOPER_GUIDE が SQLite の位置付けとリリース番号の例示を明確化** — cdidx が「設定ゼロ・単一ファイル・本番依存1個」の CLI であり、その唯一の本番依存が `Microsoft.Data.Sqlite` であることを明記した。あわせて、リリース手順のチェックリスト中に出てくる `1.9.0` は固定値ではなく例示であり、実際のリリース番号へ読み替える前提だと明示した。対象: `DEVELOPER_GUIDE.md`。 #### 修正 -- **`[\n Serializable\n]` のような複数行にまたがる引数なし C# 属性セクションを索引化 (#293 追加対応)** — 引数なし C# 属性の正規表現は、これまで `[` / `,` を左端、`]` / `,` を右端として同一行内でアンカーしていたため、行頭 `[` と行頭 `]` の間に書かれた ` Serializable` のような裸識別子が `references --kind attribute` から黙って脱落していた。左側は単語境界 lookbehind、右側は行末(`$`)も許容するように緩和し、誤検出を防ぐため引き続き `IsInsideCSharpAttributeRange` の属性レンジ内かどうかでゲートする(`arr[i]` のような indexer アクセスは弾く)。`SymbolExtractor` 側でも `BuildCSharpMatchLines` に `StripMultiLineCSharpAttributeInterior` を追加し、複数行にまたがる `[...]` セクションの深さを跨行で追跡して内部行を宣言 regex 適用前に空白化するようにした。これにより ` Serializable` がトップレベル `function` シンボルとして誤抽出され、`definitionNames` ガード経由で参照分類が詰まってしまうのを防ぐ。`[\n Serializable\n]`・`[\n global::System.Obsolete\n]`・`[Required,\n Key]` を `SymbolExtractor.Extract` + `ReferenceExtractor.Extract` で end-to-end に回して SymbolExtractor 側の回帰も固定する回帰テストを追加。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Indexer/SymbolExtractor.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 +- **`[\n Serializable\n]` のような複数行にまたがる引数なし C# 属性セクションを、属性引数内の識別子を誤って取り込まずに索引化 (#293 追加対応)** — 引数なし C# 属性の正規表現は、これまで `[` / `,` を左端、`]` / `,` を右端として同一行内でアンカーしていたため、行頭 `[` と行頭 `]` の間に書かれた ` Serializable` のような裸識別子が `references --kind attribute` から黙って脱落していた。左側は単語境界 lookbehind、右側は行末(`$`)も許容するように緩和した。一方で `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]` のように属性の引数リスト内で行末に現れる enum / 修飾定数識別子(`AllowNumbers` など)が no-arg 属性として誤分類される回帰を避けるため、`BuildCSharpAttributeRanges` は `(` / `)` で属性セクションを分割した paren 深さ 0 の「top-level ゾーン」テーブルも並列で出力するようにし、no-arg 属性の採用位置はそのテーブルに限定した(既存の引数付き metadata 分類パスは引き続き属性セクション全体のレンジを使う)。`SymbolExtractor` 側でも `BuildCSharpMatchLines` に `StripMultiLineCSharpAttributeInterior` を追加し、複数行にまたがる `[...]` セクションの深さを跨行で追跡して内部行を宣言 regex 適用前に空白化するようにした。これにより ` Serializable` がトップレベル `function` シンボルとして誤抽出され、`definitionNames` ガード経由で参照分類が詰まってしまうのを防ぐ。`[\n Serializable\n]`・`[\n global::System.Obsolete\n]`・`[Required,\n Key]`・`[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]`(`AllowNumbers` / `ConverterStrategy` が `attribute` として取り込まれないことを固定)を `SymbolExtractor.Extract` + `ReferenceExtractor.Extract` で end-to-end に回して SymbolExtractor 側の回帰も固定する回帰テストを追加。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Indexer/SymbolExtractor.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 - **`::` 修飾の C# 引数なし属性と JavaScript の `@Decorator` を正しく分類 (#293 追加対応)** — 引数なし C# 属性の正規表現が修飾子セパレータとして `.` に加えて `::` も受け付けるようになり、`[global::System.Obsolete]` や `[Alias::MyAttr]` のような形が `references --kind attribute` から黙って脱落しなくなった。`AnnotationLanguages` にも `javascript` を追加し、JavaScript はもともと graph 対応言語なので、`@sealed`(引数なし)や `@injectable()`(引数付き)のような JS decorator も `annotation` として再分類され、TypeScript と同じ契約で call-graph に phantom `call` 行を残さない。`[global::System.Obsolete]`・`[Alias::MyAttr]`・JavaScript の `@sealed` / `@injectable()` を押さえる回帰テストを追加。あわせて、以前に進めていた `callers` / `callees` の metadata 契約を README(英日)と MCP ツール説明の両方で引き戻し、metadata 行は注釈対象そのものではなく body-range 上の外側シンボル(クラス直下宣言ならクラス、`[assembly: ...]` のようなファイルレベル target では `container_name` が `null`)に帰属するため `kind: "attribute"` / `kind: "annotation"` を `callers` / `callees` に渡すのは信頼できる metadata 調査経路ではなく、metadata 列挙は `references --kind attribute|annotation` を使うよう案内する文言に修正した。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Mcp/McpToolDefinitions.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`、`README.md`。 - **`impact` BFS が C# の event subscribe edge を辿るよう修正 (#293 追加対応)** — `impact` / `impact_analysis` BFS の唯一の呼び出し箇所である `DbReader.GetCallersExact` が、共有 filter `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` で参照を絞り込むようになった(以前は `InvokeReferenceKindsSql = ('call', 'instantiate')` を使っていたため、`Changed += OnChanged` のような subscribe 行を静かに捨てていた)。これにより `callers` / `callees` / `hotspots` と同じ call-graph 契約を `impact` も共有し、イベント駆動の transitive caller chain が `impact` でだけ短くなる不整合が解消する。`DbReaderTests.GetTransitiveCallers_FollowsSubscribeEdges` の回帰テストで subscribe edge が BFS 出力に含まれることを固定。Metadata edge (`attribute` / `annotation`) の除外は従来通り。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 - **C# のパラメータ属性・型パラメータ属性・ラムダ属性・複数行 `[...]` 属性を `attribute` として分類し、引数なし属性とアノテーションも索引化、MCP ツール説明に既定の metadata フィルタを反映 (#293 追加対応)** — `ReferenceExtractor` がファイル全体の C# `[...]` セクションを事前パスでトークン化するようになり、`void M([FromServices] IService s)` のように `[` の直前が宣言境界ではなく `(` / `,` で始まるパラメータ属性、`class C<[Attr("x")] T>` のように `<` で始まる型パラメータ属性、`void M([Attr("x")] (int a, int b) value)` のように `]` の直後が `(` のまま続く tuple 型パラメータ属性、`var f = [Attr("x")] () => 0;` のように `=` の直後にあるラムダ属性、さらに `[\n Foo("x")\n]` や `public void M(\n [Attr("x")]\n int a)` のような複数行形式も、既存の同一行ケースと同じように属性として扱われるようになった。`(` / `,` / `<` / `=` 直後の曖昧さは、`[` から対応する `]` まで前方走査し、`]` の直後の非空白トークンが宣言を開始するかで判定する — 識別子は原則受理するが、式継続キーワード `is` / `as` / `switch` / `with` / `when` は宣言扱いしないので `Consume([Make()] is int[] xs)` / `([Make()] as int[])` / `Consume([Make()] switch { _ => true })` のような collection expression は `call` のまま残り、`@` は verbatim 識別子の先頭として、`(` は tuple 型パラメータとラムダ仮引数リストの先頭として受理し、`[A][B]` のような連続 bracket は内側 `[` に再帰して判定する。さらに `[Serializable]`、`[Obsolete]`、`[System.Obsolete]`、`[assembly: CLSCompliant]`、`[Required, Key]` のような引数なし C# 属性と、`@Deprecated`・`@Override`・`@org.junit.Test`・`@field:Deprecated` のような引数なし Java 系アノテーションは、`(` を要求する既存の `CallRegex` では拾えないため専用の no-arg 正規表現から emit するようになった。C# 側では引き続き `[...]` 事前パスのレンジに含まれるかで gate するので `arr[i]` のような indexer アクセスは属性化されず、アノテーション側は lookbehind `(?` 型パラメータ属性、`void M([Attr("x")] (int a, int b) value)` tuple 型パラメータ属性、`var f = [Attr("x")] () => 0;` ラムダ属性、上記 no-arg C# 属性群、Java `@Deprecated` / `@Override` / `@org.junit.Test` の no-arg アノテーション群、Kotlin `@field:Deprecated` と `return@foo` の negative ケース、indexer アクセスが属性化されないこと、`[\n Foo("x")\n]`、改行を挟んだパラメータ属性、`[return: NotNullWhen(true)]`、防御的に `Consume([Make()])` の collection expression-in-argument、および `[Make()] is ...` / `[Make()] as int[]` / `[Make()] switch { ... }` の 3 つの式継続ケースを押さえる回帰テストを追加。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Mcp/McpToolDefinitions.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 diff --git a/src/CodeIndex/Indexer/ReferenceExtractor.cs b/src/CodeIndex/Indexer/ReferenceExtractor.cs index 05b451ae51..b1202675ac 100644 --- a/src/CodeIndex/Indexer/ReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/ReferenceExtractor.cs @@ -220,9 +220,18 @@ public static List Extract(long fileId, string? lang, string co // attributes `void M([Attr] T x)` are classified consistently with same-line `[Foo]`. // 行を跨いだ `[\n Foo("x")\n]` やパラメータ属性 `void M([Attr] T x)` も、同一行の `[Foo]` と // 同じ判定で属性として扱えるように、事前パスで C# 属性セクションの範囲を構築する。 - var csharpAttrRanges = language == "csharp" + var csharpAttrTables = language == "csharp" ? BuildCSharpAttributeRanges(preparedLines) - : null; + : (null, null); + var csharpAttrRanges = csharpAttrTables.Item1; + // Top-level (paren-depth 0) zones inside attribute sections. Used by the no-arg + // attribute regex so that enum / qualified-constant identifiers appearing inside + // attribute argument lists (e.g. `AllowNumbers` in `[JsonConverter(ConverterStrategy.AllowNumbers)]`) + // are not misclassified as no-arg attribute references. + // 属性セクション内で paren 深さ 0 の top-level ゾーンだけを別テーブルで持つ。複数行 + // `[...]` の引数中に現れる enum / 修飾定数(`ConverterStrategy.AllowNumbers` など)が + // no-arg attribute として誤分類されないよう、no-arg 属性用ゲートに使う。 + var csharpAttrTopLevelRanges = csharpAttrTables.Item2; var definitionNamesByLine = symbols .GroupBy(symbol => symbol.Line) .ToDictionary(group => group.Key, group => group.Select(symbol => symbol.Name).ToHashSet(StringComparer.Ordinal)); @@ -249,6 +258,7 @@ public static List Extract(long fileId, string? lang, string co if (string.IsNullOrWhiteSpace(preparedLine)) continue; var csharpAttrRangesOnLine = csharpAttrRanges?[i]; + var csharpAttrTopLevelOnLine = csharpAttrTopLevelRanges?[i]; var context = originalLine.Trim(); if (context.Length == 0) @@ -295,13 +305,20 @@ public static List Extract(long fileId, string? lang, string co // and their siblings still populate the reference table. // issue #293: 引数なしの属性・アノテーションは `(` が必須な CallRegex では拾えないため、 // 専用 regex から `[Serializable]` / `@Deprecated` などの素形を reference テーブルへ反映する。 - if (language == "csharp" && csharpAttrRangesOnLine != null && csharpAttrRangesOnLine.Count > 0) + if (language == "csharp" && csharpAttrTopLevelOnLine != null && csharpAttrTopLevelOnLine.Count > 0) { foreach (Match match in CSharpNoArgAttributeRegex.Matches(preparedLine)) { var name = match.Groups["name"].Value; var nameIndex = match.Groups["name"].Index; - if (!IsInsideCSharpAttributeRange(csharpAttrRangesOnLine, nameIndex)) + // Gate on the attribute-section top-level (paren-depth 0) zones only, so + // identifiers that sit inside an attribute's argument list (e.g. + // `ConverterStrategy.AllowNumbers` in `[JsonConverter(...)]`) are not + // misclassified as no-arg attributes. + // 属性セクションの top-level(paren 深さ 0)ゾーンでのみ採用する。属性の + // 引数リスト内にある識別子(`[JsonConverter(ConverterStrategy.AllowNumbers)]` + // の `AllowNumbers` など)を no-arg 属性として誤分類しないため。 + if (!IsInsideCSharpAttributeRange(csharpAttrTopLevelOnLine, nameIndex)) continue; if (IsIgnoredCallName(language, name)) continue; @@ -520,11 +537,15 @@ private static bool IsIdentifierChar(char ch) => /// `(開始列, 終端列 (exclusive))` のレンジを保持し、呼び出し名の列がどれかのレンジに含まれる場合に /// `call` ではなく `attribute` へ再分類する。 /// - private static List> BuildCSharpAttributeRanges(string[] preparedLines) + private static (List>, List>) BuildCSharpAttributeRanges(string[] preparedLines) { var perLine = new List>(preparedLines.Length); + var perLineTopLevel = new List>(preparedLines.Length); for (var i = 0; i < preparedLines.Length; i++) + { perLine.Add(new List<(int, int)>()); + perLineTopLevel.Add(new List<(int, int)>()); + } // Stack entries capture the opening `[` position and whether that bracket was at // a C# declaration (attribute) position. Parens inside a section are tracked so the @@ -536,6 +557,28 @@ private static bool IsIdentifierChar(char ch) => int parenDepth = 0; bool lastClosedBracketWasAttribute = false; + // Top-level zone tracking: while we are inside an attribute section and parenDepth==0, + // the current zone span is open. When parens open we close it; when they fully close + // again we reopen. When the attribute section itself closes, we emit the final span. + // top-level ゾーン追跡: 属性セクション内かつ paren 深さ 0 のあいだだけゾーンを開いておき、 + // `(` で閉じ、`)` で再び開く。セクションが閉じる `]` で確定させる。 + int topZoneStartLi = -1; + int topZoneStartCi = 0; + + void EmitTopZone(int endLi, int endCi) + { + if (topZoneStartLi < 0) + return; + for (var l = topZoneStartLi; l <= endLi; l++) + { + int s = (l == topZoneStartLi) ? topZoneStartCi : 0; + int e = (l == endLi) ? endCi : preparedLines[l].Length; + if (e > s) + perLineTopLevel[l].Add((s, e)); + } + topZoneStartLi = -1; + } + for (var li = 0; li < preparedLines.Length; li++) { var line = preparedLines[li]; @@ -547,6 +590,13 @@ private static bool IsIdentifierChar(char ch) => if (c == '(') { + // If we are in a top-level attribute zone and this `(` opens the first + // paren depth, close the top-level zone just before the `(`. + // top-level ゾーン中で paren 深さが 0→1 になる `(` ならここでゾーンを閉じる。 + if (parenDepth == 0 && bracketStack.Count > 0 && bracketStack.Peek().isAttr && topZoneStartLi >= 0) + { + EmitTopZone(li, ci); + } parenDepth++; lastMeaningful = c; continue; @@ -554,7 +604,15 @@ private static bool IsIdentifierChar(char ch) => if (c == ')') { if (parenDepth > 0) + { parenDepth--; + // paren 深さが 1→0 に戻ったら、依然として属性セクション内なら top-level ゾーンを再開する。 + if (parenDepth == 0 && bracketStack.Count > 0 && bracketStack.Peek().isAttr && topZoneStartLi < 0) + { + topZoneStartLi = li; + topZoneStartCi = ci + 1; + } + } lastMeaningful = c; continue; } @@ -564,6 +622,14 @@ private static bool IsIdentifierChar(char ch) => bool isAttr = EvaluateCSharpAttributePosition( lastMeaningful, lastClosedBracketWasAttribute, preparedLines, li, ci); bracketStack.Push((li, ci, isAttr)); + if (isAttr && parenDepth == 0 && topZoneStartLi < 0) + { + // Start top-level zone just after the `[` so the `[` itself is not + // inside the zone. + // `[` 直後から top-level ゾーンを開始する。 + topZoneStartLi = li; + topZoneStartCi = ci + 1; + } lastMeaningful = c; continue; } @@ -586,6 +652,19 @@ private static bool IsIdentifierChar(char ch) => int e = (l == li) ? ci + 1 : preparedLines[l].Length; perLine[l].Add((s, e)); } + // Close the top-level zone at the `]` (paren depth should already + // be 0 here — if it is not, we simply drop the open zone because + // paren balancing was malformed). + // ここで top-level ゾーンを確定する。paren が閉じ切っていない場合は + // 不整合入力なのでゾーンを捨てる。 + if (parenDepth == 0) + { + EmitTopZone(li, ci + 1); + } + else + { + topZoneStartLi = -1; + } } } else @@ -600,7 +679,7 @@ private static bool IsIdentifierChar(char ch) => } } - return perLine; + return (perLine, perLineTopLevel); } /// diff --git a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs index e0d99d8211..56e151e929 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs @@ -1859,6 +1859,42 @@ public class C Assert.DoesNotContain(references, r => r.ReferenceKind == "call"); } + [Fact] + public void Extract_CsharpMultiLineAttributeArgumentEnum_NotClassifiedAsAttribute() + { + // Regression (issue #293 follow-up): identifiers appearing inside the argument list of + // a multi-line attribute such as `ConverterStrategy.AllowNumbers` must NOT be recorded + // as `attribute` references. Only the attribute-list top level (`[`/`,` boundary, paren + // depth 0) is a valid no-arg attribute name position. + // リグレッション (issue #293 補足): 複数行属性の引数リスト内にある識別子 + // (例: `ConverterStrategy.AllowNumbers`) は `attribute` として記録してはならない。 + // 属性リストの top-level (paren 深さ 0 の `[` / `,` 境界) のみが no-arg 属性名の位置。 + const string content = """ + [ + JsonConverter( + ConverterStrategy.AllowNumbers + ) + ] + public class A + { + } + """; + + var symbols = SymbolExtractor.Extract(1, "csharp", content); + var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); + + // JsonConverter is the only attribute here (with-args, classified by the metadata path). + var jsonConverter = Assert.Single(references.Where(r => r.SymbolName == "JsonConverter")); + Assert.Equal("attribute", jsonConverter.ReferenceKind); + + // AllowNumbers is an enum member access inside the attribute arguments — it must not be + // picked up as a no-arg attribute even though it happens to end at end-of-line inside + // the `[...]` section. + // AllowNumbers は属性引数内の enum メンバーアクセスなので、no-arg 属性として取り込まれないこと。 + Assert.DoesNotContain(references, r => r.SymbolName == "AllowNumbers" && r.ReferenceKind == "attribute"); + Assert.DoesNotContain(references, r => r.SymbolName == "ConverterStrategy" && r.ReferenceKind == "attribute"); + } + [Fact] public void Extract_CsharpAliasQualifiedNoArgAttribute_ClassifiedAsAttribute() { From eddb25592a1c182172045080498e98bfe5e93166 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 18 Apr 2026 04:26:15 +0900 Subject: [PATCH 10/30] Fix #293 track attribute-section paren depth locally for no-arg gating The previous commit introduced top-level-zone gating that used a single global `parenDepth` counter. That broke no-arg parameter / delegate / lambda parameter attributes such as `void M([FromServices] IService s)`, `delegate void D([Attr] int x)`, and `([Attr] int x) => x`: their `[` opens while the global paren depth is already > 0 (inside the outer parameter list), so the `parenDepth == 0` check for entering a top-level zone never fired and the no-arg match was silently dropped. Each `[` now snapshots the current global paren depth onto its bracket stack entry (`parenDepthAtOpen`). The top-level zone logic compares against the innermost open attribute section's snapshot instead of against absolute depth 0: - `[` pushes a section whose zone opens immediately (section-local 0). - `(` closes the zone only when the current depth matches the section's `parenDepthAtOpen` (i.e. we were at the section's local top level). - `)` reopens the zone when depth returns to the section's snapshot. - `]` commits the zone when depth matches the section's snapshot. This preserves the multi-line attribute-argument fix (`AllowNumbers` inside `[JsonConverter(...)]` stays unclassified) while restoring no-arg attribute classification in all outer-parenthesized contexts. Regression tests: - `Extract_CsharpNoArgParameterAttribute_ClassifiedAsAttribute` pins the parameter attribute case that Codex flagged. - `Extract_CsharpNoArgDelegateAndLambdaParameterAttributes_ClassifiedAsAttribute` pins delegate and lambda parameter cases. Co-Authored-By: Claude Opus 4 --- CHANGELOG.md | 4 +- src/CodeIndex/Indexer/ReferenceExtractor.cs | 85 ++++++++++++------- .../ReferenceExtractorTests.cs | 55 ++++++++++++ 3 files changed, 112 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eebb13f10f..b97b537b1d 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/), - **Developer Guide now distinguishes SQLite's role from "zero dependencies" and labels release numbers as examples** — Clarified that cdidx remains a zero-configuration, single-file CLI with exactly one production dependency (`Microsoft.Data.Sqlite`), and made the release-workflow checklist explicitly state that `1.9.0` is only an example version to substitute during an actual release. Affected: `DEVELOPER_GUIDE.md`. #### Fixed -- **Multi-line no-arg C# attribute sections like `[\n Serializable\n]` are now indexed without over-matching attribute argument identifiers (#293 follow-up)** — The no-arg C# attribute regex previously required the match to start at a `[` / `,` boundary and end at `]` / `,` on the same line, so a bare identifier on the interior line of a multi-line attribute section (e.g. ` Serializable` between a line-leading `[` and a line-leading `]`) was silently dropped from `references --kind attribute`. The left anchor is now a word-boundary lookbehind and the right anchor also accepts end-of-line. To avoid misclassifying enum / qualified-constant identifiers that happen to sit at end-of-line inside an attribute argument list (e.g. `ConverterStrategy.AllowNumbers` in `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]`) as no-arg attributes, `BuildCSharpAttributeRanges` now also emits a parallel "top-level zone" table that splits each attribute section by `(` / `)` so only paren-depth-0 positions inside the section qualify as no-arg attribute name positions. The no-arg attribute path uses that top-level table instead of the full attribute-section ranges, while the existing with-args metadata classification still uses the full ranges. `SymbolExtractor` also now tracks multi-line `[...]` depth across lines via `StripMultiLineCSharpAttributeInterior` in `BuildCSharpMatchLines`, so interior lines of a multi-line attribute section are blanked out before declaration regexes run — this prevents ` Serializable` from being mis-extracted as a top-level `function` symbol (which then masked the reference through the `definitionNames` guard). Regression coverage adds `[\n Serializable\n]`, `[\n global::System.Obsolete\n]`, `[Required,\n Key]`, and `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]` (pinning that `AllowNumbers` / `ConverterStrategy` are NOT classified as `attribute`), running through `SymbolExtractor.Extract` + `ReferenceExtractor.Extract` end-to-end so both sides of the regression stay pinned. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Indexer/SymbolExtractor.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. +- **Multi-line no-arg C# attribute sections like `[\n Serializable\n]` are now indexed without over-matching attribute argument identifiers (#293 follow-up)** — The no-arg C# attribute regex previously required the match to start at a `[` / `,` boundary and end at `]` / `,` on the same line, so a bare identifier on the interior line of a multi-line attribute section (e.g. ` Serializable` between a line-leading `[` and a line-leading `]`) was silently dropped from `references --kind attribute`. The left anchor is now a word-boundary lookbehind and the right anchor also accepts end-of-line. To avoid misclassifying enum / qualified-constant identifiers that happen to sit at end-of-line inside an attribute argument list (e.g. `ConverterStrategy.AllowNumbers` in `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]`) as no-arg attributes, `BuildCSharpAttributeRanges` now also emits a parallel "top-level zone" table that splits each attribute section by `(` / `)` so only paren-depth-0 positions inside the section qualify as no-arg attribute name positions. Paren depth is tracked section-locally (each `[` snapshots the global depth at open time) so parameter attributes like `void M([FromServices] IService s)`, delegate parameter attributes like `delegate void D([Attr] int x)`, and lambda parameter attributes like `Func f = ([Attr] int x) => x` — whose `[` opens while the global paren depth is already > 0 — still have their attribute-list top level correctly identified. The no-arg attribute path uses that top-level table instead of the full attribute-section ranges, while the existing with-args metadata classification still uses the full ranges. `SymbolExtractor` also now tracks multi-line `[...]` depth across lines via `StripMultiLineCSharpAttributeInterior` in `BuildCSharpMatchLines`, so interior lines of a multi-line attribute section are blanked out before declaration regexes run — this prevents ` Serializable` from being mis-extracted as a top-level `function` symbol (which then masked the reference through the `definitionNames` guard). Regression coverage adds `[\n Serializable\n]`, `[\n global::System.Obsolete\n]`, `[Required,\n Key]`, and `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]` (pinning that `AllowNumbers` / `ConverterStrategy` are NOT classified as `attribute`), running through `SymbolExtractor.Extract` + `ReferenceExtractor.Extract` end-to-end so both sides of the regression stay pinned. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Indexer/SymbolExtractor.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. - **`::`-qualified C# no-arg attributes and JavaScript `@Decorator` usages are now classified correctly (#293 follow-up)** — The no-arg C# attribute regex now accepts `::` alongside `.` as a qualifier separator, so `[global::System.Obsolete]` and `[Alias::MyAttr]` reach `references --kind attribute` instead of being silently dropped. `AnnotationLanguages` additionally includes `javascript`, so JS decorators such as `@sealed` (no-arg) and `@injectable()` (with args) are reclassified to `annotation` rather than leaking into the call-graph as phantom `call` rows — matching the existing TypeScript contract since JavaScript is already a graph-supported language. Added regression tests pinning `[global::System.Obsolete]`, `[Alias::MyAttr]`, and JavaScript `@sealed` / `@injectable()`. Also rewrote the README `callers` row (EN + JP) and the MCP tool descriptions for `callers` / `callees` to pull back the earlier over-promise that `kind: "attribute"` / `kind: "annotation"` on `callers` / `callees` is a supported metadata-inspection path: a metadata row is attributed to its enclosing body-range symbol (for a class-level declaration, that is the class itself) and drops entirely for file-level targets such as `[assembly: ...]` where `container_name` is `null`, so the docs now direct users to `references --kind attribute|annotation` for metadata enumeration. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`, `README.md`. - **`impact` BFS now follows C# event subscribe edges (#293 follow-up)** — `DbReader.GetCallersExact`, the only caller site used by the `impact` / `impact_analysis` BFS, now filters references through the shared `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` set instead of `InvokeReferenceKindsSql = ('call', 'instantiate')`. Previously `impact` quietly dropped `Changed += OnChanged` subscribe rows while `callers` / `callees` / `hotspots` kept them, so an event-driven transitive caller chain appeared shorter under `impact` than under `callers`. A new `DbReaderTests.GetTransitiveCallers_FollowsSubscribeEdges` regression pins the subscribe edge into the BFS output. Metadata edges (`attribute`, `annotation`) remain excluded. Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. - **C# parameter, type-parameter, lambda, and multi-line `[...]` attribute lists are now classified as `attribute`, no-arg attributes and annotations are indexed, and MCP tool descriptions document the default metadata filter (#293 follow-up)** — `ReferenceExtractor` now runs a pre-pass that tokenizes every C# `[...]` section across the whole file, so parameter attributes such as `void M([FromServices] IService s)` (where `[` is preceded by `(` or `,` rather than a declaration boundary), type-parameter attributes such as `class C<[Attr("x")] T>` (preceded by `<`), tuple-typed parameter attributes such as `void M([Attr("x")] (int a, int b) value)` (where the next token after `]` is `(`), lambda attributes such as `var f = [Attr("x")] () => 0;` (where `[` is preceded by `=`), and multi-line forms such as `[\n Foo("x")\n]` or `public void M(\n [Attr("x")]\n int a)` are recognized alongside the existing same-line cases. The pre-pass disambiguates the `(` / `,` / `<` / `=` entry point against C# 12 collection expressions in argument position (`Consume([Make()])`), pattern expressions (`[Make()] is int[] xs`), `as` casts (`[Make()] as int[]`), and switch expressions (`[Make()] switch { _ => true }`) by scanning forward from `[` to the matching `]` and checking whether the next meaningful token begins a declaration — identifiers are accepted unless they are the expression-continuation keywords `is` / `as` / `switch` / `with` / `when`, `@` is accepted as a verbatim-identifier lead, `(` is accepted for tuple-typed parameters and lambda parameter lists, and chained `[A][B]` recurses through the inner bracket. Bare no-arg C# attributes such as `[Serializable]`, `[Obsolete]`, `[System.Obsolete]`, `[assembly: CLSCompliant]`, and `[Required, Key]`, plus bare no-arg Java-family annotations such as `@Deprecated`, `@Override`, `@org.junit.Test`, and `@field:Deprecated`, are now emitted through dedicated no-arg regexes since the original `CallRegex` requires `(` — the C# path still gates on the attribute-range pre-pass so indexer access such as `arr[i]` stays unclassified, and the annotation path uses a lookbehind `(?`, tuple-typed parameter `void M([Attr("x")] (int a, int b) value)`, lambda `var f = [Attr("x")] () => 0;`, the no-arg C# attribute set above, the no-arg Java `@Deprecated` / `@Override` / `@org.junit.Test` set, Kotlin `@field:Deprecated` plus the `return@foo` negative, indexer access staying unclassified, `[\n Foo("x")\n]`, the cross-line parameter attribute shape, the targeted `[return: ...]` shape, the defense-in-depth collection-expression-in-argument case, and the three expression-continuation cases `[Make()] is ...`, `[Make()] as int[]`, and `[Make()] switch { ... }`. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. @@ -682,7 +682,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **DEVELOPER_GUIDE が SQLite の位置付けとリリース番号の例示を明確化** — cdidx が「設定ゼロ・単一ファイル・本番依存1個」の CLI であり、その唯一の本番依存が `Microsoft.Data.Sqlite` であることを明記した。あわせて、リリース手順のチェックリスト中に出てくる `1.9.0` は固定値ではなく例示であり、実際のリリース番号へ読み替える前提だと明示した。対象: `DEVELOPER_GUIDE.md`。 #### 修正 -- **`[\n Serializable\n]` のような複数行にまたがる引数なし C# 属性セクションを、属性引数内の識別子を誤って取り込まずに索引化 (#293 追加対応)** — 引数なし C# 属性の正規表現は、これまで `[` / `,` を左端、`]` / `,` を右端として同一行内でアンカーしていたため、行頭 `[` と行頭 `]` の間に書かれた ` Serializable` のような裸識別子が `references --kind attribute` から黙って脱落していた。左側は単語境界 lookbehind、右側は行末(`$`)も許容するように緩和した。一方で `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]` のように属性の引数リスト内で行末に現れる enum / 修飾定数識別子(`AllowNumbers` など)が no-arg 属性として誤分類される回帰を避けるため、`BuildCSharpAttributeRanges` は `(` / `)` で属性セクションを分割した paren 深さ 0 の「top-level ゾーン」テーブルも並列で出力するようにし、no-arg 属性の採用位置はそのテーブルに限定した(既存の引数付き metadata 分類パスは引き続き属性セクション全体のレンジを使う)。`SymbolExtractor` 側でも `BuildCSharpMatchLines` に `StripMultiLineCSharpAttributeInterior` を追加し、複数行にまたがる `[...]` セクションの深さを跨行で追跡して内部行を宣言 regex 適用前に空白化するようにした。これにより ` Serializable` がトップレベル `function` シンボルとして誤抽出され、`definitionNames` ガード経由で参照分類が詰まってしまうのを防ぐ。`[\n Serializable\n]`・`[\n global::System.Obsolete\n]`・`[Required,\n Key]`・`[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]`(`AllowNumbers` / `ConverterStrategy` が `attribute` として取り込まれないことを固定)を `SymbolExtractor.Extract` + `ReferenceExtractor.Extract` で end-to-end に回して SymbolExtractor 側の回帰も固定する回帰テストを追加。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Indexer/SymbolExtractor.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 +- **`[\n Serializable\n]` のような複数行にまたがる引数なし C# 属性セクションを、属性引数内の識別子を誤って取り込まずに索引化 (#293 追加対応)** — 引数なし C# 属性の正規表現は、これまで `[` / `,` を左端、`]` / `,` を右端として同一行内でアンカーしていたため、行頭 `[` と行頭 `]` の間に書かれた ` Serializable` のような裸識別子が `references --kind attribute` から黙って脱落していた。左側は単語境界 lookbehind、右側は行末(`$`)も許容するように緩和した。一方で `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]` のように属性の引数リスト内で行末に現れる enum / 修飾定数識別子(`AllowNumbers` など)が no-arg 属性として誤分類される回帰を避けるため、`BuildCSharpAttributeRanges` は `(` / `)` で属性セクションを分割した paren 深さ 0 の「top-level ゾーン」テーブルも並列で出力するようにし、no-arg 属性の採用位置はそのテーブルに限定した(既存の引数付き metadata 分類パスは引き続き属性セクション全体のレンジを使う)。paren 深さは section-local に追跡する(各 `[` がその瞬間のグローバル paren 深さを snapshot として保持)ため、`void M([FromServices] IService s)` のようなパラメータ属性、`delegate void D([Attr] int x)` のようなデリゲートパラメータ属性、`Func f = ([Attr] int x) => x` のようなラムダパラメータ属性のように、`[` が開いた時点で既にグローバル paren 深さが 0 より大きいケースでも、属性リストの top-level を正しく認識できる。`SymbolExtractor` 側でも `BuildCSharpMatchLines` に `StripMultiLineCSharpAttributeInterior` を追加し、複数行にまたがる `[...]` セクションの深さを跨行で追跡して内部行を宣言 regex 適用前に空白化するようにした。これにより ` Serializable` がトップレベル `function` シンボルとして誤抽出され、`definitionNames` ガード経由で参照分類が詰まってしまうのを防ぐ。`[\n Serializable\n]`・`[\n global::System.Obsolete\n]`・`[Required,\n Key]`・`[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]`(`AllowNumbers` / `ConverterStrategy` が `attribute` として取り込まれないことを固定)を `SymbolExtractor.Extract` + `ReferenceExtractor.Extract` で end-to-end に回して SymbolExtractor 側の回帰も固定する回帰テストを追加。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Indexer/SymbolExtractor.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 - **`::` 修飾の C# 引数なし属性と JavaScript の `@Decorator` を正しく分類 (#293 追加対応)** — 引数なし C# 属性の正規表現が修飾子セパレータとして `.` に加えて `::` も受け付けるようになり、`[global::System.Obsolete]` や `[Alias::MyAttr]` のような形が `references --kind attribute` から黙って脱落しなくなった。`AnnotationLanguages` にも `javascript` を追加し、JavaScript はもともと graph 対応言語なので、`@sealed`(引数なし)や `@injectable()`(引数付き)のような JS decorator も `annotation` として再分類され、TypeScript と同じ契約で call-graph に phantom `call` 行を残さない。`[global::System.Obsolete]`・`[Alias::MyAttr]`・JavaScript の `@sealed` / `@injectable()` を押さえる回帰テストを追加。あわせて、以前に進めていた `callers` / `callees` の metadata 契約を README(英日)と MCP ツール説明の両方で引き戻し、metadata 行は注釈対象そのものではなく body-range 上の外側シンボル(クラス直下宣言ならクラス、`[assembly: ...]` のようなファイルレベル target では `container_name` が `null`)に帰属するため `kind: "attribute"` / `kind: "annotation"` を `callers` / `callees` に渡すのは信頼できる metadata 調査経路ではなく、metadata 列挙は `references --kind attribute|annotation` を使うよう案内する文言に修正した。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Mcp/McpToolDefinitions.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`、`README.md`。 - **`impact` BFS が C# の event subscribe edge を辿るよう修正 (#293 追加対応)** — `impact` / `impact_analysis` BFS の唯一の呼び出し箇所である `DbReader.GetCallersExact` が、共有 filter `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` で参照を絞り込むようになった(以前は `InvokeReferenceKindsSql = ('call', 'instantiate')` を使っていたため、`Changed += OnChanged` のような subscribe 行を静かに捨てていた)。これにより `callers` / `callees` / `hotspots` と同じ call-graph 契約を `impact` も共有し、イベント駆動の transitive caller chain が `impact` でだけ短くなる不整合が解消する。`DbReaderTests.GetTransitiveCallers_FollowsSubscribeEdges` の回帰テストで subscribe edge が BFS 出力に含まれることを固定。Metadata edge (`attribute` / `annotation`) の除外は従来通り。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 - **C# のパラメータ属性・型パラメータ属性・ラムダ属性・複数行 `[...]` 属性を `attribute` として分類し、引数なし属性とアノテーションも索引化、MCP ツール説明に既定の metadata フィルタを反映 (#293 追加対応)** — `ReferenceExtractor` がファイル全体の C# `[...]` セクションを事前パスでトークン化するようになり、`void M([FromServices] IService s)` のように `[` の直前が宣言境界ではなく `(` / `,` で始まるパラメータ属性、`class C<[Attr("x")] T>` のように `<` で始まる型パラメータ属性、`void M([Attr("x")] (int a, int b) value)` のように `]` の直後が `(` のまま続く tuple 型パラメータ属性、`var f = [Attr("x")] () => 0;` のように `=` の直後にあるラムダ属性、さらに `[\n Foo("x")\n]` や `public void M(\n [Attr("x")]\n int a)` のような複数行形式も、既存の同一行ケースと同じように属性として扱われるようになった。`(` / `,` / `<` / `=` 直後の曖昧さは、`[` から対応する `]` まで前方走査し、`]` の直後の非空白トークンが宣言を開始するかで判定する — 識別子は原則受理するが、式継続キーワード `is` / `as` / `switch` / `with` / `when` は宣言扱いしないので `Consume([Make()] is int[] xs)` / `([Make()] as int[])` / `Consume([Make()] switch { _ => true })` のような collection expression は `call` のまま残り、`@` は verbatim 識別子の先頭として、`(` は tuple 型パラメータとラムダ仮引数リストの先頭として受理し、`[A][B]` のような連続 bracket は内側 `[` に再帰して判定する。さらに `[Serializable]`、`[Obsolete]`、`[System.Obsolete]`、`[assembly: CLSCompliant]`、`[Required, Key]` のような引数なし C# 属性と、`@Deprecated`・`@Override`・`@org.junit.Test`・`@field:Deprecated` のような引数なし Java 系アノテーションは、`(` を要求する既存の `CallRegex` では拾えないため専用の no-arg 正規表現から emit するようになった。C# 側では引き続き `[...]` 事前パスのレンジに含まれるかで gate するので `arr[i]` のような indexer アクセスは属性化されず、アノテーション側は lookbehind `(?` 型パラメータ属性、`void M([Attr("x")] (int a, int b) value)` tuple 型パラメータ属性、`var f = [Attr("x")] () => 0;` ラムダ属性、上記 no-arg C# 属性群、Java `@Deprecated` / `@Override` / `@org.junit.Test` の no-arg アノテーション群、Kotlin `@field:Deprecated` と `return@foo` の negative ケース、indexer アクセスが属性化されないこと、`[\n Foo("x")\n]`、改行を挟んだパラメータ属性、`[return: NotNullWhen(true)]`、防御的に `Consume([Make()])` の collection expression-in-argument、および `[Make()] is ...` / `[Make()] as int[]` / `[Make()] switch { ... }` の 3 つの式継続ケースを押さえる回帰テストを追加。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Mcp/McpToolDefinitions.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 diff --git a/src/CodeIndex/Indexer/ReferenceExtractor.cs b/src/CodeIndex/Indexer/ReferenceExtractor.cs index b1202675ac..f3b3ca93b6 100644 --- a/src/CodeIndex/Indexer/ReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/ReferenceExtractor.cs @@ -547,21 +547,30 @@ private static (List>, List()); } - // Stack entries capture the opening `[` position and whether that bracket was at - // a C# declaration (attribute) position. Parens inside a section are tracked so the - // stack only pops when the bracket actually closes, not when an inner `)` appears. - // スタックは `[` の位置と、その bracket が属性位置だったかを保持する。セクション内の - // 括弧は独立して追跡し、内部の `)` によってスタックが誤って pop されないようにする。 - var bracketStack = new Stack<(int li, int ci, bool isAttr)>(); + // Stack entries capture the opening `[` position, whether that bracket was at + // a C# declaration (attribute) position, and a snapshot of the global paren depth + // at that moment. The snapshot lets us compute an attribute-section-local paren + // depth (`parenDepth - parenDepthAtOpen`), which is what the top-level zone tracking + // uses so that parameter attributes like `void M([Attr] int x)` still have their + // attribute-list top level at section-local depth 0 even though the global depth + // is inside the method's parameter list. + // スタックは `[` の位置、その bracket が属性位置だったか、および開いた瞬間の + // グローバル paren 深さのスナップショットを保持する。スナップショットを使うと + // 属性セクション内ローカルの paren 深さ (`parenDepth - parenDepthAtOpen`) が + // 得られるので、`void M([Attr] int x)` のように外側の method 引数リストの中で + // 開く属性セクションでも、セクション内では top-level (local depth 0) として扱える。 + var bracketStack = new Stack<(int li, int ci, bool isAttr, int parenDepthAtOpen)>(); char lastMeaningful = '\0'; int parenDepth = 0; bool lastClosedBracketWasAttribute = false; - // Top-level zone tracking: while we are inside an attribute section and parenDepth==0, - // the current zone span is open. When parens open we close it; when they fully close - // again we reopen. When the attribute section itself closes, we emit the final span. - // top-level ゾーン追跡: 属性セクション内かつ paren 深さ 0 のあいだだけゾーンを開いておき、 - // `(` で閉じ、`)` で再び開く。セクションが閉じる `]` で確定させる。 + // Top-level zone tracking: while we are inside an attribute section and the paren + // depth is at the section's open snapshot (section-local depth 0), the current zone + // span is open. When parens open inside the section we close it; when they fully + // close again we reopen. When the attribute section itself closes, we emit the span. + // top-level ゾーン追跡: 属性セクション内かつセクションローカルの paren 深さが 0 の + // あいだだけゾーンを開いておき、セクション内の `(` で閉じ、`)` で再び開く。 + // セクションが閉じる `]` で確定させる。 int topZoneStartLi = -1; int topZoneStartCi = 0; @@ -590,12 +599,19 @@ void EmitTopZone(int endLi, int endCi) if (c == '(') { - // If we are in a top-level attribute zone and this `(` opens the first - // paren depth, close the top-level zone just before the `(`. - // top-level ゾーン中で paren 深さが 0→1 になる `(` ならここでゾーンを閉じる。 - if (parenDepth == 0 && bracketStack.Count > 0 && bracketStack.Peek().isAttr && topZoneStartLi >= 0) + // If the innermost enclosing bracket is an attribute section and we are + // currently at that section's local top level, close the top-level zone + // just before the `(`. Use the stack top's `parenDepthAtOpen` snapshot so + // parameter attributes inside an outer `(...)` still get their top level + // tracked correctly. + // 直近の `[` が属性セクションで、かつその section-local 深さで top-level のとき、 + // `(` 直前でゾーンを閉じる。外側の `(...)` の中で開く属性セクションにも対応するため、 + // グローバル depth ではなくスタック top の開いたときの snapshot と比較する。 + if (bracketStack.Count > 0) { - EmitTopZone(li, ci); + var top = bracketStack.Peek(); + if (top.isAttr && parenDepth == top.parenDepthAtOpen && topZoneStartLi >= 0) + EmitTopZone(li, ci); } parenDepth++; lastMeaningful = c; @@ -606,11 +622,18 @@ void EmitTopZone(int endLi, int endCi) if (parenDepth > 0) { parenDepth--; - // paren 深さが 1→0 に戻ったら、依然として属性セクション内なら top-level ゾーンを再開する。 - if (parenDepth == 0 && bracketStack.Count > 0 && bracketStack.Peek().isAttr && topZoneStartLi < 0) + // If the innermost `[` is an attribute section and we just returned + // to that section's local top level, reopen the top-level zone. + // 直近の `[` が属性セクションで、section-local top-level に戻ってきたら + // top-level ゾーンを再開する。 + if (bracketStack.Count > 0) { - topZoneStartLi = li; - topZoneStartCi = ci + 1; + var top = bracketStack.Peek(); + if (top.isAttr && parenDepth == top.parenDepthAtOpen && topZoneStartLi < 0) + { + topZoneStartLi = li; + topZoneStartCi = ci + 1; + } } } lastMeaningful = c; @@ -621,12 +644,13 @@ void EmitTopZone(int endLi, int endCi) { bool isAttr = EvaluateCSharpAttributePosition( lastMeaningful, lastClosedBracketWasAttribute, preparedLines, li, ci); - bracketStack.Push((li, ci, isAttr)); - if (isAttr && parenDepth == 0 && topZoneStartLi < 0) + bracketStack.Push((li, ci, isAttr, parenDepth)); + if (isAttr && topZoneStartLi < 0) { // Start top-level zone just after the `[` so the `[` itself is not - // inside the zone. - // `[` 直後から top-level ゾーンを開始する。 + // inside the zone. Section-local depth is 0 by construction at the + // open bracket. + // `[` 直後から top-level ゾーンを開始する。開いた瞬間は section-local 深さ 0。 topZoneStartLi = li; topZoneStartCi = ci + 1; } @@ -652,12 +676,13 @@ void EmitTopZone(int endLi, int endCi) int e = (l == li) ? ci + 1 : preparedLines[l].Length; perLine[l].Add((s, e)); } - // Close the top-level zone at the `]` (paren depth should already - // be 0 here — if it is not, we simply drop the open zone because - // paren balancing was malformed). - // ここで top-level ゾーンを確定する。paren が閉じ切っていない場合は - // 不整合入力なのでゾーンを捨てる。 - if (parenDepth == 0) + // Close the top-level zone at the `]`. Section-local depth should + // be 0 here (we are at the closing bracket of this section) — if + // it is not, we drop the open zone because paren balancing was + // malformed. + // `]` で top-level ゾーンを確定する。section-local 深さが 0 のはず。 + // 不整合入力ならゾーンを捨てる。 + if (parenDepth == opened.parenDepthAtOpen) { EmitTopZone(li, ci + 1); } diff --git a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs index 56e151e929..e5c12e5a33 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs @@ -1859,6 +1859,61 @@ public class C Assert.DoesNotContain(references, r => r.ReferenceKind == "call"); } + [Fact] + public void Extract_CsharpNoArgParameterAttribute_ClassifiedAsAttribute() + { + // Regression (issue #293 follow-up): no-arg parameter attributes such as + // `void M([FromServices] IService s)` must still classify as `attribute`. + // A previous iteration of the top-level-zone gate tracked paren depth globally + // so the attribute section — which opens at global paren depth 1 inside the + // method parameter list — never entered top-level. The fix is to track paren + // depth section-locally so the section's own `[` / `]` define its zero point. + // リグレッション (issue #293 補足): `void M([FromServices] IService s)` のような + // 引数なしパラメータ属性も引き続き `attribute` として取り込まれること。 + const string content = """ + public class S + { + public void M([FromServices] IService s) { } + } + """; + + var symbols = SymbolExtractor.Extract(1, "csharp", content); + var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); + + var fromServices = Assert.Single(references.Where(r => r.SymbolName == "FromServices")); + Assert.Equal("attribute", fromServices.ReferenceKind); + Assert.DoesNotContain(references, r => r.SymbolName == "FromServices" && r.ReferenceKind == "call"); + } + + [Fact] + public void Extract_CsharpNoArgDelegateAndLambdaParameterAttributes_ClassifiedAsAttribute() + { + // Regression (issue #293 follow-up): no-arg attributes on delegate parameters and + // lambda parameters also open their `[` inside outer parens, so they require + // section-local paren-depth tracking for top-level zone detection. + // リグレッション (issue #293 補足): デリゲート・ラムダの仮引数に付く no-arg 属性も + // `(` の中で `[` が開くため、section-local の paren 深さ追跡が必要。 + const string content = """ + public delegate void D([Attr] int x); + public class C + { + public void M() + { + System.Func f = ([Attr] int x) => x; + } + } + """; + + var symbols = SymbolExtractor.Extract(1, "csharp", content); + var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); + + // Both occurrences of `Attr` should be classified as `attribute`, not `call`. + // 2 箇所の `Attr` が `attribute` として分類され、`call` にはならないこと。 + var attrs = references.Where(r => r.SymbolName == "Attr").ToList(); + Assert.Equal(2, attrs.Count); + Assert.All(attrs, r => Assert.Equal("attribute", r.ReferenceKind)); + } + [Fact] public void Extract_CsharpMultiLineAttributeArgumentEnum_NotClassifiedAsAttribute() { From 99c48c9d2e6c19f0b0bffa762869ae77f31038fe Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 18 Apr 2026 04:46:59 +0900 Subject: [PATCH 11/30] Fix #293 blank interior of multi-line C# attribute with non-leading [ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `StripMultiLineCSharpAttributeInterior` previously only activated when the opening `[` was the first non-whitespace character on the line, so a valid C# multi-line attribute that opens `[` mid-line — parameter attributes like `void M([\n FromServices\n] IService s)`, type-parameter attributes like `class Bar<[\n TypeParamAttr\n] T>`, and delegate/lambda parameter attributes like `delegate void D([\n DelegateParamAttr\n] int x)` — went through unchanged. Their interior identifier lines were then extracted as phantom `function` symbols, and the downstream `definitionNames` guard in `ReferenceExtractor` silently suppressed the real `attribute` reference on those names, dropping them from `references --kind attribute`. Extend the scan to find any `[` that does not close on the same line (keeping leading text before the `[` intact so `void M(` etc. are still recognizable to downstream declaration regexes), and blank the interior across subsequent lines until the matching `]`. This also naturally covers chained brackets (`[Foo][`-opens-multi-line) and array-indexer-then-attr mixes without extra logic. Regression: `Extract_CsharpMultiLineNoArgAttribute_NonLeadingOpenBracket_ClassifiedAsAttribute` runs SymbolExtractor + ReferenceExtractor end-to-end on the parameter, type-parameter, and delegate-parameter multi-line shapes and pins that `FromServices` / `TypeParamAttr` / `DelegateParamAttr` are classified as `attribute` and are NOT emitted as phantom `function` symbols. Co-Authored-By: Claude Opus 4 --- CHANGELOG.md | 4 +- src/CodeIndex/Indexer/SymbolExtractor.cs | 60 ++++++++++++------- .../ReferenceExtractorTests.cs | 52 ++++++++++++++++ 3 files changed, 91 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b97b537b1d..88ef44be04 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/), - **Developer Guide now distinguishes SQLite's role from "zero dependencies" and labels release numbers as examples** — Clarified that cdidx remains a zero-configuration, single-file CLI with exactly one production dependency (`Microsoft.Data.Sqlite`), and made the release-workflow checklist explicitly state that `1.9.0` is only an example version to substitute during an actual release. Affected: `DEVELOPER_GUIDE.md`. #### Fixed -- **Multi-line no-arg C# attribute sections like `[\n Serializable\n]` are now indexed without over-matching attribute argument identifiers (#293 follow-up)** — The no-arg C# attribute regex previously required the match to start at a `[` / `,` boundary and end at `]` / `,` on the same line, so a bare identifier on the interior line of a multi-line attribute section (e.g. ` Serializable` between a line-leading `[` and a line-leading `]`) was silently dropped from `references --kind attribute`. The left anchor is now a word-boundary lookbehind and the right anchor also accepts end-of-line. To avoid misclassifying enum / qualified-constant identifiers that happen to sit at end-of-line inside an attribute argument list (e.g. `ConverterStrategy.AllowNumbers` in `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]`) as no-arg attributes, `BuildCSharpAttributeRanges` now also emits a parallel "top-level zone" table that splits each attribute section by `(` / `)` so only paren-depth-0 positions inside the section qualify as no-arg attribute name positions. Paren depth is tracked section-locally (each `[` snapshots the global depth at open time) so parameter attributes like `void M([FromServices] IService s)`, delegate parameter attributes like `delegate void D([Attr] int x)`, and lambda parameter attributes like `Func f = ([Attr] int x) => x` — whose `[` opens while the global paren depth is already > 0 — still have their attribute-list top level correctly identified. The no-arg attribute path uses that top-level table instead of the full attribute-section ranges, while the existing with-args metadata classification still uses the full ranges. `SymbolExtractor` also now tracks multi-line `[...]` depth across lines via `StripMultiLineCSharpAttributeInterior` in `BuildCSharpMatchLines`, so interior lines of a multi-line attribute section are blanked out before declaration regexes run — this prevents ` Serializable` from being mis-extracted as a top-level `function` symbol (which then masked the reference through the `definitionNames` guard). Regression coverage adds `[\n Serializable\n]`, `[\n global::System.Obsolete\n]`, `[Required,\n Key]`, and `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]` (pinning that `AllowNumbers` / `ConverterStrategy` are NOT classified as `attribute`), running through `SymbolExtractor.Extract` + `ReferenceExtractor.Extract` end-to-end so both sides of the regression stay pinned. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Indexer/SymbolExtractor.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. +- **Multi-line no-arg C# attribute sections like `[\n Serializable\n]` are now indexed without over-matching attribute argument identifiers (#293 follow-up)** — The no-arg C# attribute regex previously required the match to start at a `[` / `,` boundary and end at `]` / `,` on the same line, so a bare identifier on the interior line of a multi-line attribute section (e.g. ` Serializable` between a line-leading `[` and a line-leading `]`) was silently dropped from `references --kind attribute`. The left anchor is now a word-boundary lookbehind and the right anchor also accepts end-of-line. To avoid misclassifying enum / qualified-constant identifiers that happen to sit at end-of-line inside an attribute argument list (e.g. `ConverterStrategy.AllowNumbers` in `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]`) as no-arg attributes, `BuildCSharpAttributeRanges` now also emits a parallel "top-level zone" table that splits each attribute section by `(` / `)` so only paren-depth-0 positions inside the section qualify as no-arg attribute name positions. Paren depth is tracked section-locally (each `[` snapshots the global depth at open time) so parameter attributes like `void M([FromServices] IService s)`, delegate parameter attributes like `delegate void D([Attr] int x)`, and lambda parameter attributes like `Func f = ([Attr] int x) => x` — whose `[` opens while the global paren depth is already > 0 — still have their attribute-list top level correctly identified. The no-arg attribute path uses that top-level table instead of the full attribute-section ranges, while the existing with-args metadata classification still uses the full ranges. `SymbolExtractor` also now tracks multi-line `[...]` depth across lines via `StripMultiLineCSharpAttributeInterior` in `BuildCSharpMatchLines`, so interior lines of a multi-line attribute section are blanked out before declaration regexes run — this prevents ` Serializable` from being mis-extracted as a top-level `function` symbol (which then masked the reference through the `definitionNames` guard). The multi-line blanker now activates on any `[` that opens without a matching `]` on the same line, not just `[` that sits at the start of the line, so multi-line parameter attributes like `void M([\n FromServices\n] IService s)`, type-parameter attributes like `class Bar<[\n TypeParamAttr\n] T>`, and delegate/lambda parameter attributes like `delegate void D([\n DelegateParamAttr\n] int x)` are also blanked out correctly — previously only the leading-`[` case was covered, so the interior identifiers of those forms were mis-extracted as `function` symbols and their attribute references were silently dropped from `references --kind attribute`. Regression coverage adds `[\n Serializable\n]`, `[\n global::System.Obsolete\n]`, `[Required,\n Key]`, `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]` (pinning that `AllowNumbers` / `ConverterStrategy` are NOT classified as `attribute`), and the non-leading-`[` parameter / type-parameter / delegate-parameter multi-line cases above, running through `SymbolExtractor.Extract` + `ReferenceExtractor.Extract` end-to-end so both sides of the regression stay pinned. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Indexer/SymbolExtractor.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. - **`::`-qualified C# no-arg attributes and JavaScript `@Decorator` usages are now classified correctly (#293 follow-up)** — The no-arg C# attribute regex now accepts `::` alongside `.` as a qualifier separator, so `[global::System.Obsolete]` and `[Alias::MyAttr]` reach `references --kind attribute` instead of being silently dropped. `AnnotationLanguages` additionally includes `javascript`, so JS decorators such as `@sealed` (no-arg) and `@injectable()` (with args) are reclassified to `annotation` rather than leaking into the call-graph as phantom `call` rows — matching the existing TypeScript contract since JavaScript is already a graph-supported language. Added regression tests pinning `[global::System.Obsolete]`, `[Alias::MyAttr]`, and JavaScript `@sealed` / `@injectable()`. Also rewrote the README `callers` row (EN + JP) and the MCP tool descriptions for `callers` / `callees` to pull back the earlier over-promise that `kind: "attribute"` / `kind: "annotation"` on `callers` / `callees` is a supported metadata-inspection path: a metadata row is attributed to its enclosing body-range symbol (for a class-level declaration, that is the class itself) and drops entirely for file-level targets such as `[assembly: ...]` where `container_name` is `null`, so the docs now direct users to `references --kind attribute|annotation` for metadata enumeration. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`, `README.md`. - **`impact` BFS now follows C# event subscribe edges (#293 follow-up)** — `DbReader.GetCallersExact`, the only caller site used by the `impact` / `impact_analysis` BFS, now filters references through the shared `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` set instead of `InvokeReferenceKindsSql = ('call', 'instantiate')`. Previously `impact` quietly dropped `Changed += OnChanged` subscribe rows while `callers` / `callees` / `hotspots` kept them, so an event-driven transitive caller chain appeared shorter under `impact` than under `callers`. A new `DbReaderTests.GetTransitiveCallers_FollowsSubscribeEdges` regression pins the subscribe edge into the BFS output. Metadata edges (`attribute`, `annotation`) remain excluded. Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. - **C# parameter, type-parameter, lambda, and multi-line `[...]` attribute lists are now classified as `attribute`, no-arg attributes and annotations are indexed, and MCP tool descriptions document the default metadata filter (#293 follow-up)** — `ReferenceExtractor` now runs a pre-pass that tokenizes every C# `[...]` section across the whole file, so parameter attributes such as `void M([FromServices] IService s)` (where `[` is preceded by `(` or `,` rather than a declaration boundary), type-parameter attributes such as `class C<[Attr("x")] T>` (preceded by `<`), tuple-typed parameter attributes such as `void M([Attr("x")] (int a, int b) value)` (where the next token after `]` is `(`), lambda attributes such as `var f = [Attr("x")] () => 0;` (where `[` is preceded by `=`), and multi-line forms such as `[\n Foo("x")\n]` or `public void M(\n [Attr("x")]\n int a)` are recognized alongside the existing same-line cases. The pre-pass disambiguates the `(` / `,` / `<` / `=` entry point against C# 12 collection expressions in argument position (`Consume([Make()])`), pattern expressions (`[Make()] is int[] xs`), `as` casts (`[Make()] as int[]`), and switch expressions (`[Make()] switch { _ => true }`) by scanning forward from `[` to the matching `]` and checking whether the next meaningful token begins a declaration — identifiers are accepted unless they are the expression-continuation keywords `is` / `as` / `switch` / `with` / `when`, `@` is accepted as a verbatim-identifier lead, `(` is accepted for tuple-typed parameters and lambda parameter lists, and chained `[A][B]` recurses through the inner bracket. Bare no-arg C# attributes such as `[Serializable]`, `[Obsolete]`, `[System.Obsolete]`, `[assembly: CLSCompliant]`, and `[Required, Key]`, plus bare no-arg Java-family annotations such as `@Deprecated`, `@Override`, `@org.junit.Test`, and `@field:Deprecated`, are now emitted through dedicated no-arg regexes since the original `CallRegex` requires `(` — the C# path still gates on the attribute-range pre-pass so indexer access such as `arr[i]` stays unclassified, and the annotation path uses a lookbehind `(?`, tuple-typed parameter `void M([Attr("x")] (int a, int b) value)`, lambda `var f = [Attr("x")] () => 0;`, the no-arg C# attribute set above, the no-arg Java `@Deprecated` / `@Override` / `@org.junit.Test` set, Kotlin `@field:Deprecated` plus the `return@foo` negative, indexer access staying unclassified, `[\n Foo("x")\n]`, the cross-line parameter attribute shape, the targeted `[return: ...]` shape, the defense-in-depth collection-expression-in-argument case, and the three expression-continuation cases `[Make()] is ...`, `[Make()] as int[]`, and `[Make()] switch { ... }`. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. @@ -682,7 +682,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **DEVELOPER_GUIDE が SQLite の位置付けとリリース番号の例示を明確化** — cdidx が「設定ゼロ・単一ファイル・本番依存1個」の CLI であり、その唯一の本番依存が `Microsoft.Data.Sqlite` であることを明記した。あわせて、リリース手順のチェックリスト中に出てくる `1.9.0` は固定値ではなく例示であり、実際のリリース番号へ読み替える前提だと明示した。対象: `DEVELOPER_GUIDE.md`。 #### 修正 -- **`[\n Serializable\n]` のような複数行にまたがる引数なし C# 属性セクションを、属性引数内の識別子を誤って取り込まずに索引化 (#293 追加対応)** — 引数なし C# 属性の正規表現は、これまで `[` / `,` を左端、`]` / `,` を右端として同一行内でアンカーしていたため、行頭 `[` と行頭 `]` の間に書かれた ` Serializable` のような裸識別子が `references --kind attribute` から黙って脱落していた。左側は単語境界 lookbehind、右側は行末(`$`)も許容するように緩和した。一方で `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]` のように属性の引数リスト内で行末に現れる enum / 修飾定数識別子(`AllowNumbers` など)が no-arg 属性として誤分類される回帰を避けるため、`BuildCSharpAttributeRanges` は `(` / `)` で属性セクションを分割した paren 深さ 0 の「top-level ゾーン」テーブルも並列で出力するようにし、no-arg 属性の採用位置はそのテーブルに限定した(既存の引数付き metadata 分類パスは引き続き属性セクション全体のレンジを使う)。paren 深さは section-local に追跡する(各 `[` がその瞬間のグローバル paren 深さを snapshot として保持)ため、`void M([FromServices] IService s)` のようなパラメータ属性、`delegate void D([Attr] int x)` のようなデリゲートパラメータ属性、`Func f = ([Attr] int x) => x` のようなラムダパラメータ属性のように、`[` が開いた時点で既にグローバル paren 深さが 0 より大きいケースでも、属性リストの top-level を正しく認識できる。`SymbolExtractor` 側でも `BuildCSharpMatchLines` に `StripMultiLineCSharpAttributeInterior` を追加し、複数行にまたがる `[...]` セクションの深さを跨行で追跡して内部行を宣言 regex 適用前に空白化するようにした。これにより ` Serializable` がトップレベル `function` シンボルとして誤抽出され、`definitionNames` ガード経由で参照分類が詰まってしまうのを防ぐ。`[\n Serializable\n]`・`[\n global::System.Obsolete\n]`・`[Required,\n Key]`・`[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]`(`AllowNumbers` / `ConverterStrategy` が `attribute` として取り込まれないことを固定)を `SymbolExtractor.Extract` + `ReferenceExtractor.Extract` で end-to-end に回して SymbolExtractor 側の回帰も固定する回帰テストを追加。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Indexer/SymbolExtractor.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 +- **`[\n Serializable\n]` のような複数行にまたがる引数なし C# 属性セクションを、属性引数内の識別子を誤って取り込まずに索引化 (#293 追加対応)** — 引数なし C# 属性の正規表現は、これまで `[` / `,` を左端、`]` / `,` を右端として同一行内でアンカーしていたため、行頭 `[` と行頭 `]` の間に書かれた ` Serializable` のような裸識別子が `references --kind attribute` から黙って脱落していた。左側は単語境界 lookbehind、右側は行末(`$`)も許容するように緩和した。一方で `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]` のように属性の引数リスト内で行末に現れる enum / 修飾定数識別子(`AllowNumbers` など)が no-arg 属性として誤分類される回帰を避けるため、`BuildCSharpAttributeRanges` は `(` / `)` で属性セクションを分割した paren 深さ 0 の「top-level ゾーン」テーブルも並列で出力するようにし、no-arg 属性の採用位置はそのテーブルに限定した(既存の引数付き metadata 分類パスは引き続き属性セクション全体のレンジを使う)。paren 深さは section-local に追跡する(各 `[` がその瞬間のグローバル paren 深さを snapshot として保持)ため、`void M([FromServices] IService s)` のようなパラメータ属性、`delegate void D([Attr] int x)` のようなデリゲートパラメータ属性、`Func f = ([Attr] int x) => x` のようなラムダパラメータ属性のように、`[` が開いた時点で既にグローバル paren 深さが 0 より大きいケースでも、属性リストの top-level を正しく認識できる。`SymbolExtractor` 側でも `BuildCSharpMatchLines` に `StripMultiLineCSharpAttributeInterior` を追加し、複数行にまたがる `[...]` セクションの深さを跨行で追跡して内部行を宣言 regex 適用前に空白化するようにした。これにより ` Serializable` がトップレベル `function` シンボルとして誤抽出され、`definitionNames` ガード経由で参照分類が詰まってしまうのを防ぐ。さらに、複数行の空白化起動条件を「同一行内で閉じない `[` があるかどうか」に広げ、行頭の `[` だけでなく `void M([\n FromServices\n] IService s)` のようなパラメータ属性、`class Bar<[\n TypeParamAttr\n] T>` のような型パラメータ属性、`delegate void D([\n DelegateParamAttr\n] int x)` のようなデリゲート / ラムダのパラメータ属性でも内部行が正しく空白化されるようにした — 以前は開口行で `[` が行頭にある場合しかカバーされていなかったため、上記の形の内部識別子が `function` シンボルとして誤抽出され、`references --kind attribute` から属性参照が黙って脱落していた。`[\n Serializable\n]`・`[\n global::System.Obsolete\n]`・`[Required,\n Key]`・`[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]`(`AllowNumbers` / `ConverterStrategy` が `attribute` として取り込まれないことを固定)に加え、上記の行頭でない `[` で開く複数行パラメータ / 型パラメータ / デリゲートパラメータ属性のケースも `SymbolExtractor.Extract` + `ReferenceExtractor.Extract` で end-to-end に回して SymbolExtractor 側の回帰も固定する回帰テストを追加。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Indexer/SymbolExtractor.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 - **`::` 修飾の C# 引数なし属性と JavaScript の `@Decorator` を正しく分類 (#293 追加対応)** — 引数なし C# 属性の正規表現が修飾子セパレータとして `.` に加えて `::` も受け付けるようになり、`[global::System.Obsolete]` や `[Alias::MyAttr]` のような形が `references --kind attribute` から黙って脱落しなくなった。`AnnotationLanguages` にも `javascript` を追加し、JavaScript はもともと graph 対応言語なので、`@sealed`(引数なし)や `@injectable()`(引数付き)のような JS decorator も `annotation` として再分類され、TypeScript と同じ契約で call-graph に phantom `call` 行を残さない。`[global::System.Obsolete]`・`[Alias::MyAttr]`・JavaScript の `@sealed` / `@injectable()` を押さえる回帰テストを追加。あわせて、以前に進めていた `callers` / `callees` の metadata 契約を README(英日)と MCP ツール説明の両方で引き戻し、metadata 行は注釈対象そのものではなく body-range 上の外側シンボル(クラス直下宣言ならクラス、`[assembly: ...]` のようなファイルレベル target では `container_name` が `null`)に帰属するため `kind: "attribute"` / `kind: "annotation"` を `callers` / `callees` に渡すのは信頼できる metadata 調査経路ではなく、metadata 列挙は `references --kind attribute|annotation` を使うよう案内する文言に修正した。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Mcp/McpToolDefinitions.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`、`README.md`。 - **`impact` BFS が C# の event subscribe edge を辿るよう修正 (#293 追加対応)** — `impact` / `impact_analysis` BFS の唯一の呼び出し箇所である `DbReader.GetCallersExact` が、共有 filter `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` で参照を絞り込むようになった(以前は `InvokeReferenceKindsSql = ('call', 'instantiate')` を使っていたため、`Changed += OnChanged` のような subscribe 行を静かに捨てていた)。これにより `callers` / `callees` / `hotspots` と同じ call-graph 契約を `impact` も共有し、イベント駆動の transitive caller chain が `impact` でだけ短くなる不整合が解消する。`DbReaderTests.GetTransitiveCallers_FollowsSubscribeEdges` の回帰テストで subscribe edge が BFS 出力に含まれることを固定。Metadata edge (`attribute` / `annotation`) の除外は従来通り。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 - **C# のパラメータ属性・型パラメータ属性・ラムダ属性・複数行 `[...]` 属性を `attribute` として分類し、引数なし属性とアノテーションも索引化、MCP ツール説明に既定の metadata フィルタを反映 (#293 追加対応)** — `ReferenceExtractor` がファイル全体の C# `[...]` セクションを事前パスでトークン化するようになり、`void M([FromServices] IService s)` のように `[` の直前が宣言境界ではなく `(` / `,` で始まるパラメータ属性、`class C<[Attr("x")] T>` のように `<` で始まる型パラメータ属性、`void M([Attr("x")] (int a, int b) value)` のように `]` の直後が `(` のまま続く tuple 型パラメータ属性、`var f = [Attr("x")] () => 0;` のように `=` の直後にあるラムダ属性、さらに `[\n Foo("x")\n]` や `public void M(\n [Attr("x")]\n int a)` のような複数行形式も、既存の同一行ケースと同じように属性として扱われるようになった。`(` / `,` / `<` / `=` 直後の曖昧さは、`[` から対応する `]` まで前方走査し、`]` の直後の非空白トークンが宣言を開始するかで判定する — 識別子は原則受理するが、式継続キーワード `is` / `as` / `switch` / `with` / `when` は宣言扱いしないので `Consume([Make()] is int[] xs)` / `([Make()] as int[])` / `Consume([Make()] switch { _ => true })` のような collection expression は `call` のまま残り、`@` は verbatim 識別子の先頭として、`(` は tuple 型パラメータとラムダ仮引数リストの先頭として受理し、`[A][B]` のような連続 bracket は内側 `[` に再帰して判定する。さらに `[Serializable]`、`[Obsolete]`、`[System.Obsolete]`、`[assembly: CLSCompliant]`、`[Required, Key]` のような引数なし C# 属性と、`@Deprecated`・`@Override`・`@org.junit.Test`・`@field:Deprecated` のような引数なし Java 系アノテーションは、`(` を要求する既存の `CallRegex` では拾えないため専用の no-arg 正規表現から emit するようになった。C# 側では引き続き `[...]` 事前パスのレンジに含まれるかで gate するので `arr[i]` のような indexer アクセスは属性化されず、アノテーション側は lookbehind `(?` 型パラメータ属性、`void M([Attr("x")] (int a, int b) value)` tuple 型パラメータ属性、`var f = [Attr("x")] () => 0;` ラムダ属性、上記 no-arg C# 属性群、Java `@Deprecated` / `@Override` / `@org.junit.Test` の no-arg アノテーション群、Kotlin `@field:Deprecated` と `return@foo` の negative ケース、indexer アクセスが属性化されないこと、`[\n Foo("x")\n]`、改行を挟んだパラメータ属性、`[return: NotNullWhen(true)]`、防御的に `Consume([Make()])` の collection expression-in-argument、および `[Make()] is ...` / `[Make()] as int[]` / `[Make()] switch { ... }` の 3 つの式継続ケースを押さえる回帰テストを追加。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Mcp/McpToolDefinitions.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 diff --git a/src/CodeIndex/Indexer/SymbolExtractor.cs b/src/CodeIndex/Indexer/SymbolExtractor.cs index edce0b1d4a..ab1652734f 100644 --- a/src/CodeIndex/Indexer/SymbolExtractor.cs +++ b/src/CodeIndex/Indexer/SymbolExtractor.cs @@ -5365,45 +5365,59 @@ private static string[] BuildCSharpMatchLines(string[] structuralLines) } /// - /// Track multi-line C# `[...]` attribute sections across lines and blank out any text that + /// Track multi-line C# `[...]` bracket sections across lines and blank out any text that /// sits inside those sections, so downstream symbol regexes do not treat interior identifiers - /// as declarations. Only activates when a leading `[` (optionally after whitespace) opens the - /// section without a matching `]` on the same line — single-line attribute lists continue to - /// be handled by `StripLeadingCSharpAttributeLists`. - /// 複数行にまたがる C# `[...]` 属性セクションを跨行で追跡し、内部の文字列を空白化することで - /// 下流のシンボル regex が内部の識別子を宣言として誤解釈しないようにする。行頭(空白の後)の - /// `[` が同一行内で閉じないときだけ起動し、同一行で完結する属性リストは + /// as declarations. Activates whenever a `[` opens without a matching `]` on the same line, + /// regardless of whether the `[` sits at the start of the line (leading attribute) or deeper + /// inside the line (parameter attribute like `void M([\n Attr\n] T x)`, type-parameter + /// attribute like `class C<[\n Attr\n] T>`, delegate/lambda parameter attributes, etc.). + /// Single-line attribute lists continue to be handled by `StripLeadingCSharpAttributeLists`. + /// 複数行にまたがる C# `[...]` セクションを跨行で追跡し、内部の文字列を空白化することで + /// 下流のシンボル regex が内部の識別子を宣言として誤解釈しないようにする。`[` が行頭 + /// (空白の後)にある場合だけでなく、`void M([\n Attr\n] T x)` のようなパラメータ属性、 + /// `class C<[\n Attr\n] T>` のような型パラメータ属性、delegate / lambda のパラメータ属性など、 + /// 行の途中で開いて同一行で閉じない `[` でも作動する。同一行で完結する属性リストは /// `StripLeadingCSharpAttributeLists` が引き続き担当する。 /// private static string StripMultiLineCSharpAttributeInterior(string line, ref int depth) { if (depth == 0) { - // Detect the opening of a multi-line attribute section: line starts with `[`, and the - // `[` is not closed on the same line (single-line cases are handled downstream). - var cursor = 0; - while (cursor < line.Length && char.IsWhiteSpace(line[cursor])) - cursor++; - if (cursor >= line.Length || line[cursor] != '[') - return line; - - // Count `[` / `]` on this line to decide whether we span into following lines. + // Scan the line for a `[` that is NOT closed on the same line. Everything before + // that `[` is real code (method header text like `void M(`, generic opener like + // `class C<`, etc.) and must be preserved so downstream declaration regexes can + // still recognize the surrounding construct. Everything from the unclosed `[` + // onward is blanked, and subsequent lines are blanked until the matching `]`. + // 行内を走査し、同一行で閉じない `[` を探す。その `[` より前は通常のコード + // (`void M(` のようなメソッドヘッダ、`class C<` のようなジェネリック開口など) + // であり、下流の宣言 regex が外側の構文を認識できるように残す必要がある。 + // 閉じない `[` 以降は空白化し、対応する `]` が現れるまで後続行も空白化する。 + int openIndex = -1; int localDepth = 0; - for (int i = cursor; i < line.Length; i++) + for (int i = 0; i < line.Length; i++) { - if (line[i] == '[') localDepth++; - else if (line[i] == ']') + if (line[i] == '[') { - localDepth--; if (localDepth == 0) - return line; // single-line section — leave to StripLeadingCSharpAttributeLists + openIndex = i; + localDepth++; + } + else if (line[i] == ']') + { + if (localDepth > 0) + { + localDepth--; + if (localDepth == 0) + openIndex = -1; + } } } - if (localDepth <= 0) + + if (openIndex < 0 || localDepth <= 0) return line; depth = localDepth; - return string.Empty; + return line.Substring(0, openIndex); } // We are inside a multi-line attribute section. Walk the line, closing brackets when we diff --git a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs index e5c12e5a33..9e48d4426c 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs @@ -1914,6 +1914,58 @@ public void M() Assert.All(attrs, r => Assert.Equal("attribute", r.ReferenceKind)); } + [Fact] + public void Extract_CsharpMultiLineNoArgAttribute_NonLeadingOpenBracket_ClassifiedAsAttribute() + { + // Regression (issue #293 follow-up): multi-line `[...]` sections that open with `[` + // appearing AFTER other text on the opening line (e.g. `void M([`, `class C<[`, + // `delegate void D([`) must also blank out the interior in SymbolExtractor. Otherwise + // the bare identifier on the interior line is extracted as a phantom `function` + // declaration, and the downstream `definitionNames` guard suppresses the real + // `attribute` reference, silently dropping it from `references --kind attribute`. + // リグレッション (issue #293 補足): 開口行の途中で `[` が開く複数行属性 + // (`void M([`, `class C<[`, `delegate void D([` 等) も SymbolExtractor 側で + // 内部を空白化しなければならない。そうしないと、内部行の裸識別子が phantom な + // `function` 宣言として抽出され、下流の `definitionNames` ガードに食われて + // 本来の `attribute` 参照が `references --kind attribute` から消える。 + const string content = """ + public class Foo + { + public void M([ + FromServices + ] IService s) { } + } + + public class Bar<[ + TypeParamAttr + ] T> + { + } + + public delegate void D([ + DelegateParamAttr + ] int x); + """; + + var symbols = SymbolExtractor.Extract(1, "csharp", content); + var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); + + // None of the attribute names should be misclassified as phantom function symbols. + // 属性名が phantom な function シンボルとして抽出されていないこと。 + Assert.DoesNotContain(symbols, s => s.Name == "FromServices" && s.Kind == "function"); + Assert.DoesNotContain(symbols, s => s.Name == "TypeParamAttr" && s.Kind == "function"); + Assert.DoesNotContain(symbols, s => s.Name == "DelegateParamAttr" && s.Kind == "function"); + + var fromServices = Assert.Single(references.Where(r => r.SymbolName == "FromServices")); + Assert.Equal("attribute", fromServices.ReferenceKind); + + var typeParamAttr = Assert.Single(references.Where(r => r.SymbolName == "TypeParamAttr")); + Assert.Equal("attribute", typeParamAttr.ReferenceKind); + + var delegateParamAttr = Assert.Single(references.Where(r => r.SymbolName == "DelegateParamAttr")); + Assert.Equal("attribute", delegateParamAttr.ReferenceKind); + } + [Fact] public void Extract_CsharpMultiLineAttributeArgumentEnum_NotClassifiedAsAttribute() { From f8d1a46a6f50440609a25f5590d2c58c66993877 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 18 Apr 2026 05:49:24 +0900 Subject: [PATCH 12/30] Fix #293 exclude metadata refs from deps/impact, reject metadata kinds on callers/callees MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address two blocking findings from the Codex adversarial review: 1. deps and impact file-level dependency hints leaked metadata refs. DbReader.GetFileDependencies (cdidx deps) and GetFileDependencyHintsToResolvedType (cdidx impact / MCP impact_analysis heuristic fallback) filtered only by graph-supported language, so [JsonConverter(typeof(User))] or @Inject(User.class) — compile-time metadata that touches a type but does not create a runtime dependency — made the annotated file look like it depended on the target type. Both CTEs now also gate on r.reference_kind IN CallGraphReferenceKindsSql ('call', 'instantiate', 'subscribe'), matching the contract already enforced by callers / callees / hotspots / impact BFS. 2. callers --kind attribute|annotation silently returned wrong rows. Metadata refs are attributed to their enclosing body-range symbol rather than the annotated target, so callers Obsolete --kind attribute returned [Obsolete] void M() under the enclosing class (not M), and file-level targets like [assembly: CLSCompliant] or Kotlin @file:JvmName dropped entirely because container_name is NULL. Reject --kind attribute|annotation at the CLI (QueryCommandRunner.RunCallers/RunCallees with UsageError) and MCP boundary (ExecuteCallers/ExecuteCallees returns isError tool response). Both paths redirect users to references --kind attribute|annotation, which IS a correct metadata enumeration path. Also reclassify Swift / Gradle @-metadata as annotation: AnnotationLanguages now includes swift and gradle. Swift @available(iOS 13.0, *) / @objc / @MainActor and Gradle/Groovy @CompileStatic / @TaskAction are now recorded as annotation instead of call (the @Name(args) forms previously leaked into callers / callees / hotspots / impact as phantom call edges, and the no-arg forms dropped from the index entirely because no-arg annotation emission is gated on this set). Regression coverage (+12 tests): DbReaderTests.GetFileDependencies_ExcludesMetadataOnlyReferences, AnalyzeImpact_ExcludesMetadataOnlyReferencesFromHeuristicHints; QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError (theory, 4 cases); McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError (theory, 4 cases); ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation, Extract_GradleAnnotation_ClassifiedAsAnnotation. Full suite 1565 passed / 2 skipped / 0 failed. Co-Authored-By: Claude Opus 4 --- CHANGELOG.md | 4 + src/CodeIndex/Cli/QueryCommandRunner.cs | 42 ++++++ src/CodeIndex/Database/DbReader.cs | 19 ++- src/CodeIndex/Indexer/ReferenceExtractor.cs | 17 ++- src/CodeIndex/Mcp/McpToolHandlers.cs | 19 +++ tests/CodeIndex.Tests/DbReaderTests.cs | 125 ++++++++++++++++++ tests/CodeIndex.Tests/McpServerTests.cs | 30 +++++ .../QueryCommandRunnerTests.cs | 43 ++++++ .../ReferenceExtractorTests.cs | 71 ++++++++++ 9 files changed, 363 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88ef44be04..221b9f7ce8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **Developer Guide now distinguishes SQLite's role from "zero dependencies" and labels release numbers as examples** — Clarified that cdidx remains a zero-configuration, single-file CLI with exactly one production dependency (`Microsoft.Data.Sqlite`), and made the release-workflow checklist explicitly state that `1.9.0` is only an example version to substitute during an actual release. Affected: `DEVELOPER_GUIDE.md`. #### Fixed +- **`deps` and `impact` file-level dependency hints now exclude metadata-only references (#293 follow-up)** — `DbReader.GetFileDependencies` (backing `cdidx deps`) and `DbReader.GetFileDependencyHintsToResolvedType` (backing `cdidx impact` / MCP `impact_analysis` heuristic file-hint fallback) used to project every `symbol_references` row into the logical reference CTE, filtering only by graph-supported language. Metadata-only usages such as `[JsonConverter(typeof(User))]` or `@Inject(User.class)` — which touch a type at compile time but do not create a runtime dependency — therefore leaked into dependency output and made the annotated file look like it depended on the target type. Both queries now additionally gate the CTE on `r.reference_kind IN CallGraphReferenceKindsSql ('call', 'instantiate', 'subscribe')`, matching the contract already enforced by `callers` / `callees` / `hotspots` / `impact` BFS. Added regression coverage in `DbReaderTests`: `GetFileDependencies_ExcludesMetadataOnlyReferences` pins that the attribute-only consumer does not create a `deps` edge while `new User()` still does, and `AnalyzeImpact_ExcludesMetadataOnlyReferencesFromHeuristicHints` pins that an attribute-only consumer produces zero heuristic file hints. Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. +- **`callers` / `callees` now reject `--kind attribute|annotation` at the CLI and MCP boundary, and Swift / Gradle `@` metadata is reclassified as `annotation` (#293 follow-up)** — Metadata rows (`attribute` / `annotation`) are attributed to their enclosing body-range symbol rather than the annotated target, so `callers Obsolete --kind attribute` would silently return `[Obsolete] void M()` under the enclosing class (not `M`), and file-level targets such as `[assembly: CLSCompliant]` or Kotlin `@file:JvmName` would drop entirely because `container_name` is `NULL`. `QueryCommandRunner.RunCallers` / `RunCallees` now reject `--kind attribute` and `--kind annotation` with `CommandExitCodes.UsageError`, and the MCP `callers` / `callees` tools return an `isError` tool response; both paths direct users to `references --kind attribute|annotation`, which IS a correct metadata enumeration path. Separately, `AnnotationLanguages` now includes `swift` and `gradle`, so Swift `@available(iOS 13.0, *)` / `@objc` / `@MainActor` and Gradle/Groovy `@CompileStatic` / `@TaskAction` are recorded as `annotation` instead of `call` (previously the `@Name(args)` forms leaked into `callers` / `callees` / `hotspots` / `impact` as phantom call edges, and the no-arg `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` forms dropped from the index entirely because no-arg annotation emission was gated to languages in this set). Added `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError` (theory, 4 cases), `McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError` (theory, 4 cases), `ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`, and `ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation`. Affected: `src/CodeIndex/Cli/QueryCommandRunner.cs`, `src/CodeIndex/Mcp/McpToolHandlers.cs`, `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. - **Multi-line no-arg C# attribute sections like `[\n Serializable\n]` are now indexed without over-matching attribute argument identifiers (#293 follow-up)** — The no-arg C# attribute regex previously required the match to start at a `[` / `,` boundary and end at `]` / `,` on the same line, so a bare identifier on the interior line of a multi-line attribute section (e.g. ` Serializable` between a line-leading `[` and a line-leading `]`) was silently dropped from `references --kind attribute`. The left anchor is now a word-boundary lookbehind and the right anchor also accepts end-of-line. To avoid misclassifying enum / qualified-constant identifiers that happen to sit at end-of-line inside an attribute argument list (e.g. `ConverterStrategy.AllowNumbers` in `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]`) as no-arg attributes, `BuildCSharpAttributeRanges` now also emits a parallel "top-level zone" table that splits each attribute section by `(` / `)` so only paren-depth-0 positions inside the section qualify as no-arg attribute name positions. Paren depth is tracked section-locally (each `[` snapshots the global depth at open time) so parameter attributes like `void M([FromServices] IService s)`, delegate parameter attributes like `delegate void D([Attr] int x)`, and lambda parameter attributes like `Func f = ([Attr] int x) => x` — whose `[` opens while the global paren depth is already > 0 — still have their attribute-list top level correctly identified. The no-arg attribute path uses that top-level table instead of the full attribute-section ranges, while the existing with-args metadata classification still uses the full ranges. `SymbolExtractor` also now tracks multi-line `[...]` depth across lines via `StripMultiLineCSharpAttributeInterior` in `BuildCSharpMatchLines`, so interior lines of a multi-line attribute section are blanked out before declaration regexes run — this prevents ` Serializable` from being mis-extracted as a top-level `function` symbol (which then masked the reference through the `definitionNames` guard). The multi-line blanker now activates on any `[` that opens without a matching `]` on the same line, not just `[` that sits at the start of the line, so multi-line parameter attributes like `void M([\n FromServices\n] IService s)`, type-parameter attributes like `class Bar<[\n TypeParamAttr\n] T>`, and delegate/lambda parameter attributes like `delegate void D([\n DelegateParamAttr\n] int x)` are also blanked out correctly — previously only the leading-`[` case was covered, so the interior identifiers of those forms were mis-extracted as `function` symbols and their attribute references were silently dropped from `references --kind attribute`. Regression coverage adds `[\n Serializable\n]`, `[\n global::System.Obsolete\n]`, `[Required,\n Key]`, `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]` (pinning that `AllowNumbers` / `ConverterStrategy` are NOT classified as `attribute`), and the non-leading-`[` parameter / type-parameter / delegate-parameter multi-line cases above, running through `SymbolExtractor.Extract` + `ReferenceExtractor.Extract` end-to-end so both sides of the regression stay pinned. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Indexer/SymbolExtractor.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. - **`::`-qualified C# no-arg attributes and JavaScript `@Decorator` usages are now classified correctly (#293 follow-up)** — The no-arg C# attribute regex now accepts `::` alongside `.` as a qualifier separator, so `[global::System.Obsolete]` and `[Alias::MyAttr]` reach `references --kind attribute` instead of being silently dropped. `AnnotationLanguages` additionally includes `javascript`, so JS decorators such as `@sealed` (no-arg) and `@injectable()` (with args) are reclassified to `annotation` rather than leaking into the call-graph as phantom `call` rows — matching the existing TypeScript contract since JavaScript is already a graph-supported language. Added regression tests pinning `[global::System.Obsolete]`, `[Alias::MyAttr]`, and JavaScript `@sealed` / `@injectable()`. Also rewrote the README `callers` row (EN + JP) and the MCP tool descriptions for `callers` / `callees` to pull back the earlier over-promise that `kind: "attribute"` / `kind: "annotation"` on `callers` / `callees` is a supported metadata-inspection path: a metadata row is attributed to its enclosing body-range symbol (for a class-level declaration, that is the class itself) and drops entirely for file-level targets such as `[assembly: ...]` where `container_name` is `null`, so the docs now direct users to `references --kind attribute|annotation` for metadata enumeration. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`, `README.md`. - **`impact` BFS now follows C# event subscribe edges (#293 follow-up)** — `DbReader.GetCallersExact`, the only caller site used by the `impact` / `impact_analysis` BFS, now filters references through the shared `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` set instead of `InvokeReferenceKindsSql = ('call', 'instantiate')`. Previously `impact` quietly dropped `Changed += OnChanged` subscribe rows while `callers` / `callees` / `hotspots` kept them, so an event-driven transitive caller chain appeared shorter under `impact` than under `callers`. A new `DbReaderTests.GetTransitiveCallers_FollowsSubscribeEdges` regression pins the subscribe edge into the BFS output. Metadata edges (`attribute`, `annotation`) remain excluded. Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. @@ -682,6 +684,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **DEVELOPER_GUIDE が SQLite の位置付けとリリース番号の例示を明確化** — cdidx が「設定ゼロ・単一ファイル・本番依存1個」の CLI であり、その唯一の本番依存が `Microsoft.Data.Sqlite` であることを明記した。あわせて、リリース手順のチェックリスト中に出てくる `1.9.0` は固定値ではなく例示であり、実際のリリース番号へ読み替える前提だと明示した。対象: `DEVELOPER_GUIDE.md`。 #### 修正 +- **`deps` と `impact` のファイルレベル依存ヒントから metadata-only 参照を除外 (#293 追加対応)** — `cdidx deps` の実装である `DbReader.GetFileDependencies` と、`cdidx impact` / MCP `impact_analysis` の heuristic file-hint フォールバックである `DbReader.GetFileDependencyHintsToResolvedType` が、これまで `symbol_references` を graph 対応言語でしか絞っておらず `reference_kind` で絞っていなかったため、`[JsonConverter(typeof(User))]` や `@Inject(User.class)` のような compile-time metadata として型に触れるだけで実行時依存ではない参照が依存エッジとして漏れていた(注釈ファイルが対象型に依存しているように見えた)。両クエリの logical reference CTE を `r.reference_kind IN CallGraphReferenceKindsSql ('call', 'instantiate', 'subscribe')` で追加絞り込みし、`callers` / `callees` / `hotspots` / `impact` BFS と同じ契約に揃えた。回帰テストを `DbReaderTests` に追加: `GetFileDependencies_ExcludesMetadataOnlyReferences` は attribute-only consumer が `deps` エッジを作らず `new User()` は作ることを固定し、`AnalyzeImpact_ExcludesMetadataOnlyReferencesFromHeuristicHints` は attribute-only consumer が heuristic file hint を 1 件も生成しないことを固定する。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 +- **`callers` / `callees` が CLI と MCP 境界で `--kind attribute|annotation` を拒否し、Swift / Gradle の `@` metadata を `annotation` に再分類 (#293 追加対応)** — metadata 行 (`attribute` / `annotation`) は注釈対象そのものではなく body-range の外側シンボルに帰属するため、`callers Obsolete --kind attribute` は `[Obsolete] void M()` を `M` ではなく外側クラスの下に黙って返し、`[assembly: CLSCompliant]` や Kotlin の `@file:JvmName` のような file-level target は `container_name = NULL` で完全脱落していた。`QueryCommandRunner.RunCallers` / `RunCallees` は `--kind attribute` / `--kind annotation` を `CommandExitCodes.UsageError` で弾き、MCP の `callers` / `callees` ツールは `isError` ツールレスポンスを返すようにした。どちらの経路も、metadata 列挙の正しいパスである `references --kind attribute|annotation` へ誘導する。あわせて `AnnotationLanguages` に `swift` と `gradle` を追加し、Swift の `@available(iOS 13.0, *)` / `@objc` / `@MainActor` や Gradle / Groovy の `@CompileStatic` / `@TaskAction` を `call` ではなく `annotation` として記録するようにした(修正前は引数付きの `@Name(args)` 形は `callers` / `callees` / `hotspots` / `impact` に phantom call edge として漏れ、引数なしの `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` 形はこの集合で gate されていた no-arg annotation 出力から外れてインデックス自体に残らなかった)。回帰テストとして `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError`(theory 4 ケース)、`McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError`(theory 4 ケース)、`ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`、`ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation` を追加。対象: `src/CodeIndex/Cli/QueryCommandRunner.cs`、`src/CodeIndex/Mcp/McpToolHandlers.cs`、`src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 - **`[\n Serializable\n]` のような複数行にまたがる引数なし C# 属性セクションを、属性引数内の識別子を誤って取り込まずに索引化 (#293 追加対応)** — 引数なし C# 属性の正規表現は、これまで `[` / `,` を左端、`]` / `,` を右端として同一行内でアンカーしていたため、行頭 `[` と行頭 `]` の間に書かれた ` Serializable` のような裸識別子が `references --kind attribute` から黙って脱落していた。左側は単語境界 lookbehind、右側は行末(`$`)も許容するように緩和した。一方で `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]` のように属性の引数リスト内で行末に現れる enum / 修飾定数識別子(`AllowNumbers` など)が no-arg 属性として誤分類される回帰を避けるため、`BuildCSharpAttributeRanges` は `(` / `)` で属性セクションを分割した paren 深さ 0 の「top-level ゾーン」テーブルも並列で出力するようにし、no-arg 属性の採用位置はそのテーブルに限定した(既存の引数付き metadata 分類パスは引き続き属性セクション全体のレンジを使う)。paren 深さは section-local に追跡する(各 `[` がその瞬間のグローバル paren 深さを snapshot として保持)ため、`void M([FromServices] IService s)` のようなパラメータ属性、`delegate void D([Attr] int x)` のようなデリゲートパラメータ属性、`Func f = ([Attr] int x) => x` のようなラムダパラメータ属性のように、`[` が開いた時点で既にグローバル paren 深さが 0 より大きいケースでも、属性リストの top-level を正しく認識できる。`SymbolExtractor` 側でも `BuildCSharpMatchLines` に `StripMultiLineCSharpAttributeInterior` を追加し、複数行にまたがる `[...]` セクションの深さを跨行で追跡して内部行を宣言 regex 適用前に空白化するようにした。これにより ` Serializable` がトップレベル `function` シンボルとして誤抽出され、`definitionNames` ガード経由で参照分類が詰まってしまうのを防ぐ。さらに、複数行の空白化起動条件を「同一行内で閉じない `[` があるかどうか」に広げ、行頭の `[` だけでなく `void M([\n FromServices\n] IService s)` のようなパラメータ属性、`class Bar<[\n TypeParamAttr\n] T>` のような型パラメータ属性、`delegate void D([\n DelegateParamAttr\n] int x)` のようなデリゲート / ラムダのパラメータ属性でも内部行が正しく空白化されるようにした — 以前は開口行で `[` が行頭にある場合しかカバーされていなかったため、上記の形の内部識別子が `function` シンボルとして誤抽出され、`references --kind attribute` から属性参照が黙って脱落していた。`[\n Serializable\n]`・`[\n global::System.Obsolete\n]`・`[Required,\n Key]`・`[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]`(`AllowNumbers` / `ConverterStrategy` が `attribute` として取り込まれないことを固定)に加え、上記の行頭でない `[` で開く複数行パラメータ / 型パラメータ / デリゲートパラメータ属性のケースも `SymbolExtractor.Extract` + `ReferenceExtractor.Extract` で end-to-end に回して SymbolExtractor 側の回帰も固定する回帰テストを追加。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Indexer/SymbolExtractor.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 - **`::` 修飾の C# 引数なし属性と JavaScript の `@Decorator` を正しく分類 (#293 追加対応)** — 引数なし C# 属性の正規表現が修飾子セパレータとして `.` に加えて `::` も受け付けるようになり、`[global::System.Obsolete]` や `[Alias::MyAttr]` のような形が `references --kind attribute` から黙って脱落しなくなった。`AnnotationLanguages` にも `javascript` を追加し、JavaScript はもともと graph 対応言語なので、`@sealed`(引数なし)や `@injectable()`(引数付き)のような JS decorator も `annotation` として再分類され、TypeScript と同じ契約で call-graph に phantom `call` 行を残さない。`[global::System.Obsolete]`・`[Alias::MyAttr]`・JavaScript の `@sealed` / `@injectable()` を押さえる回帰テストを追加。あわせて、以前に進めていた `callers` / `callees` の metadata 契約を README(英日)と MCP ツール説明の両方で引き戻し、metadata 行は注釈対象そのものではなく body-range 上の外側シンボル(クラス直下宣言ならクラス、`[assembly: ...]` のようなファイルレベル target では `container_name` が `null`)に帰属するため `kind: "attribute"` / `kind: "annotation"` を `callers` / `callees` に渡すのは信頼できる metadata 調査経路ではなく、metadata 列挙は `references --kind attribute|annotation` を使うよう案内する文言に修正した。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Mcp/McpToolDefinitions.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`、`README.md`。 - **`impact` BFS が C# の event subscribe edge を辿るよう修正 (#293 追加対応)** — `impact` / `impact_analysis` BFS の唯一の呼び出し箇所である `DbReader.GetCallersExact` が、共有 filter `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` で参照を絞り込むようになった(以前は `InvokeReferenceKindsSql = ('call', 'instantiate')` を使っていたため、`Changed += OnChanged` のような subscribe 行を静かに捨てていた)。これにより `callers` / `callees` / `hotspots` と同じ call-graph 契約を `impact` も共有し、イベント駆動の transitive caller chain が `impact` でだけ短くなる不整合が解消する。`DbReaderTests.GetTransitiveCallers_FollowsSubscribeEdges` の回帰テストで subscribe edge が BFS 出力に含まれることを固定。Metadata edge (`attribute` / `annotation`) の除外は従来通り。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 diff --git a/src/CodeIndex/Cli/QueryCommandRunner.cs b/src/CodeIndex/Cli/QueryCommandRunner.cs index da433be4a0..f66328f967 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.cs @@ -388,6 +388,8 @@ public static int RunCallers(string[] cmdArgs, JsonSerializerOptions jsonOptions return CommandExitCodes.UsageError; if (TryWriteParseError(options, "callers")) return CommandExitCodes.UsageError; + if (TryRejectMetadataKindForGraphCommand("callers", options.Kind)) + return CommandExitCodes.UsageError; if (!TryResolveNameExactMode(options, "callers", out var exact, out var exactError)) { Console.Error.WriteLine(exactError); @@ -486,6 +488,8 @@ public static int RunCallees(string[] cmdArgs, JsonSerializerOptions jsonOptions return CommandExitCodes.UsageError; if (TryWriteParseError(options, "callees")) return CommandExitCodes.UsageError; + if (TryRejectMetadataKindForGraphCommand("callees", options.Kind)) + return CommandExitCodes.UsageError; if (!TryResolveNameExactMode(options, "callees", out var exact, out var exactError)) { Console.Error.WriteLine(exactError); @@ -2990,6 +2994,44 @@ private static void WriteGraphReferenceKindHint(string command, string? kind, bo Console.Error.WriteLine($"Hint: '{kind}' is not a known reference kind for '{command}'. Available reference kinds: {string.Join(", ", AllValidReferenceKinds)}"); } + // Reference kinds that are valid `references --kind` values but NOT valid + // `callers --kind` / `callees --kind` values. A metadata row (`attribute` / + // `annotation`) is attributed to its enclosing body-range symbol rather than + // to the annotated target itself, so `callers Obsolete --kind attribute` and + // equivalent `callees` queries return structurally wrong answers: method-level + // metadata is reported under the enclosing class, and file-level targets + // like `[assembly: ...]` drop entirely because `container_name` is null. + // Reject these kinds at the CLI boundary and redirect users to + // `references --kind attribute|annotation` (which IS correct). + // `references --kind` では有効だが、`callers --kind` / `callees --kind` では + // 使ってはいけない reference kind。metadata 行は注釈対象そのものではなく + // body-range 上の外側シンボルに帰属するため、`callers` / `callees` でこの kind を + // 受け付けると構造的に誤答する(メソッドレベルは外側クラスに寄り、 + // `[assembly: ...]` のようなファイルレベルは `container_name = null` で丸ごと消える)。 + // CLI 境界で弾き、正しい列挙パスである `references --kind attribute|annotation` に誘導する。 + private static readonly HashSet MetadataReferenceKinds = new(StringComparer.Ordinal) + { + "attribute", "annotation", + }; + + /// + /// Reject `--kind attribute` / `--kind annotation` on commands (`callers` / `callees`) + /// whose data model cannot answer metadata questions correctly. Returns true if the + /// kind was rejected; the caller should then return `CommandExitCodes.UsageError`. + /// `callers` / `callees` のようにデータモデル的に metadata に答えられないコマンドで + /// `--kind attribute` / `--kind annotation` を弾く。弾いた場合 true を返すので、 + /// 呼び出し側は `CommandExitCodes.UsageError` を返すこと。 + /// + private static bool TryRejectMetadataKindForGraphCommand(string command, string? kind) + { + if (string.IsNullOrWhiteSpace(kind) || !MetadataReferenceKinds.Contains(kind)) + return false; + + Console.Error.WriteLine($"Error: '--kind {kind}' is not supported on '{command}'. Metadata references are attributed to the enclosing body-range symbol rather than the annotated target, so `{command} --kind {kind}` cannot return accurate rows (file-level targets such as `[assembly: ...]` drop entirely)."); + Console.Error.WriteLine($"Hint: use `cdidx references --kind {kind}` for metadata enumeration instead."); + return true; + } + private static void WriteGraphSupportHint(string? lang) { if (lang != null && !ReferenceExtractor.SupportsLanguage(lang)) diff --git a/src/CodeIndex/Database/DbReader.cs b/src/CodeIndex/Database/DbReader.cs index 34201ab1a7..ef03a5a1d6 100644 --- a/src/CodeIndex/Database/DbReader.cs +++ b/src/CodeIndex/Database/DbReader.cs @@ -1666,7 +1666,16 @@ FROM symbols s " + GetLogicalReferenceKindSql("r.reference_kind") + @" AS logical_reference_kind FROM symbol_references r JOIN files src ON r.file_id = src.id - WHERE src.path != @impactTargetPath"; + WHERE src.path != @impactTargetPath + AND r.reference_kind IN " + CallGraphReferenceKindsSql; + // Metadata reference kinds (`attribute`, `annotation`) only touch a type/symbol + // as compile-time metadata and should NOT form a logical file dependency edge + // (e.g. `[JsonConverter(typeof(User))]` or `@Inject(User.class)` must not make + // the annotated file depend on `User`). Restrict to call-graph kinds. + // `attribute` / `annotation` は compile-time metadata で型や記号に触れるだけなので、 + // ファイル間の論理的な依存 edge として扱ってはいけない(例: + // `[JsonConverter(typeof(User))]` / `@Inject(User.class)` で annotated ファイルが + // `User` に依存しているかのように見える)。call-graph kind に絞る。 innerSql += $" AND {BuildGraphSupportedLanguagePredicate(cmd, "src", "impactDepsLang")}"; if (lang != null) innerSql += " AND src.lang = @lang"; @@ -2317,7 +2326,13 @@ WITH logical_references AS ( " + GetLogicalReferenceKindSql("r.reference_kind") + @" AS logical_reference_kind FROM symbol_references r JOIN files src ON r.file_id = src.id - WHERE 1 = 1"; + WHERE 1 = 1 + AND r.reference_kind IN " + CallGraphReferenceKindsSql; + // Same rationale as GetFileDependencyHintsToResolvedType: metadata kinds + // (`attribute` / `annotation`) must not form dependency edges because they + // only record compile-time metadata usage, not logical runtime dependencies. + // `GetFileDependencyHintsToResolvedType` と同じ理由で、`attribute` / `annotation` + // は実行時の論理依存ではないため、file-level dependency edge を作らない。 sql += $" AND {BuildGraphSupportedLanguagePredicate(cmd, "src", "depsLang")}"; if (lang != null) sql += " AND src.lang = @lang"; diff --git a/src/CodeIndex/Indexer/ReferenceExtractor.cs b/src/CodeIndex/Indexer/ReferenceExtractor.cs index f3b3ca93b6..58a7a115c2 100644 --- a/src/CodeIndex/Indexer/ReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/ReferenceExtractor.cs @@ -162,13 +162,20 @@ public static class ReferenceExtractor @"(?[A-Za-z_]\w*)\b(?!\s*[.(])", RegexOptions.Compiled); - // Languages whose `@Decorator(args)` / `@Annotation(args)` syntax should produce - // `annotation` reference rows rather than `call` rows (issue #293). - // `@Decorator(args)` / `@Annotation(args)` 構文を `call` ではなく `annotation` として - // 記録すべき言語 (issue #293)。 + // Languages whose `@Decorator(args)` / `@Annotation(args)` / `@Attribute(args)` syntax + // should produce `annotation` reference rows rather than `call` rows (issue #293). + // Swift uses `@available(...)`, `@objc`, `@MainActor`, etc. as compile-time metadata; + // Gradle/Groovy uses `@CompileStatic`, `@TaskAction`, etc. the same way. Without this + // reclassification, `callers` / `callees` / `hotspots` / `impact` on those languages + // get polluted with metadata edges. + // `@Decorator(args)` / `@Annotation(args)` / `@Attribute(args)` を `call` ではなく + // `annotation` として記録すべき言語 (issue #293)。Swift の `@available(...)` / `@objc` / + // `@MainActor` や、Gradle/Groovy の `@CompileStatic` / `@TaskAction` も compile-time + // metadata なので同じ扱いにする。再分類しないと `callers` / `callees` / `hotspots` / + // `impact` に metadata edge が混入する。 private static readonly HashSet AnnotationLanguages = new(StringComparer.Ordinal) { - "java", "kotlin", "scala", "typescript", "javascript", + "java", "kotlin", "scala", "typescript", "javascript", "swift", "gradle", }; // Kotlin use-site target prefixes for annotations (e.g. `@field:Deprecated("msg")`, diff --git a/src/CodeIndex/Mcp/McpToolHandlers.cs b/src/CodeIndex/Mcp/McpToolHandlers.cs index a8b4d4cff3..221d678061 100644 --- a/src/CodeIndex/Mcp/McpToolHandlers.cs +++ b/src/CodeIndex/Mcp/McpToolHandlers.cs @@ -90,6 +90,21 @@ private static void AddExactZeroHint(JsonObject payload, ExactZeroHintResult? ex /// private static int ClampLimit(int limit) => Math.Clamp(limit, 1, MaxLimit); + /// + /// Return true when the requested reference kind is a metadata kind (`attribute` / + /// `annotation`) — these are valid on the `references` tool but must be rejected on + /// `callers` / `callees`, whose data model cannot answer metadata questions correctly + /// (metadata rows are attributed to the enclosing body-range symbol rather than the + /// annotated target, so file-level targets drop entirely and method-level metadata + /// appears under the enclosing class). + /// `references` では有効だが `callers` / `callees` では構造的に誤答するため弾くべき + /// metadata kind (`attribute` / `annotation`) かを返す。metadata 行は注釈対象ではなく + /// body-range 上の外側シンボルに帰属するため、`callers` / `callees` はこの kind に + /// 正しく答えられない。 + /// + private static bool IsMetadataReferenceKind(string? kind) => + kind == "attribute" || kind == "annotation"; + private JsonNode? TryGetValidatedMaxLineWidth(JsonNode? id, JsonNode? args, out int maxLineWidth, string propertyName = "maxLineWidth") { var maxLineWidthValue = args?[propertyName]?.GetValue(); @@ -491,6 +506,8 @@ private JsonNode ExecuteCallers(JsonNode? id, JsonNode? args) return CreateToolErrorResponse(id, $"Query too long (max {MaxQueryLength} characters)"); var kind = args?["kind"]?.GetValue(); + if (IsMetadataReferenceKind(kind)) + return CreateToolErrorResponse(id, $"'kind: {kind}' is not supported on 'callers'. Metadata references are attributed to the enclosing body-range symbol, so `callers` cannot return accurate rows for kind '{kind}'. Use the 'references' tool with kind '{kind}' for metadata enumeration."); var lang = args?["lang"]?.GetValue(); var limit = ClampLimit(args?["limit"]?.GetValue() ?? 20); var pathPatterns = ReadPathList(args, "path"); @@ -545,6 +562,8 @@ private JsonNode ExecuteCallees(JsonNode? id, JsonNode? args) return CreateToolErrorResponse(id, $"Query too long (max {MaxQueryLength} characters)"); var kind = args?["kind"]?.GetValue(); + if (IsMetadataReferenceKind(kind)) + return CreateToolErrorResponse(id, $"'kind: {kind}' is not supported on 'callees'. Metadata references are attributed to the enclosing body-range symbol, so `callees` cannot return accurate rows for kind '{kind}'. Use the 'references' tool with kind '{kind}' for metadata enumeration."); var lang = args?["lang"]?.GetValue(); var limit = ClampLimit(args?["limit"]?.GetValue() ?? 20); var pathPatterns = ReadPathList(args, "path"); diff --git a/tests/CodeIndex.Tests/DbReaderTests.cs b/tests/CodeIndex.Tests/DbReaderTests.cs index 1348fa8fb6..881d1bf05e 100644 --- a/tests/CodeIndex.Tests/DbReaderTests.cs +++ b/tests/CodeIndex.Tests/DbReaderTests.cs @@ -2249,6 +2249,68 @@ def Run(): Assert.DoesNotContain("foo.py", dependency.TargetPath, StringComparison.Ordinal); } + [Fact] + public void GetFileDependencies_ExcludesMetadataOnlyReferences() + { + // issue #293 follow-up: `[JsonConverter(typeof(User))]` touches `User` only as + // compile-time metadata and must NOT create a dependency edge from the annotated + // file to `User.cs`. Before the fix, `GetFileDependencies` filtered by graph-supported + // language but not by `reference_kind`, so metadata-only references (attribute/annotation) + // leaked into `cdidx deps` output. + // issue #293 補足: `[JsonConverter(typeof(User))]` は compile-time metadata として + // `User` に触れるだけなので、注釈ファイルから `User.cs` への依存 edge を作ってはいけない。 + // 修正前は `GetFileDependencies` が graph-supported 言語しか絞っておらず + // `reference_kind` で絞っていなかったため、metadata-only 参照 (attribute/annotation) が + // `cdidx deps` 出力に漏れていた。 + InsertIndexedFile("src/User.cs", "csharp", + """ + public class User + { + public string Name { get; set; } = ""; + } + """); + // Metadata-only usage — attribute typeof(User), no real runtime dependency. + // metadata-only の利用 (attribute typeof(User))。実行時の依存関係は無い。 + InsertIndexedFile("src/Serializer.cs", "csharp", + """ + using System; + + [AttributeUsage(AttributeTargets.Class)] + public class JsonConverterAttribute : Attribute + { + public Type ConverterType { get; } + public JsonConverterAttribute(Type converterType) => ConverterType = converterType; + } + + [JsonConverter(typeof(User))] + public class SerializerConfig + { + } + """); + // Real runtime dependency — `new User()` is a `call`/`instantiate` kind edge. + // 実際の実行時依存 — `new User()` は `call`/`instantiate` 種別の edge。 + InsertIndexedFile("src/Caller.cs", "csharp", + """ + public class Caller + { + public void Do() + { + var u = new User(); + System.Console.WriteLine(u.Name); + } + } + """); + + var dependencies = _reader.GetFileDependencies(limit: 10, lang: "csharp"); + + // Only Caller.cs → User.cs via `new User()` should remain. + // `new User()` 経由の Caller.cs → User.cs のみが残る。 + Assert.Single(dependencies); + Assert.Equal("src/Caller.cs", dependencies[0].SourcePath); + Assert.Equal("src/User.cs", dependencies[0].TargetPath); + Assert.DoesNotContain(dependencies, d => d.SourcePath == "src/Serializer.cs"); + } + [Fact] public void GetGroupedSymbolHotspots_CollapsesDuplicateNamesWithoutBareJoinInflation() { @@ -2519,6 +2581,69 @@ public void Run(FolderDiffService service) Assert.Contains("ExecuteFolderDiffAsync", edge.Symbols); } + [Fact] + public void AnalyzeImpact_ExcludesMetadataOnlyReferencesFromHeuristicHints() + { + // issue #293 follow-up: when `impact` falls back to file-dependency hints + // (heuristic mode), metadata-only references must not produce phantom edges. + // `[JsonConverter(typeof(User))]` touches `User` only as attribute metadata + // and must not make the annotated file show up as a file-level impact of `User`. + // Before the fix, `GetFileDependencyHintsToResolvedType` did not filter by + // `reference_kind` so attribute/annotation rows leaked into the hint output. + // issue #293 補足: `impact` が file-dependency hints にフォールバックする heuristic mode でも + // metadata-only 参照は phantom edge を作ってはいけない。`[JsonConverter(typeof(User))]` は + // attribute metadata として `User` に触れるだけで、注釈ファイルを `User` の file-level + // impact として表示してはいけない。修正前は `GetFileDependencyHintsToResolvedType` が + // `reference_kind` で絞っておらず attribute/annotation 行が hint 出力に漏れていた。 + InsertIndexedFile("src/MetadataUser.cs", "csharp", + """ + public class MetadataUser + { + public string Name { get; set; } = ""; + } + """); + // Attribute uses MetadataUser via typeof — metadata only, no runtime dep. + // No other file references MetadataUser, so there are no call-graph callers and + // the impact analysis falls through to the heuristic file-dependency-hint mode. + // Before the fix, the attribute-only reference would leak into that hint output; + // after the fix, it must be filtered out and hint count must be 0. + // 属性が typeof で MetadataUser に触れるだけ。実行時の依存は無い。他のファイルからは + // 参照していないため call-graph caller は 0 件となり、impact 分析は heuristic な + // file-dependency-hint モードに落ちる。修正前はこの hint に attribute-only 参照が + // 漏れていたが、修正後はフィルタで除外され hint 件数は 0 になる。 + InsertIndexedFile("src/AttributeConsumer.cs", "csharp", + """ + using System; + + [AttributeUsage(AttributeTargets.Class)] + public class MetadataAttribute : Attribute + { + public Type T { get; } + public MetadataAttribute(Type t) => T = t; + } + + [Metadata(typeof(MetadataUser))] + public class AttributeConsumer + { + } + """); + + var analysis = _reader.AnalyzeImpact("MetadataUser", maxDepth: 3, limit: 10); + + // With no real runtime caller and the attribute-only reference filtered out by the + // call-graph reference_kind predicate, there are 0 callers AND 0 heuristic file + // hints. Before the fix, the attribute row would leak into the hint output and the + // attribute-only consumer file would show up as a phantom dependency. + // 実行時 caller が無く、attribute-only 参照も call-graph reference_kind 述語で + // 除外されるため、caller は 0 件 / heuristic file hint も 0 件。修正前は attribute + // 行が hint 出力に漏れ、attribute-only consumer が phantom 依存として出ていた。 + Assert.True(analysis.HasClassLikeDefinitions); + Assert.Empty(analysis.Callers); + Assert.DoesNotContain(analysis.FileImpacts, e => e.SourcePath == "src/AttributeConsumer.cs"); + Assert.Empty(analysis.FileImpacts); + Assert.Equal(0, analysis.HintCount); + } + [Fact] public void AnalyzeImpact_ClassAndNamespaceWithSameName_StillReturnsHeuristicHints() { diff --git a/tests/CodeIndex.Tests/McpServerTests.cs b/tests/CodeIndex.Tests/McpServerTests.cs index a91cf6b4f6..f551190852 100644 --- a/tests/CodeIndex.Tests/McpServerTests.cs +++ b/tests/CodeIndex.Tests/McpServerTests.cs @@ -790,6 +790,36 @@ public void ToolsCall_Callees_UnsupportedLanguage_ReturnsGraphSupportHint() Assert.Contains("not indexed", response["result"]!["structuredContent"]!["graphSupportReason"]!.GetValue()); } + [Theory] + [InlineData("callers", "attribute")] + [InlineData("callers", "annotation")] + [InlineData("callees", "attribute")] + [InlineData("callees", "annotation")] + public void ToolsCall_CallersOrCallees_MetadataKindReturnsToolError(string tool, string kind) + { + // issue #293 follow-up: the MCP `callers` / `callees` tools must reject `kind: + // attribute` / `kind: annotation` because metadata rows are attributed to the + // enclosing body-range symbol (so `callers Obsolete kind=attribute` reports the + // enclosing class instead of the annotated method, and file-level targets drop + // entirely). AI clients should be redirected to the `references` tool for metadata + // enumeration. + // issue #293 補足: MCP の `callers` / `callees` ツールは `kind: attribute` / + // `kind: annotation` を必ず弾くこと。metadata 行は body-range の外側シンボルに帰属する + // ため、`callers Obsolete kind=attribute` は注釈対象のメソッドではなく外側クラスを返し、 + // file-level target は完全に脱落する。AI クライアントは metadata 列挙のために + // `references` ツールに誘導する。 + var requestJson = "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\"," + + "\"params\":{\"name\":\"" + tool + "\"," + + "\"arguments\":{\"query\":\"SomeSymbol\",\"kind\":\"" + kind + "\"}}}"; + var request = JsonNode.Parse(requestJson)!; + var response = _server.HandleMessage(request)!; + + Assert.True(response["result"]!["isError"]!.GetValue()); + var text = response["result"]!["content"]![0]!["text"]!.GetValue(); + Assert.Contains($"'kind: {kind}' is not supported on '{tool}'", text); + Assert.Contains("'references' tool", text); + } + [Fact] public void ToolsCall_ImpactAnalysis_ClassSymbolReturnsHeuristicFileDependencyHints() { diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs index 638df3a264..d6d523781c 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs @@ -1857,6 +1857,49 @@ public string Render() } } + [Theory] + [InlineData("callers", "attribute")] + [InlineData("callers", "annotation")] + [InlineData("callees", "attribute")] + [InlineData("callees", "annotation")] + public void RunCallersCallees_RejectMetadataKind_WithUsageError(string command, string kind) + { + // issue #293 follow-up: `callers` / `callees` must reject `--kind attribute` and + // `--kind annotation` at the CLI boundary. Metadata references are attributed to the + // enclosing body-range symbol rather than the annotated target, so `callers Obsolete + // --kind attribute` would return `[Obsolete] void M()` under the enclosing class + // instead of `M`, and file-level targets like `[assembly: Foo]` drop entirely because + // `container_name` is NULL. The correct path for metadata enumeration is + // `references --kind attribute|annotation`. + // issue #293 補足: `callers` / `callees` は CLI 境界で `--kind attribute` / + // `--kind annotation` を必ず弾かなければならない。metadata 参照は注釈対象ではなく + // body-range の外側シンボルに帰属するため、`callers Obsolete --kind attribute` では + // `[Obsolete] void M()` が `M` ではなく外側クラスに寄り、`[assembly: Foo]` のような + // file-level target は `container_name = NULL` で完全に脱落する。metadata 列挙の + // 正しい経路は `references --kind attribute|annotation`。 + var projectRoot = TestProjectHelper.CreateTempProject($"cdidx_{command}_reject_kind_{kind}"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + var args = new[] { "Symbol", "--db", dbPath, "--kind", kind }; + + var (exitCode, _, stderr) = command switch + { + "callers" => CaptureConsole(() => QueryCommandRunner.RunCallers(args, _jsonOptions)), + "callees" => CaptureConsole(() => QueryCommandRunner.RunCallees(args, _jsonOptions)), + _ => throw new InvalidOperationException($"Unexpected command: {command}") + }; + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains($"'--kind {kind}' is not supported on '{command}'", stderr); + Assert.Contains($"references --kind {kind}", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + [Fact] public void RunCallers_JsonZeroResults_WithMissingGraphTable_ReturnsDegradedPayload() { diff --git a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs index 9e48d4426c..6e6b80bbc1 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs @@ -1194,6 +1194,77 @@ fun old() {} Assert.Equal("annotation", deprecated.ReferenceKind); } + [Fact] + public void Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation() + { + // issue #293 follow-up: Swift `@available(...)` / `@objc` / `@MainActor` are + // compile-time metadata, not runtime calls. Before the fix they were recorded + // as `call` references (polluting `callers`/`callees`/`hotspots`/`impact`) and + // `@objc` / `@MainActor` no-arg attributes dropped entirely from the index. + // After the fix they must all classify as `annotation`. + // issue #293 補足: Swift の `@available(...)` / `@objc` / `@MainActor` は compile-time + // metadata であり runtime の call ではない。修正前は `call` として記録され + // (`callers`/`callees`/`hotspots`/`impact` が汚染)、`@objc` / `@MainActor` の no-arg + // 版はインデックスから完全に脱落していた。修正後はすべて `annotation` として分類される。 + const string content = """ + import Foundation + + @available(iOS 13.0, *) + class NetworkClient { + @objc func fetch() {} + + @MainActor + func process() {} + } + """; + + var symbols = SymbolExtractor.Extract(1, "swift", content); + var references = ReferenceExtractor.Extract(1, "swift", content, symbols); + + var available = Assert.Single(references.Where(r => r.SymbolName == "available")); + Assert.Equal("annotation", available.ReferenceKind); + + var objc = Assert.Single(references.Where(r => r.SymbolName == "objc")); + Assert.Equal("annotation", objc.ReferenceKind); + + var mainActor = Assert.Single(references.Where(r => r.SymbolName == "MainActor")); + Assert.Equal("annotation", mainActor.ReferenceKind); + } + + [Fact] + public void Extract_GradleAnnotation_ClassifiedAsAnnotation() + { + // issue #293 follow-up: Gradle/Groovy `@CompileStatic` / `@TaskAction` and similar + // transform/task annotations are compile-time metadata. Before the fix they were + // recorded as `call` references (or dropped for the no-arg form), which made + // `callers TaskAction` / `callees` pick up fake graph edges in build scripts. + // After the fix they must all classify as `annotation`. + // issue #293 補足: Gradle/Groovy の `@CompileStatic` / `@TaskAction` なども compile-time + // metadata。修正前は `call` として記録されるか no-arg 版が脱落し、ビルドスクリプトで + // `callers TaskAction` / `callees` に偽のグラフエッジが混入していた。修正後はすべて + // `annotation` として分類される。 + const string content = """ + import groovy.transform.CompileStatic + + @CompileStatic + class BuildConfig { + @TaskAction + void run() { + println "built" + } + } + """; + + var symbols = SymbolExtractor.Extract(1, "gradle", content); + var references = ReferenceExtractor.Extract(1, "gradle", content, symbols); + + var compileStatic = Assert.Single(references.Where(r => r.SymbolName == "CompileStatic")); + Assert.Equal("annotation", compileStatic.ReferenceKind); + + var taskAction = Assert.Single(references.Where(r => r.SymbolName == "TaskAction")); + Assert.Equal("annotation", taskAction.ReferenceKind); + } + [Fact] public void Extract_JavaMethodBodyCall_StaysCall() { From b9b2534e244032472c6cdaa8e8ef5efbc6e5bbed Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 18 Apr 2026 06:09:52 +0900 Subject: [PATCH 13/30] Fix #293 align callers/callees kind schema with handler rejection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The callers/callees handlers reject `kind: "attribute"` / `kind: "annotation"` as a usage error (CLI) / isError tool response (MCP), but the MCP tool schema still listed all five kinds in the `kind` property description as if they were valid filter values. The CLI `--help` `--kind` row similarly lumped references/callers/callees together for the full five-kind list. Update the MCP callers/callees `kind` description to list only the call-graph kinds and direct clients to `references` for metadata enumeration, split the CLI `--kind` help row so `references` (accepts all five kinds) is distinguished from `callers`/`callees` (call-graph only, metadata rejected), and add `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` to pin the corrected schema wording. The MCP `references` tool schema is unchanged because it still accepts every indexed kind. The `references` tool at line 77 of McpToolDefinitions.cs keeps the original wording since all five kinds remain valid there. Test plan: - dotnet test --no-build — 1566 passed, 2 skipped, 0 failed. - Manual end-to-end with /tmp/cdidx-293-indexer fixture confirms that multi-line C# indexer declarations (`public int this[\n int i\n] => 0;`, block-body form, generic form, `ref readonly` form) are still extracted correctly, contradicting the adversarial review's theoretical claim that `StripMultiLineCSharpAttributeInterior` breaks indexer symbol extraction. --- CHANGELOG.md | 2 ++ src/CodeIndex/Cli/ConsoleUi.cs | 2 +- src/CodeIndex/Mcp/McpToolDefinitions.cs | 4 ++-- tests/CodeIndex.Tests/ConsoleUiTests.cs | 2 +- tests/CodeIndex.Tests/McpServerTests.cs | 24 ++++++++++++++++++++++++ 5 files changed, 30 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 221b9f7ce8..8f74616b0c 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/), - **Developer Guide now distinguishes SQLite's role from "zero dependencies" and labels release numbers as examples** — Clarified that cdidx remains a zero-configuration, single-file CLI with exactly one production dependency (`Microsoft.Data.Sqlite`), and made the release-workflow checklist explicitly state that `1.9.0` is only an example version to substitute during an actual release. Affected: `DEVELOPER_GUIDE.md`. #### Fixed +- **MCP `callers` / `callees` `kind` schema and CLI help no longer advertise metadata kinds as valid filter values (#293 follow-up)** — The `callers` / `callees` handlers reject `kind: "attribute"` / `kind: "annotation"` with a usage error (CLI) / `isError` tool response (MCP), but the MCP tool schema's `kind` property description still listed all five kinds as if they were valid filter values (`"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"`), and `cdidx --help` similarly lumped `callers` / `callees` together with `references` for the `--kind` help row. Schema and handler now agree: the `callers` / `callees` `kind` description now reads `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."`, and the CLI `--kind` help row splits `references` (accepts all five kinds) from `callers` / `callees` (call-graph kinds only, metadata rejected, redirects to `references`). The MCP `references` tool description is unchanged because it still accepts every indexed kind. Added `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` to pin the corrected schema wording. Affected: `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `src/CodeIndex/Cli/ConsoleUi.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`. - **`deps` and `impact` file-level dependency hints now exclude metadata-only references (#293 follow-up)** — `DbReader.GetFileDependencies` (backing `cdidx deps`) and `DbReader.GetFileDependencyHintsToResolvedType` (backing `cdidx impact` / MCP `impact_analysis` heuristic file-hint fallback) used to project every `symbol_references` row into the logical reference CTE, filtering only by graph-supported language. Metadata-only usages such as `[JsonConverter(typeof(User))]` or `@Inject(User.class)` — which touch a type at compile time but do not create a runtime dependency — therefore leaked into dependency output and made the annotated file look like it depended on the target type. Both queries now additionally gate the CTE on `r.reference_kind IN CallGraphReferenceKindsSql ('call', 'instantiate', 'subscribe')`, matching the contract already enforced by `callers` / `callees` / `hotspots` / `impact` BFS. Added regression coverage in `DbReaderTests`: `GetFileDependencies_ExcludesMetadataOnlyReferences` pins that the attribute-only consumer does not create a `deps` edge while `new User()` still does, and `AnalyzeImpact_ExcludesMetadataOnlyReferencesFromHeuristicHints` pins that an attribute-only consumer produces zero heuristic file hints. Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. - **`callers` / `callees` now reject `--kind attribute|annotation` at the CLI and MCP boundary, and Swift / Gradle `@` metadata is reclassified as `annotation` (#293 follow-up)** — Metadata rows (`attribute` / `annotation`) are attributed to their enclosing body-range symbol rather than the annotated target, so `callers Obsolete --kind attribute` would silently return `[Obsolete] void M()` under the enclosing class (not `M`), and file-level targets such as `[assembly: CLSCompliant]` or Kotlin `@file:JvmName` would drop entirely because `container_name` is `NULL`. `QueryCommandRunner.RunCallers` / `RunCallees` now reject `--kind attribute` and `--kind annotation` with `CommandExitCodes.UsageError`, and the MCP `callers` / `callees` tools return an `isError` tool response; both paths direct users to `references --kind attribute|annotation`, which IS a correct metadata enumeration path. Separately, `AnnotationLanguages` now includes `swift` and `gradle`, so Swift `@available(iOS 13.0, *)` / `@objc` / `@MainActor` and Gradle/Groovy `@CompileStatic` / `@TaskAction` are recorded as `annotation` instead of `call` (previously the `@Name(args)` forms leaked into `callers` / `callees` / `hotspots` / `impact` as phantom call edges, and the no-arg `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` forms dropped from the index entirely because no-arg annotation emission was gated to languages in this set). Added `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError` (theory, 4 cases), `McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError` (theory, 4 cases), `ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`, and `ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation`. Affected: `src/CodeIndex/Cli/QueryCommandRunner.cs`, `src/CodeIndex/Mcp/McpToolHandlers.cs`, `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. - **Multi-line no-arg C# attribute sections like `[\n Serializable\n]` are now indexed without over-matching attribute argument identifiers (#293 follow-up)** — The no-arg C# attribute regex previously required the match to start at a `[` / `,` boundary and end at `]` / `,` on the same line, so a bare identifier on the interior line of a multi-line attribute section (e.g. ` Serializable` between a line-leading `[` and a line-leading `]`) was silently dropped from `references --kind attribute`. The left anchor is now a word-boundary lookbehind and the right anchor also accepts end-of-line. To avoid misclassifying enum / qualified-constant identifiers that happen to sit at end-of-line inside an attribute argument list (e.g. `ConverterStrategy.AllowNumbers` in `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]`) as no-arg attributes, `BuildCSharpAttributeRanges` now also emits a parallel "top-level zone" table that splits each attribute section by `(` / `)` so only paren-depth-0 positions inside the section qualify as no-arg attribute name positions. Paren depth is tracked section-locally (each `[` snapshots the global depth at open time) so parameter attributes like `void M([FromServices] IService s)`, delegate parameter attributes like `delegate void D([Attr] int x)`, and lambda parameter attributes like `Func f = ([Attr] int x) => x` — whose `[` opens while the global paren depth is already > 0 — still have their attribute-list top level correctly identified. The no-arg attribute path uses that top-level table instead of the full attribute-section ranges, while the existing with-args metadata classification still uses the full ranges. `SymbolExtractor` also now tracks multi-line `[...]` depth across lines via `StripMultiLineCSharpAttributeInterior` in `BuildCSharpMatchLines`, so interior lines of a multi-line attribute section are blanked out before declaration regexes run — this prevents ` Serializable` from being mis-extracted as a top-level `function` symbol (which then masked the reference through the `definitionNames` guard). The multi-line blanker now activates on any `[` that opens without a matching `]` on the same line, not just `[` that sits at the start of the line, so multi-line parameter attributes like `void M([\n FromServices\n] IService s)`, type-parameter attributes like `class Bar<[\n TypeParamAttr\n] T>`, and delegate/lambda parameter attributes like `delegate void D([\n DelegateParamAttr\n] int x)` are also blanked out correctly — previously only the leading-`[` case was covered, so the interior identifiers of those forms were mis-extracted as `function` symbols and their attribute references were silently dropped from `references --kind attribute`. Regression coverage adds `[\n Serializable\n]`, `[\n global::System.Obsolete\n]`, `[Required,\n Key]`, `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]` (pinning that `AllowNumbers` / `ConverterStrategy` are NOT classified as `attribute`), and the non-leading-`[` parameter / type-parameter / delegate-parameter multi-line cases above, running through `SymbolExtractor.Extract` + `ReferenceExtractor.Extract` end-to-end so both sides of the regression stay pinned. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Indexer/SymbolExtractor.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. @@ -684,6 +685,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **DEVELOPER_GUIDE が SQLite の位置付けとリリース番号の例示を明確化** — cdidx が「設定ゼロ・単一ファイル・本番依存1個」の CLI であり、その唯一の本番依存が `Microsoft.Data.Sqlite` であることを明記した。あわせて、リリース手順のチェックリスト中に出てくる `1.9.0` は固定値ではなく例示であり、実際のリリース番号へ読み替える前提だと明示した。対象: `DEVELOPER_GUIDE.md`。 #### 修正 +- **MCP の `callers` / `callees` の `kind` schema と CLI help が metadata kinds を有効フィルタ値として広告しないよう修正 (#293 追加対応)** — `callers` / `callees` の handler は `kind: "attribute"` / `kind: "annotation"` を usage error(CLI)/ `isError` ツールレスポンス(MCP)で拒否するにもかかわらず、MCP ツール schema の `kind` プロパティ説明は 5 種すべてを列挙した `"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"` のままで、`cdidx --help` の `--kind` 行も `callers` / `callees` を `references` と同じ行にまとめていた。schema と handler の契約を一致させ、`callers` / `callees` の `kind` description を `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."` に修正し、CLI の `--kind` help 行も `references`(5 種すべて受理)と `callers` / `callees`(call-graph 種別のみ、metadata kinds は拒否して `references` へ誘導)を分けるようにした。MCP の `references` ツール説明は全 reference kind を受け付ける契約のままなので変更なし。修正後の schema 文言を固定する `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` を追加。対象: `src/CodeIndex/Mcp/McpToolDefinitions.cs`、`src/CodeIndex/Cli/ConsoleUi.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`。 - **`deps` と `impact` のファイルレベル依存ヒントから metadata-only 参照を除外 (#293 追加対応)** — `cdidx deps` の実装である `DbReader.GetFileDependencies` と、`cdidx impact` / MCP `impact_analysis` の heuristic file-hint フォールバックである `DbReader.GetFileDependencyHintsToResolvedType` が、これまで `symbol_references` を graph 対応言語でしか絞っておらず `reference_kind` で絞っていなかったため、`[JsonConverter(typeof(User))]` や `@Inject(User.class)` のような compile-time metadata として型に触れるだけで実行時依存ではない参照が依存エッジとして漏れていた(注釈ファイルが対象型に依存しているように見えた)。両クエリの logical reference CTE を `r.reference_kind IN CallGraphReferenceKindsSql ('call', 'instantiate', 'subscribe')` で追加絞り込みし、`callers` / `callees` / `hotspots` / `impact` BFS と同じ契約に揃えた。回帰テストを `DbReaderTests` に追加: `GetFileDependencies_ExcludesMetadataOnlyReferences` は attribute-only consumer が `deps` エッジを作らず `new User()` は作ることを固定し、`AnalyzeImpact_ExcludesMetadataOnlyReferencesFromHeuristicHints` は attribute-only consumer が heuristic file hint を 1 件も生成しないことを固定する。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 - **`callers` / `callees` が CLI と MCP 境界で `--kind attribute|annotation` を拒否し、Swift / Gradle の `@` metadata を `annotation` に再分類 (#293 追加対応)** — metadata 行 (`attribute` / `annotation`) は注釈対象そのものではなく body-range の外側シンボルに帰属するため、`callers Obsolete --kind attribute` は `[Obsolete] void M()` を `M` ではなく外側クラスの下に黙って返し、`[assembly: CLSCompliant]` や Kotlin の `@file:JvmName` のような file-level target は `container_name = NULL` で完全脱落していた。`QueryCommandRunner.RunCallers` / `RunCallees` は `--kind attribute` / `--kind annotation` を `CommandExitCodes.UsageError` で弾き、MCP の `callers` / `callees` ツールは `isError` ツールレスポンスを返すようにした。どちらの経路も、metadata 列挙の正しいパスである `references --kind attribute|annotation` へ誘導する。あわせて `AnnotationLanguages` に `swift` と `gradle` を追加し、Swift の `@available(iOS 13.0, *)` / `@objc` / `@MainActor` や Gradle / Groovy の `@CompileStatic` / `@TaskAction` を `call` ではなく `annotation` として記録するようにした(修正前は引数付きの `@Name(args)` 形は `callers` / `callees` / `hotspots` / `impact` に phantom call edge として漏れ、引数なしの `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` 形はこの集合で gate されていた no-arg annotation 出力から外れてインデックス自体に残らなかった)。回帰テストとして `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError`(theory 4 ケース)、`McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError`(theory 4 ケース)、`ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`、`ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation` を追加。対象: `src/CodeIndex/Cli/QueryCommandRunner.cs`、`src/CodeIndex/Mcp/McpToolHandlers.cs`、`src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 - **`[\n Serializable\n]` のような複数行にまたがる引数なし C# 属性セクションを、属性引数内の識別子を誤って取り込まずに索引化 (#293 追加対応)** — 引数なし C# 属性の正規表現は、これまで `[` / `,` を左端、`]` / `,` を右端として同一行内でアンカーしていたため、行頭 `[` と行頭 `]` の間に書かれた ` Serializable` のような裸識別子が `references --kind attribute` から黙って脱落していた。左側は単語境界 lookbehind、右側は行末(`$`)も許容するように緩和した。一方で `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]` のように属性の引数リスト内で行末に現れる enum / 修飾定数識別子(`AllowNumbers` など)が no-arg 属性として誤分類される回帰を避けるため、`BuildCSharpAttributeRanges` は `(` / `)` で属性セクションを分割した paren 深さ 0 の「top-level ゾーン」テーブルも並列で出力するようにし、no-arg 属性の採用位置はそのテーブルに限定した(既存の引数付き metadata 分類パスは引き続き属性セクション全体のレンジを使う)。paren 深さは section-local に追跡する(各 `[` がその瞬間のグローバル paren 深さを snapshot として保持)ため、`void M([FromServices] IService s)` のようなパラメータ属性、`delegate void D([Attr] int x)` のようなデリゲートパラメータ属性、`Func f = ([Attr] int x) => x` のようなラムダパラメータ属性のように、`[` が開いた時点で既にグローバル paren 深さが 0 より大きいケースでも、属性リストの top-level を正しく認識できる。`SymbolExtractor` 側でも `BuildCSharpMatchLines` に `StripMultiLineCSharpAttributeInterior` を追加し、複数行にまたがる `[...]` セクションの深さを跨行で追跡して内部行を宣言 regex 適用前に空白化するようにした。これにより ` Serializable` がトップレベル `function` シンボルとして誤抽出され、`definitionNames` ガード経由で参照分類が詰まってしまうのを防ぐ。さらに、複数行の空白化起動条件を「同一行内で閉じない `[` があるかどうか」に広げ、行頭の `[` だけでなく `void M([\n FromServices\n] IService s)` のようなパラメータ属性、`class Bar<[\n TypeParamAttr\n] T>` のような型パラメータ属性、`delegate void D([\n DelegateParamAttr\n] int x)` のようなデリゲート / ラムダのパラメータ属性でも内部行が正しく空白化されるようにした — 以前は開口行で `[` が行頭にある場合しかカバーされていなかったため、上記の形の内部識別子が `function` シンボルとして誤抽出され、`references --kind attribute` から属性参照が黙って脱落していた。`[\n Serializable\n]`・`[\n global::System.Obsolete\n]`・`[Required,\n Key]`・`[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]`(`AllowNumbers` / `ConverterStrategy` が `attribute` として取り込まれないことを固定)に加え、上記の行頭でない `[` で開く複数行パラメータ / 型パラメータ / デリゲートパラメータ属性のケースも `SymbolExtractor.Extract` + `ReferenceExtractor.Extract` で end-to-end に回して SymbolExtractor 側の回帰も固定する回帰テストを追加。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Indexer/SymbolExtractor.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 diff --git a/src/CodeIndex/Cli/ConsoleUi.cs b/src/CodeIndex/Cli/ConsoleUi.cs index 2129ada457..4f828cfe51 100644 --- a/src/CodeIndex/Cli/ConsoleUi.cs +++ b/src/CodeIndex/Cli/ConsoleUi.cs @@ -388,7 +388,7 @@ public static void PrintUsage(bool showBanner = true) Console.WriteLine(" --exact Backward-compatible shorthand. Prefer --exact-substring for search, keep --exact for find, and prefer --exact-name for symbols/definition/references/callers/callees/inspect."); Console.WriteLine(" --exact-substring Search only: case-sensitive exact substring (no FTS5)"); Console.WriteLine(" --exact-name symbols/definition/references/callers/callees/inspect: NFKC + Unicode CaseFold exact name match (legacy/stale-fold DBs fall back to ASCII NOCASE; use `cdidx backfill-fold` or check `status --json` fold_ready)"); - Console.WriteLine(" --kind definition/symbols/hotspots/unused: symbol kind; references/callers/callees: reference kind (call/instantiate/subscribe/attribute/annotation); validate: issue kind"); + Console.WriteLine(" --kind definition/symbols/hotspots/unused: symbol kind; references: reference kind (call/instantiate/subscribe/attribute/annotation); callers/callees: call-graph kinds only (call/instantiate/subscribe — metadata kinds rejected, use references instead); validate: issue kind"); Console.WriteLine(" --count Count only; search/definition/references/callers/callees/symbols/files/find/unused ignore --limit, impact/hotspots still use visible page counts"); Console.WriteLine(" --since Filter to files modified since this timestamp (ISO 8601)"); Console.WriteLine(" --depth Max BFS depth for impact analysis (default: 5)"); diff --git a/src/CodeIndex/Mcp/McpToolDefinitions.cs b/src/CodeIndex/Mcp/McpToolDefinitions.cs index 96045f2bc4..4c29bcb3fa 100644 --- a/src/CodeIndex/Mcp/McpToolDefinitions.cs +++ b/src/CodeIndex/Mcp/McpToolDefinitions.cs @@ -96,7 +96,7 @@ private JsonNode HandleToolsList(JsonNode? id) ["properties"] = new JsonObject { ["query"] = new JsonObject { ["type"] = "string", ["description"] = "Callee symbol name pattern to search for" }, - ["kind"] = new JsonObject { ["type"] = "string", ["description"] = "Filter by reference kind (call, instantiate, subscribe, attribute, annotation)" }, + ["kind"] = new JsonObject { ["type"] = "string", ["description"] = "Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use `references` for metadata enumeration." }, ["lang"] = new JsonObject { ["type"] = "string", ["description"] = "Filter by language" }, ["limit"] = new JsonObject { ["type"] = "integer", ["description"] = "Max results (default: 20)", ["default"] = 20 }, ["path"] = new JsonObject { ["oneOf"] = new JsonArray { new JsonObject { ["type"] = "string" }, new JsonObject { ["type"] = "array", ["items"] = new JsonObject { ["type"] = "string" } } }, ["description"] = "Prefer or restrict matches to paths containing this text. Accepts a single string or an array; multiple values are OR'd together." }, @@ -117,7 +117,7 @@ private JsonNode HandleToolsList(JsonNode? id) ["properties"] = new JsonObject { ["query"] = new JsonObject { ["type"] = "string", ["description"] = "Caller/container symbol name pattern to search for" }, - ["kind"] = new JsonObject { ["type"] = "string", ["description"] = "Filter by reference kind (call, instantiate, subscribe, attribute, annotation)" }, + ["kind"] = new JsonObject { ["type"] = "string", ["description"] = "Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use `references` for metadata enumeration." }, ["lang"] = new JsonObject { ["type"] = "string", ["description"] = "Filter by language" }, ["limit"] = new JsonObject { ["type"] = "integer", ["description"] = "Max results (default: 20)", ["default"] = 20 }, ["path"] = new JsonObject { ["oneOf"] = new JsonArray { new JsonObject { ["type"] = "string" }, new JsonObject { ["type"] = "array", ["items"] = new JsonObject { ["type"] = "string" } } }, ["description"] = "Prefer or restrict matches to paths containing this text. Accepts a single string or an array; multiple values are OR'd together." }, diff --git a/tests/CodeIndex.Tests/ConsoleUiTests.cs b/tests/CodeIndex.Tests/ConsoleUiTests.cs index 8528276cf2..58aef1d181 100644 --- a/tests/CodeIndex.Tests/ConsoleUiTests.cs +++ b/tests/CodeIndex.Tests/ConsoleUiTests.cs @@ -40,7 +40,7 @@ public void PrintUsage_WithoutBanner_HidesAsciiArtAndEasterEggFlags() Assert.Contains("cdidx find --path ", output); Assert.Contains("--exact-substring Search only: case-sensitive exact substring (no FTS5)", output); Assert.Contains("--exact-name symbols/definition/references/callers/callees/inspect: NFKC + Unicode CaseFold exact name match", output); - Assert.Contains("--kind definition/symbols/hotspots/unused: symbol kind; references/callers/callees: reference kind (call/instantiate/subscribe/attribute/annotation); validate: issue kind", output); + Assert.Contains("--kind definition/symbols/hotspots/unused: symbol kind; references: reference kind (call/instantiate/subscribe/attribute/annotation); callers/callees: call-graph kinds only (call/instantiate/subscribe — metadata kinds rejected, use references instead); validate: issue kind", output); Assert.Contains("--count Count only; search/definition/references/callers/callees/symbols/files/find/unused ignore --limit, impact/hotspots still use visible page counts", output); Assert.Contains("--commits [id ...] Update only files changed in the specified git commits (preferred after commits)", output); Assert.Contains("--files [path ...] Update only the specified files; old rename/delete paths are not purged unless also listed", output); diff --git a/tests/CodeIndex.Tests/McpServerTests.cs b/tests/CodeIndex.Tests/McpServerTests.cs index f551190852..bdb6539020 100644 --- a/tests/CodeIndex.Tests/McpServerTests.cs +++ b/tests/CodeIndex.Tests/McpServerTests.cs @@ -327,6 +327,30 @@ public void ToolsList_ExactAliasParametersAreExposed() Assert.NotNull(symbolsTool["inputSchema"]!["properties"]!["exact"]); } + [Fact] + public void ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds() + { + // Keep the `kind` schema description honest: the callers/callees handlers reject + // metadata kinds (`attribute`, `annotation`) as a usage error, so the schema must + // not advertise them as valid filter values. + // callers/callees の handler は metadata kinds (`attribute` / `annotation`) を拒否するため、 + // schema の `kind` description も有効値として列挙しないこと。 + var request = JsonNode.Parse("""{"jsonrpc":"2.0","id":1,"method":"tools/list"}""")!; + var response = _server.HandleMessage(request)!; + + var tools = response["result"]!["tools"]!.AsArray(); + foreach (var name in new[] { "callers", "callees" }) + { + var tool = tools.First(t => t!["name"]!.GetValue() == name)!; + var kindDescription = tool["inputSchema"]!["properties"]!["kind"]!["description"]!.GetValue(); + + Assert.Contains("call-graph", kindDescription); + Assert.Contains("call, instantiate, subscribe", kindDescription); + Assert.Contains("rejected", kindDescription); + Assert.Contains("references", kindDescription); + } + } + [Fact] public void ToolsList_ImpactAnalysisDescribesHeuristicFallback() { From d1347b2c0a4fcd06e9b13f4d539237826e4b4aa0 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 18 Apr 2026 06:17:30 +0900 Subject: [PATCH 14/30] Fix #293 update README --kind row to match callers/callees contract The README `--kind` option table (EN line 436 / JP line 1307) still suggested passing `--kind attribute` or `--kind annotation` on `callers` / `callees`, but the implementation rejects those combinations with `CommandExitCodes.UsageError` (CLI) / `isError` tool response (MCP). The CLI help and MCP tool schema were already updated in the previous commit but the README was missed. Update the English and Japanese README `--kind` rows to state that `callers` / `callees` reject `--kind attribute|annotation` with a usage error and direct users to `references --kind attribute|annotation` for metadata enumeration. Extend the corresponding CHANGELOG entry (EN + JP) to list README.md among affected files and document the README fix. Test plan: - No code changes; docs-only update. - Verified the reported mismatch by running `dotnet ./src/CodeIndex/bin/Debug/net8.0/cdidx.dll callers Foo --kind attribute --db /tmp/cdidx-293-indexer/.cdidx/codeindex.db` which fails with `'--kind attribute' is not supported on 'callers'`, matching the new README wording. --- CHANGELOG.md | 4 ++-- README.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f74616b0c..a37f58f11d 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/), - **Developer Guide now distinguishes SQLite's role from "zero dependencies" and labels release numbers as examples** — Clarified that cdidx remains a zero-configuration, single-file CLI with exactly one production dependency (`Microsoft.Data.Sqlite`), and made the release-workflow checklist explicitly state that `1.9.0` is only an example version to substitute during an actual release. Affected: `DEVELOPER_GUIDE.md`. #### Fixed -- **MCP `callers` / `callees` `kind` schema and CLI help no longer advertise metadata kinds as valid filter values (#293 follow-up)** — The `callers` / `callees` handlers reject `kind: "attribute"` / `kind: "annotation"` with a usage error (CLI) / `isError` tool response (MCP), but the MCP tool schema's `kind` property description still listed all five kinds as if they were valid filter values (`"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"`), and `cdidx --help` similarly lumped `callers` / `callees` together with `references` for the `--kind` help row. Schema and handler now agree: the `callers` / `callees` `kind` description now reads `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."`, and the CLI `--kind` help row splits `references` (accepts all five kinds) from `callers` / `callees` (call-graph kinds only, metadata rejected, redirects to `references`). The MCP `references` tool description is unchanged because it still accepts every indexed kind. Added `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` to pin the corrected schema wording. Affected: `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `src/CodeIndex/Cli/ConsoleUi.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`. +- **MCP `callers` / `callees` `kind` schema, CLI help, and README `--kind` option row no longer advertise metadata kinds as valid filter values (#293 follow-up)** — The `callers` / `callees` handlers reject `kind: "attribute"` / `kind: "annotation"` with a usage error (CLI) / `isError` tool response (MCP), but the MCP tool schema's `kind` property description still listed all five kinds as if they were valid filter values (`"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"`), `cdidx --help` similarly lumped `callers` / `callees` together with `references` for the `--kind` help row, and the README `--kind` option table (EN line 436 / JP line 1307) still directed readers to pass `--kind attribute` or `--kind annotation` on `callers` / `callees`. Schema and handler now agree: the `callers` / `callees` `kind` description now reads `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."`, the CLI `--kind` help row splits `references` (accepts all five kinds) from `callers` / `callees` (call-graph kinds only, metadata rejected, redirects to `references`), and the README `--kind` row (both English and Japanese) now explicitly states that `callers` / `callees` reject `--kind attribute|annotation` with a usage error and points to `references --kind attribute|annotation` as the correct metadata-enumeration path. The MCP `references` tool description is unchanged because it still accepts every indexed kind. Added `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` to pin the corrected schema wording. Affected: `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `src/CodeIndex/Cli/ConsoleUi.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `README.md`. - **`deps` and `impact` file-level dependency hints now exclude metadata-only references (#293 follow-up)** — `DbReader.GetFileDependencies` (backing `cdidx deps`) and `DbReader.GetFileDependencyHintsToResolvedType` (backing `cdidx impact` / MCP `impact_analysis` heuristic file-hint fallback) used to project every `symbol_references` row into the logical reference CTE, filtering only by graph-supported language. Metadata-only usages such as `[JsonConverter(typeof(User))]` or `@Inject(User.class)` — which touch a type at compile time but do not create a runtime dependency — therefore leaked into dependency output and made the annotated file look like it depended on the target type. Both queries now additionally gate the CTE on `r.reference_kind IN CallGraphReferenceKindsSql ('call', 'instantiate', 'subscribe')`, matching the contract already enforced by `callers` / `callees` / `hotspots` / `impact` BFS. Added regression coverage in `DbReaderTests`: `GetFileDependencies_ExcludesMetadataOnlyReferences` pins that the attribute-only consumer does not create a `deps` edge while `new User()` still does, and `AnalyzeImpact_ExcludesMetadataOnlyReferencesFromHeuristicHints` pins that an attribute-only consumer produces zero heuristic file hints. Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. - **`callers` / `callees` now reject `--kind attribute|annotation` at the CLI and MCP boundary, and Swift / Gradle `@` metadata is reclassified as `annotation` (#293 follow-up)** — Metadata rows (`attribute` / `annotation`) are attributed to their enclosing body-range symbol rather than the annotated target, so `callers Obsolete --kind attribute` would silently return `[Obsolete] void M()` under the enclosing class (not `M`), and file-level targets such as `[assembly: CLSCompliant]` or Kotlin `@file:JvmName` would drop entirely because `container_name` is `NULL`. `QueryCommandRunner.RunCallers` / `RunCallees` now reject `--kind attribute` and `--kind annotation` with `CommandExitCodes.UsageError`, and the MCP `callers` / `callees` tools return an `isError` tool response; both paths direct users to `references --kind attribute|annotation`, which IS a correct metadata enumeration path. Separately, `AnnotationLanguages` now includes `swift` and `gradle`, so Swift `@available(iOS 13.0, *)` / `@objc` / `@MainActor` and Gradle/Groovy `@CompileStatic` / `@TaskAction` are recorded as `annotation` instead of `call` (previously the `@Name(args)` forms leaked into `callers` / `callees` / `hotspots` / `impact` as phantom call edges, and the no-arg `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` forms dropped from the index entirely because no-arg annotation emission was gated to languages in this set). Added `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError` (theory, 4 cases), `McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError` (theory, 4 cases), `ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`, and `ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation`. Affected: `src/CodeIndex/Cli/QueryCommandRunner.cs`, `src/CodeIndex/Mcp/McpToolHandlers.cs`, `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. - **Multi-line no-arg C# attribute sections like `[\n Serializable\n]` are now indexed without over-matching attribute argument identifiers (#293 follow-up)** — The no-arg C# attribute regex previously required the match to start at a `[` / `,` boundary and end at `]` / `,` on the same line, so a bare identifier on the interior line of a multi-line attribute section (e.g. ` Serializable` between a line-leading `[` and a line-leading `]`) was silently dropped from `references --kind attribute`. The left anchor is now a word-boundary lookbehind and the right anchor also accepts end-of-line. To avoid misclassifying enum / qualified-constant identifiers that happen to sit at end-of-line inside an attribute argument list (e.g. `ConverterStrategy.AllowNumbers` in `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]`) as no-arg attributes, `BuildCSharpAttributeRanges` now also emits a parallel "top-level zone" table that splits each attribute section by `(` / `)` so only paren-depth-0 positions inside the section qualify as no-arg attribute name positions. Paren depth is tracked section-locally (each `[` snapshots the global depth at open time) so parameter attributes like `void M([FromServices] IService s)`, delegate parameter attributes like `delegate void D([Attr] int x)`, and lambda parameter attributes like `Func f = ([Attr] int x) => x` — whose `[` opens while the global paren depth is already > 0 — still have their attribute-list top level correctly identified. The no-arg attribute path uses that top-level table instead of the full attribute-section ranges, while the existing with-args metadata classification still uses the full ranges. `SymbolExtractor` also now tracks multi-line `[...]` depth across lines via `StripMultiLineCSharpAttributeInterior` in `BuildCSharpMatchLines`, so interior lines of a multi-line attribute section are blanked out before declaration regexes run — this prevents ` Serializable` from being mis-extracted as a top-level `function` symbol (which then masked the reference through the `definitionNames` guard). The multi-line blanker now activates on any `[` that opens without a matching `]` on the same line, not just `[` that sits at the start of the line, so multi-line parameter attributes like `void M([\n FromServices\n] IService s)`, type-parameter attributes like `class Bar<[\n TypeParamAttr\n] T>`, and delegate/lambda parameter attributes like `delegate void D([\n DelegateParamAttr\n] int x)` are also blanked out correctly — previously only the leading-`[` case was covered, so the interior identifiers of those forms were mis-extracted as `function` symbols and their attribute references were silently dropped from `references --kind attribute`. Regression coverage adds `[\n Serializable\n]`, `[\n global::System.Obsolete\n]`, `[Required,\n Key]`, `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]` (pinning that `AllowNumbers` / `ConverterStrategy` are NOT classified as `attribute`), and the non-leading-`[` parameter / type-parameter / delegate-parameter multi-line cases above, running through `SymbolExtractor.Extract` + `ReferenceExtractor.Extract` end-to-end so both sides of the regression stay pinned. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Indexer/SymbolExtractor.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. @@ -685,7 +685,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **DEVELOPER_GUIDE が SQLite の位置付けとリリース番号の例示を明確化** — cdidx が「設定ゼロ・単一ファイル・本番依存1個」の CLI であり、その唯一の本番依存が `Microsoft.Data.Sqlite` であることを明記した。あわせて、リリース手順のチェックリスト中に出てくる `1.9.0` は固定値ではなく例示であり、実際のリリース番号へ読み替える前提だと明示した。対象: `DEVELOPER_GUIDE.md`。 #### 修正 -- **MCP の `callers` / `callees` の `kind` schema と CLI help が metadata kinds を有効フィルタ値として広告しないよう修正 (#293 追加対応)** — `callers` / `callees` の handler は `kind: "attribute"` / `kind: "annotation"` を usage error(CLI)/ `isError` ツールレスポンス(MCP)で拒否するにもかかわらず、MCP ツール schema の `kind` プロパティ説明は 5 種すべてを列挙した `"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"` のままで、`cdidx --help` の `--kind` 行も `callers` / `callees` を `references` と同じ行にまとめていた。schema と handler の契約を一致させ、`callers` / `callees` の `kind` description を `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."` に修正し、CLI の `--kind` help 行も `references`(5 種すべて受理)と `callers` / `callees`(call-graph 種別のみ、metadata kinds は拒否して `references` へ誘導)を分けるようにした。MCP の `references` ツール説明は全 reference kind を受け付ける契約のままなので変更なし。修正後の schema 文言を固定する `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` を追加。対象: `src/CodeIndex/Mcp/McpToolDefinitions.cs`、`src/CodeIndex/Cli/ConsoleUi.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`。 +- **MCP の `callers` / `callees` の `kind` schema・CLI help・README の `--kind` オプション行が metadata kinds を有効フィルタ値として広告しないよう修正 (#293 追加対応)** — `callers` / `callees` の handler は `kind: "attribute"` / `kind: "annotation"` を usage error(CLI)/ `isError` ツールレスポンス(MCP)で拒否するにもかかわらず、MCP ツール schema の `kind` プロパティ説明は 5 種すべてを列挙した `"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"` のままで、`cdidx --help` の `--kind` 行も `callers` / `callees` を `references` と同じ行にまとめていた。さらに README の `--kind` オプション表(英語 436 行目 / 日本語 1307 行目)も、`callers` / `callees` で `--kind attribute` / `--kind annotation` を渡せるかのように案内していた。schema と handler の契約を一致させ、`callers` / `callees` の `kind` description を `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."` に修正し、CLI の `--kind` help 行も `references`(5 種すべて受理)と `callers` / `callees`(call-graph 種別のみ、metadata kinds は拒否して `references` へ誘導)を分けるようにし、README の `--kind` 行(英語・日本語の両方)も `callers` / `callees` が `--kind attribute|annotation` を usage error で拒否すること、metadata 列挙の正規経路が `references --kind attribute|annotation` であることを明示する文言に直した。MCP の `references` ツール説明は全 reference kind を受け付ける契約のままなので変更なし。修正後の schema 文言を固定する `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` を追加。対象: `src/CodeIndex/Mcp/McpToolDefinitions.cs`、`src/CodeIndex/Cli/ConsoleUi.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`、`README.md`。 - **`deps` と `impact` のファイルレベル依存ヒントから metadata-only 参照を除外 (#293 追加対応)** — `cdidx deps` の実装である `DbReader.GetFileDependencies` と、`cdidx impact` / MCP `impact_analysis` の heuristic file-hint フォールバックである `DbReader.GetFileDependencyHintsToResolvedType` が、これまで `symbol_references` を graph 対応言語でしか絞っておらず `reference_kind` で絞っていなかったため、`[JsonConverter(typeof(User))]` や `@Inject(User.class)` のような compile-time metadata として型に触れるだけで実行時依存ではない参照が依存エッジとして漏れていた(注釈ファイルが対象型に依存しているように見えた)。両クエリの logical reference CTE を `r.reference_kind IN CallGraphReferenceKindsSql ('call', 'instantiate', 'subscribe')` で追加絞り込みし、`callers` / `callees` / `hotspots` / `impact` BFS と同じ契約に揃えた。回帰テストを `DbReaderTests` に追加: `GetFileDependencies_ExcludesMetadataOnlyReferences` は attribute-only consumer が `deps` エッジを作らず `new User()` は作ることを固定し、`AnalyzeImpact_ExcludesMetadataOnlyReferencesFromHeuristicHints` は attribute-only consumer が heuristic file hint を 1 件も生成しないことを固定する。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 - **`callers` / `callees` が CLI と MCP 境界で `--kind attribute|annotation` を拒否し、Swift / Gradle の `@` metadata を `annotation` に再分類 (#293 追加対応)** — metadata 行 (`attribute` / `annotation`) は注釈対象そのものではなく body-range の外側シンボルに帰属するため、`callers Obsolete --kind attribute` は `[Obsolete] void M()` を `M` ではなく外側クラスの下に黙って返し、`[assembly: CLSCompliant]` や Kotlin の `@file:JvmName` のような file-level target は `container_name = NULL` で完全脱落していた。`QueryCommandRunner.RunCallers` / `RunCallees` は `--kind attribute` / `--kind annotation` を `CommandExitCodes.UsageError` で弾き、MCP の `callers` / `callees` ツールは `isError` ツールレスポンスを返すようにした。どちらの経路も、metadata 列挙の正しいパスである `references --kind attribute|annotation` へ誘導する。あわせて `AnnotationLanguages` に `swift` と `gradle` を追加し、Swift の `@available(iOS 13.0, *)` / `@objc` / `@MainActor` や Gradle / Groovy の `@CompileStatic` / `@TaskAction` を `call` ではなく `annotation` として記録するようにした(修正前は引数付きの `@Name(args)` 形は `callers` / `callees` / `hotspots` / `impact` に phantom call edge として漏れ、引数なしの `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` 形はこの集合で gate されていた no-arg annotation 出力から外れてインデックス自体に残らなかった)。回帰テストとして `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError`(theory 4 ケース)、`McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError`(theory 4 ケース)、`ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`、`ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation` を追加。対象: `src/CodeIndex/Cli/QueryCommandRunner.cs`、`src/CodeIndex/Mcp/McpToolHandlers.cs`、`src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 - **`[\n Serializable\n]` のような複数行にまたがる引数なし C# 属性セクションを、属性引数内の識別子を誤って取り込まずに索引化 (#293 追加対応)** — 引数なし C# 属性の正規表現は、これまで `[` / `,` を左端、`]` / `,` を右端として同一行内でアンカーしていたため、行頭 `[` と行頭 `]` の間に書かれた ` Serializable` のような裸識別子が `references --kind attribute` から黙って脱落していた。左側は単語境界 lookbehind、右側は行末(`$`)も許容するように緩和した。一方で `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]` のように属性の引数リスト内で行末に現れる enum / 修飾定数識別子(`AllowNumbers` など)が no-arg 属性として誤分類される回帰を避けるため、`BuildCSharpAttributeRanges` は `(` / `)` で属性セクションを分割した paren 深さ 0 の「top-level ゾーン」テーブルも並列で出力するようにし、no-arg 属性の採用位置はそのテーブルに限定した(既存の引数付き metadata 分類パスは引き続き属性セクション全体のレンジを使う)。paren 深さは section-local に追跡する(各 `[` がその瞬間のグローバル paren 深さを snapshot として保持)ため、`void M([FromServices] IService s)` のようなパラメータ属性、`delegate void D([Attr] int x)` のようなデリゲートパラメータ属性、`Func f = ([Attr] int x) => x` のようなラムダパラメータ属性のように、`[` が開いた時点で既にグローバル paren 深さが 0 より大きいケースでも、属性リストの top-level を正しく認識できる。`SymbolExtractor` 側でも `BuildCSharpMatchLines` に `StripMultiLineCSharpAttributeInterior` を追加し、複数行にまたがる `[...]` セクションの深さを跨行で追跡して内部行を宣言 regex 適用前に空白化するようにした。これにより ` Serializable` がトップレベル `function` シンボルとして誤抽出され、`definitionNames` ガード経由で参照分類が詰まってしまうのを防ぐ。さらに、複数行の空白化起動条件を「同一行内で閉じない `[` があるかどうか」に広げ、行頭の `[` だけでなく `void M([\n FromServices\n] IService s)` のようなパラメータ属性、`class Bar<[\n TypeParamAttr\n] T>` のような型パラメータ属性、`delegate void D([\n DelegateParamAttr\n] int x)` のようなデリゲート / ラムダのパラメータ属性でも内部行が正しく空白化されるようにした — 以前は開口行で `[` が行頭にある場合しかカバーされていなかったため、上記の形の内部識別子が `function` シンボルとして誤抽出され、`references --kind attribute` から属性参照が黙って脱落していた。`[\n Serializable\n]`・`[\n global::System.Obsolete\n]`・`[Required,\n Key]`・`[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]`(`AllowNumbers` / `ConverterStrategy` が `attribute` として取り込まれないことを固定)に加え、上記の行頭でない `[` で開く複数行パラメータ / 型パラメータ / デリゲートパラメータ属性のケースも `SymbolExtractor.Extract` + `ReferenceExtractor.Extract` で end-to-end に回して SymbolExtractor 側の回帰も固定する回帰テストを追加。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Indexer/SymbolExtractor.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 diff --git a/README.md b/README.md index ebd5de9abe..257be3e5b5 100644 --- a/README.md +++ b/README.md @@ -433,7 +433,7 @@ cdidx map --path src/ --exclude-tests --json | `--exact` | `search`, `find`, `symbols`, `definition`, `references`, `callers`, `callees`, `inspect` | Backward-compatible shorthand. Prefer `--exact-substring` for `search`, keep `--exact` for `find`, and prefer `--exact-name` for symbol / graph commands plus `inspect`. CLI JSON and MCP `structuredContent` expose `exact_index_available` / `degraded_reason`; MCP also keeps the legacy camelCase aliases `exactIndexAvailable` / `degradedReason` for backward compatibility. | | `--exact-substring` | `search` | Preferred explicit name for search exactness: case-sensitive exact substring (FTS5 bypassed). | | `--exact-name` | `symbols`, `definition`, `references`, `callers`, `callees`, `inspect` | Preferred explicit name for symbol-name exactness: NFKC + Unicode CaseFold exact equality (`Ä` / `ä`, `Run` / `Run`, ligatures, sharp-S, and Greek final sigma collapse). Unicode CaseFold remains locale-invariant, so Turkish dotted `İ` is still distinct from plain `i`. For C#, pass the canonical extracted name (`operator +`, `operator checked +`, `explicit operator Money`, `implicit operator decimal`, `Item`) rather than source keywords like `this` / `explicit`. Falls back to ASCII `COLLATE NOCASE` while the DB still contains stale fold metadata; prefer `cdidx backfill-fold`, or use a plain `cdidx index .` if it rewrites or purges every stale row, otherwise `--rebuild`. `status --json` exposes `fold_ready` and `csharp_symbol_name_ready` so AI clients can tell which path is active. When a read-only legacy DB is missing the fallback exact-match indexes, human-readable output warns and CLI JSON / MCP `structuredContent` expose degraded-state metadata. | -| `--kind ` | `definition`, `references`, `callers`, `callees`, `symbols`, `hotspots`, `unused`, `validate` | Filter by kind. `definition` / `symbols` / `hotspots` / `unused` use symbol kinds (`function`, `class`, `struct`, `interface`, `enum`, `property`, `event`, `delegate`, `namespace`, `import`); `references` / `callers` / `callees` use reference kinds (`call`, `instantiate`, `subscribe`, `attribute`, `annotation`). `references` defaults to every indexed reference kind so metadata usages remain visible, while `callers` / `callees` / `hotspots` / `impact` default to the call-graph kinds only (`call`, `instantiate`, `subscribe`) and exclude metadata edges (`attribute`, `annotation`); pass `--kind attribute` or `--kind annotation` to surface C# `[...]` attribute / Java-family `@Annotation(...)` usages explicitly. Identical constructor `call` + `instantiate` rows at one physical site still collapse; `validate` uses issue kinds such as `bom` | +| `--kind ` | `definition`, `references`, `callers`, `callees`, `symbols`, `hotspots`, `unused`, `validate` | Filter by kind. `definition` / `symbols` / `hotspots` / `unused` use symbol kinds (`function`, `class`, `struct`, `interface`, `enum`, `property`, `event`, `delegate`, `namespace`, `import`); `references` accepts all five reference kinds (`call`, `instantiate`, `subscribe`, `attribute`, `annotation`); `callers` / `callees` accept only the call-graph kinds (`call`, `instantiate`, `subscribe`) and reject `--kind attribute` / `--kind annotation` with a usage error (the metadata row is attributed to the enclosing body-range symbol rather than the annotated target, so `callers` / `callees` are not a reliable path for metadata — use `references --kind attribute` / `references --kind annotation` for metadata enumeration). `references` defaults to every indexed reference kind so metadata usages remain visible, while `callers` / `callees` / `hotspots` / `impact` default to the call-graph kinds only (`call`, `instantiate`, `subscribe`) and exclude metadata edges (`attribute`, `annotation`). Identical constructor `call` + `instantiate` rows at one physical site still collapse; `validate` uses issue kinds such as `bom` | | `--body` | `definition`, `inspect` | Include reconstructed body content when the language extractor can infer the body range | | `--count` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `impact`, `unused`, `hotspots` | Return only counts. `search` / `definition` / `references` / `callers` / `callees` / `symbols` / `files` / `find` / `unused` ignore `--limit` and return authoritative totals; `impact` and `hotspots` still report the visible page count and may truncate with `--limit` (with `--json`: a single count object; commands that expose file counts add `files`) | | `--group-by-name` | `hotspots` | Collapse rows that share the same `(name, kind)` across files into one representative result while preserving `definition_sites` / `paths` metadata in JSON | @@ -1304,7 +1304,7 @@ cdidx map --path src/ --exclude-tests --json | `--exact` | `search`, `find`, `symbols`, `definition`, `references`, `callers`, `callees`, `inspect` | 後方互換の短縮形。`search` では `--exact-substring`、`find` では `--exact` を使い、symbol / graph 系コマンドと `inspect` では `--exact-name` を推奨。CLI JSON と MCP `structuredContent` は `exact_index_available` / `degraded_reason` を返し、MCP では後方互換の camelCase alias も維持する。 | | `--exact-substring` | `search` | `search` 用の推奨 explicit alias。大文字小文字を区別する完全部分一致(FTS5 バイパス)。 | | `--exact-name` | `symbols`, `definition`, `references`, `callers`, `callees`, `inspect` | symbol-name exactness 用の推奨 explicit alias。NFKC + Unicode CaseFold による完全一致(`Ä` / `ä`、全角 `Run` / `Run`、合字、sharp-S、Greek final sigma を畳み込む)。Unicode CaseFold は locale-invariant のため、トルコ語の dotted `İ` は plain `i` と同一視しない。C# では `this` / `explicit` のような source keyword ではなく、抽出済みの canonical name(`operator +`、`operator checked +`、`explicit operator Money`、`implicit operator decimal`、`Item`)を渡す。DB に stale な fold metadata が残る間は ASCII `COLLATE NOCASE` に fallback するため、まず `cdidx backfill-fold`、または stale row を全置換できる通常の `cdidx index .`、それが無理なら `--rebuild` を使う(`status --json` の `fold_ready` と `csharp_symbol_name_ready` で判定)。read-only な旧DBに fallback exact-match index が無い場合は、人間向け出力が WARN を表示し、CLI JSON と MCP `structuredContent` が縮退メタデータを返す。 | -| `--kind ` | `definition`, `references`, `callers`, `callees`, `symbols`, `hotspots`, `unused`, `validate` | 種別でフィルタ。`definition` / `symbols` / `hotspots` / `unused` は symbol kind(`function`、`class`、`struct`、`interface`、`enum`、`property`、`event`、`delegate`、`namespace`、`import`)、`references` / `callers` / `callees` は reference kind(`call`、`instantiate`、`subscribe`、`attribute`、`annotation`)を使う。`references` の既定は全 reference kind を表示して metadata 参照も見えるままにするが、`callers` / `callees` / `hotspots` / `impact` の既定は call-graph kind(`call`、`instantiate`、`subscribe`)のみで、`attribute` / `annotation` のような metadata edge は除外する。C# の `[...]` 属性や Java 系 `@Annotation(...)` を明示的に取りたいときは `--kind attribute` / `--kind annotation` を指定する。同じ物理位置にある constructor の `call` + `instantiate` 重複行は引き続き集約する。`validate` は `bom` などの issue kind を使う | +| `--kind ` | `definition`, `references`, `callers`, `callees`, `symbols`, `hotspots`, `unused`, `validate` | 種別でフィルタ。`definition` / `symbols` / `hotspots` / `unused` は symbol kind(`function`、`class`、`struct`、`interface`、`enum`、`property`、`event`、`delegate`、`namespace`、`import`)、`references` は 5 種すべての reference kind(`call`、`instantiate`、`subscribe`、`attribute`、`annotation`)を受け付ける。`callers` / `callees` は call-graph 種別のみ(`call`、`instantiate`、`subscribe`)を受け付け、`--kind attribute` / `--kind annotation` は usage error で拒否する(metadata 行は注釈対象そのものではなく body-range 上の外側シンボルに帰属するため `callers` / `callees` は metadata 列挙の信頼できる経路ではない — metadata 列挙は `references --kind attribute` / `references --kind annotation` を使う)。`references` の既定は全 reference kind を表示して metadata 参照も見えるままにするが、`callers` / `callees` / `hotspots` / `impact` の既定は call-graph kind(`call`、`instantiate`、`subscribe`)のみで、`attribute` / `annotation` のような metadata edge は除外する。同じ物理位置にある constructor の `call` + `instantiate` 重複行は引き続き集約する。`validate` は `bom` などの issue kind を使う | | `--body` | `definition`, `inspect` | 言語抽出器が本体範囲を推論できる場合に本体内容も含める | | `--count` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `impact`, `unused`, `hotspots` | 件数だけを返す。`search` / `definition` / `references` / `callers` / `callees` / `symbols` / `files` / `find` / `unused` は `--limit` を無視した総件数を返し、`impact` と `hotspots` は visible page count のままで `--limit` によって切り詰められることがある(`--json` 併用時は単一の count オブジェクト。files 件数を出すコマンドは `files` も返す) | | `--group-by-name` | `hotspots` | ファイルをまたいで同じ `(name, kind)` を共有する行を代表1件に集約し、JSON では `definition_sites` / `paths` metadata を保持したまま返す | From 74e2a9c241d14dcb78463ba0af50dc688d6c9d7d Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 18 Apr 2026 11:36:16 +0900 Subject: [PATCH 15/30] Fix #293 retain metadata refs as compile-time deps in deps/impact Round 4 adversarial review surfaced that the filter added to GetFileDependencies and GetFileDependencyHintsToResolvedType silently narrowed `cdidx deps` / `cdidx impact` from "files that reference a symbol" to "call-graph-only edges", dropping real compile-time dependency edges such as `[JsonConverterAttribute]` (renaming the attribute class breaks the annotated site at build time). Call-graph semantics belong to `callers` / `callees`, which still reject `--kind attribute|annotation` at the CLI / MCP boundary. Dependency graph semantics belong to `deps` / `impact`, which must surface metadata edges. Removed the filter from both DbReader methods. The `impact` heuristic file-hint fallback additionally enforces `SourceFileHasStructuredTypeEvidence` (pre-existing, predates #293) to prevent member-name collisions. Metadata-only consumer files without other structured type evidence therefore still do not surface through the heuristic; CHANGELOG now documents that `cdidx deps --path --reverse` is the authoritative path for metadata dependency edges. - Replace DbReaderTests.GetFileDependencies_ExcludesMetadataOnlyReferences with GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies which pins both `new JsonConverter(...)` and `[JsonConverter(...)]` producing `deps` edges to the attribute-class-defining file. - Remove DbReaderTests.AnalyzeImpact_ExcludesMetadataOnlyReferencesFromHeuristicHints which locked in the (now reverted) narrowed behavior. - Update CHANGELOG entry (EN + JP) to describe the final retain-metadata behavior and call out the evidence-guard caveat for the `impact` heuristic fallback. Co-Authored-By: Claude Opus 4 --- CHANGELOG.md | 4 +- src/CodeIndex/Database/DbReader.cs | 47 +++++---- tests/CodeIndex.Tests/DbReaderTests.cs | 135 +++++++------------------ 3 files changed, 68 insertions(+), 118 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a37f58f11d..faa3f3aa3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), #### Fixed - **MCP `callers` / `callees` `kind` schema, CLI help, and README `--kind` option row no longer advertise metadata kinds as valid filter values (#293 follow-up)** — The `callers` / `callees` handlers reject `kind: "attribute"` / `kind: "annotation"` with a usage error (CLI) / `isError` tool response (MCP), but the MCP tool schema's `kind` property description still listed all five kinds as if they were valid filter values (`"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"`), `cdidx --help` similarly lumped `callers` / `callees` together with `references` for the `--kind` help row, and the README `--kind` option table (EN line 436 / JP line 1307) still directed readers to pass `--kind attribute` or `--kind annotation` on `callers` / `callees`. Schema and handler now agree: the `callers` / `callees` `kind` description now reads `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."`, the CLI `--kind` help row splits `references` (accepts all five kinds) from `callers` / `callees` (call-graph kinds only, metadata rejected, redirects to `references`), and the README `--kind` row (both English and Japanese) now explicitly states that `callers` / `callees` reject `--kind attribute|annotation` with a usage error and points to `references --kind attribute|annotation` as the correct metadata-enumeration path. The MCP `references` tool description is unchanged because it still accepts every indexed kind. Added `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` to pin the corrected schema wording. Affected: `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `src/CodeIndex/Cli/ConsoleUi.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `README.md`. -- **`deps` and `impact` file-level dependency hints now exclude metadata-only references (#293 follow-up)** — `DbReader.GetFileDependencies` (backing `cdidx deps`) and `DbReader.GetFileDependencyHintsToResolvedType` (backing `cdidx impact` / MCP `impact_analysis` heuristic file-hint fallback) used to project every `symbol_references` row into the logical reference CTE, filtering only by graph-supported language. Metadata-only usages such as `[JsonConverter(typeof(User))]` or `@Inject(User.class)` — which touch a type at compile time but do not create a runtime dependency — therefore leaked into dependency output and made the annotated file look like it depended on the target type. Both queries now additionally gate the CTE on `r.reference_kind IN CallGraphReferenceKindsSql ('call', 'instantiate', 'subscribe')`, matching the contract already enforced by `callers` / `callees` / `hotspots` / `impact` BFS. Added regression coverage in `DbReaderTests`: `GetFileDependencies_ExcludesMetadataOnlyReferences` pins that the attribute-only consumer does not create a `deps` edge while `new User()` still does, and `AnalyzeImpact_ExcludesMetadataOnlyReferencesFromHeuristicHints` pins that an attribute-only consumer produces zero heuristic file hints. Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. +- **`deps` file-level dependency analysis intentionally retains metadata references (`attribute` / `annotation`) as compile-time dependency edges (#293 follow-up)** — `DbReader.GetFileDependencies` (backing `cdidx deps`) and `DbReader.GetFileDependencyHintsToResolvedType` (backing `cdidx impact` / MCP `impact_analysis` heuristic file-hint fallback) keep metadata references in their logical reference CTE. Renaming or removing an attribute class still breaks annotated sites such as `[JsonConverterAttribute]` at compile time, so file-level blast-radius analysis must surface those edges as real dependencies — now labeled `attribute` / `annotation` in `logical_reference_kind` (previously these same sites leaked in as phantom `call` rows before classification was corrected). The `impact` heuristic file-hint fallback additionally enforces `SourceFileHasStructuredTypeEvidence` on each candidate source file (a pre-existing guard that predates #293 and protects against member-name collisions), so metadata-only consumer files without other structured type evidence will not surface through that heuristic; users should run `cdidx deps --path --reverse` to see the authoritative metadata dependency edges. `callers` / `callees` still reject `--kind attribute|annotation` at the CLI / MCP boundary because those commands model the dynamic call graph, not the dependency graph. Added regression coverage `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies` pinning that both `new JsonConverter(...)` and `[JsonConverter(...)]` produce a `deps` edge to the attribute-class-defining file. Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. - **`callers` / `callees` now reject `--kind attribute|annotation` at the CLI and MCP boundary, and Swift / Gradle `@` metadata is reclassified as `annotation` (#293 follow-up)** — Metadata rows (`attribute` / `annotation`) are attributed to their enclosing body-range symbol rather than the annotated target, so `callers Obsolete --kind attribute` would silently return `[Obsolete] void M()` under the enclosing class (not `M`), and file-level targets such as `[assembly: CLSCompliant]` or Kotlin `@file:JvmName` would drop entirely because `container_name` is `NULL`. `QueryCommandRunner.RunCallers` / `RunCallees` now reject `--kind attribute` and `--kind annotation` with `CommandExitCodes.UsageError`, and the MCP `callers` / `callees` tools return an `isError` tool response; both paths direct users to `references --kind attribute|annotation`, which IS a correct metadata enumeration path. Separately, `AnnotationLanguages` now includes `swift` and `gradle`, so Swift `@available(iOS 13.0, *)` / `@objc` / `@MainActor` and Gradle/Groovy `@CompileStatic` / `@TaskAction` are recorded as `annotation` instead of `call` (previously the `@Name(args)` forms leaked into `callers` / `callees` / `hotspots` / `impact` as phantom call edges, and the no-arg `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` forms dropped from the index entirely because no-arg annotation emission was gated to languages in this set). Added `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError` (theory, 4 cases), `McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError` (theory, 4 cases), `ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`, and `ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation`. Affected: `src/CodeIndex/Cli/QueryCommandRunner.cs`, `src/CodeIndex/Mcp/McpToolHandlers.cs`, `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. - **Multi-line no-arg C# attribute sections like `[\n Serializable\n]` are now indexed without over-matching attribute argument identifiers (#293 follow-up)** — The no-arg C# attribute regex previously required the match to start at a `[` / `,` boundary and end at `]` / `,` on the same line, so a bare identifier on the interior line of a multi-line attribute section (e.g. ` Serializable` between a line-leading `[` and a line-leading `]`) was silently dropped from `references --kind attribute`. The left anchor is now a word-boundary lookbehind and the right anchor also accepts end-of-line. To avoid misclassifying enum / qualified-constant identifiers that happen to sit at end-of-line inside an attribute argument list (e.g. `ConverterStrategy.AllowNumbers` in `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]`) as no-arg attributes, `BuildCSharpAttributeRanges` now also emits a parallel "top-level zone" table that splits each attribute section by `(` / `)` so only paren-depth-0 positions inside the section qualify as no-arg attribute name positions. Paren depth is tracked section-locally (each `[` snapshots the global depth at open time) so parameter attributes like `void M([FromServices] IService s)`, delegate parameter attributes like `delegate void D([Attr] int x)`, and lambda parameter attributes like `Func f = ([Attr] int x) => x` — whose `[` opens while the global paren depth is already > 0 — still have their attribute-list top level correctly identified. The no-arg attribute path uses that top-level table instead of the full attribute-section ranges, while the existing with-args metadata classification still uses the full ranges. `SymbolExtractor` also now tracks multi-line `[...]` depth across lines via `StripMultiLineCSharpAttributeInterior` in `BuildCSharpMatchLines`, so interior lines of a multi-line attribute section are blanked out before declaration regexes run — this prevents ` Serializable` from being mis-extracted as a top-level `function` symbol (which then masked the reference through the `definitionNames` guard). The multi-line blanker now activates on any `[` that opens without a matching `]` on the same line, not just `[` that sits at the start of the line, so multi-line parameter attributes like `void M([\n FromServices\n] IService s)`, type-parameter attributes like `class Bar<[\n TypeParamAttr\n] T>`, and delegate/lambda parameter attributes like `delegate void D([\n DelegateParamAttr\n] int x)` are also blanked out correctly — previously only the leading-`[` case was covered, so the interior identifiers of those forms were mis-extracted as `function` symbols and their attribute references were silently dropped from `references --kind attribute`. Regression coverage adds `[\n Serializable\n]`, `[\n global::System.Obsolete\n]`, `[Required,\n Key]`, `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]` (pinning that `AllowNumbers` / `ConverterStrategy` are NOT classified as `attribute`), and the non-leading-`[` parameter / type-parameter / delegate-parameter multi-line cases above, running through `SymbolExtractor.Extract` + `ReferenceExtractor.Extract` end-to-end so both sides of the regression stay pinned. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Indexer/SymbolExtractor.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. - **`::`-qualified C# no-arg attributes and JavaScript `@Decorator` usages are now classified correctly (#293 follow-up)** — The no-arg C# attribute regex now accepts `::` alongside `.` as a qualifier separator, so `[global::System.Obsolete]` and `[Alias::MyAttr]` reach `references --kind attribute` instead of being silently dropped. `AnnotationLanguages` additionally includes `javascript`, so JS decorators such as `@sealed` (no-arg) and `@injectable()` (with args) are reclassified to `annotation` rather than leaking into the call-graph as phantom `call` rows — matching the existing TypeScript contract since JavaScript is already a graph-supported language. Added regression tests pinning `[global::System.Obsolete]`, `[Alias::MyAttr]`, and JavaScript `@sealed` / `@injectable()`. Also rewrote the README `callers` row (EN + JP) and the MCP tool descriptions for `callers` / `callees` to pull back the earlier over-promise that `kind: "attribute"` / `kind: "annotation"` on `callers` / `callees` is a supported metadata-inspection path: a metadata row is attributed to its enclosing body-range symbol (for a class-level declaration, that is the class itself) and drops entirely for file-level targets such as `[assembly: ...]` where `container_name` is `null`, so the docs now direct users to `references --kind attribute|annotation` for metadata enumeration. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`, `README.md`. @@ -686,7 +686,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), #### 修正 - **MCP の `callers` / `callees` の `kind` schema・CLI help・README の `--kind` オプション行が metadata kinds を有効フィルタ値として広告しないよう修正 (#293 追加対応)** — `callers` / `callees` の handler は `kind: "attribute"` / `kind: "annotation"` を usage error(CLI)/ `isError` ツールレスポンス(MCP)で拒否するにもかかわらず、MCP ツール schema の `kind` プロパティ説明は 5 種すべてを列挙した `"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"` のままで、`cdidx --help` の `--kind` 行も `callers` / `callees` を `references` と同じ行にまとめていた。さらに README の `--kind` オプション表(英語 436 行目 / 日本語 1307 行目)も、`callers` / `callees` で `--kind attribute` / `--kind annotation` を渡せるかのように案内していた。schema と handler の契約を一致させ、`callers` / `callees` の `kind` description を `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."` に修正し、CLI の `--kind` help 行も `references`(5 種すべて受理)と `callers` / `callees`(call-graph 種別のみ、metadata kinds は拒否して `references` へ誘導)を分けるようにし、README の `--kind` 行(英語・日本語の両方)も `callers` / `callees` が `--kind attribute|annotation` を usage error で拒否すること、metadata 列挙の正規経路が `references --kind attribute|annotation` であることを明示する文言に直した。MCP の `references` ツール説明は全 reference kind を受け付ける契約のままなので変更なし。修正後の schema 文言を固定する `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` を追加。対象: `src/CodeIndex/Mcp/McpToolDefinitions.cs`、`src/CodeIndex/Cli/ConsoleUi.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`、`README.md`。 -- **`deps` と `impact` のファイルレベル依存ヒントから metadata-only 参照を除外 (#293 追加対応)** — `cdidx deps` の実装である `DbReader.GetFileDependencies` と、`cdidx impact` / MCP `impact_analysis` の heuristic file-hint フォールバックである `DbReader.GetFileDependencyHintsToResolvedType` が、これまで `symbol_references` を graph 対応言語でしか絞っておらず `reference_kind` で絞っていなかったため、`[JsonConverter(typeof(User))]` や `@Inject(User.class)` のような compile-time metadata として型に触れるだけで実行時依存ではない参照が依存エッジとして漏れていた(注釈ファイルが対象型に依存しているように見えた)。両クエリの logical reference CTE を `r.reference_kind IN CallGraphReferenceKindsSql ('call', 'instantiate', 'subscribe')` で追加絞り込みし、`callers` / `callees` / `hotspots` / `impact` BFS と同じ契約に揃えた。回帰テストを `DbReaderTests` に追加: `GetFileDependencies_ExcludesMetadataOnlyReferences` は attribute-only consumer が `deps` エッジを作らず `new User()` は作ることを固定し、`AnalyzeImpact_ExcludesMetadataOnlyReferencesFromHeuristicHints` は attribute-only consumer が heuristic file hint を 1 件も生成しないことを固定する。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 +- **`deps` のファイルレベル依存分析が metadata 参照 (`attribute` / `annotation`) を compile-time 依存として意図的に保持 (#293 追加対応)** — `cdidx deps` の実装である `DbReader.GetFileDependencies` と、`cdidx impact` / MCP `impact_analysis` の heuristic file-hint フォールバックである `DbReader.GetFileDependencyHintsToResolvedType` は、logical reference CTE に metadata 参照も含める。attribute クラスを rename / 削除すれば `[JsonConverterAttribute]` のような注釈サイトも compile-time で壊れるため、ファイル単位の blast-radius 分析はそれらも本物の依存 edge として表示する必要があり、`logical_reference_kind` では `attribute` / `annotation` として正しくラベル付けされる(分類修正前は同じサイトが phantom `call` として漏れていた)。なお `impact` の heuristic file-hint フォールバックは、`SourceFileHasStructuredTypeEvidence`(#293 以前から存在する、メンバー名衝突を防ぐための別レイヤーの guard)によって各 candidate source ファイルに構造化された型証拠があるかを追加チェックするため、他に該当型への構造化された利用を持たない metadata-only consumer ファイルは heuristic hint には出てこない。authoritative な metadata 依存エッジを見たい場合は `cdidx deps --path --reverse` を使う。`callers` / `callees` は dynamic call graph を扱うコマンドなので、`--kind attribute|annotation` は引き続き CLI / MCP boundary で拒否する(dependency graph とは別契約)。回帰テスト `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies` を追加し、`new JsonConverter(...)` と `[JsonConverter(...)]` の両方が attribute クラス定義ファイルへの `deps` エッジを作ることを固定する。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 - **`callers` / `callees` が CLI と MCP 境界で `--kind attribute|annotation` を拒否し、Swift / Gradle の `@` metadata を `annotation` に再分類 (#293 追加対応)** — metadata 行 (`attribute` / `annotation`) は注釈対象そのものではなく body-range の外側シンボルに帰属するため、`callers Obsolete --kind attribute` は `[Obsolete] void M()` を `M` ではなく外側クラスの下に黙って返し、`[assembly: CLSCompliant]` や Kotlin の `@file:JvmName` のような file-level target は `container_name = NULL` で完全脱落していた。`QueryCommandRunner.RunCallers` / `RunCallees` は `--kind attribute` / `--kind annotation` を `CommandExitCodes.UsageError` で弾き、MCP の `callers` / `callees` ツールは `isError` ツールレスポンスを返すようにした。どちらの経路も、metadata 列挙の正しいパスである `references --kind attribute|annotation` へ誘導する。あわせて `AnnotationLanguages` に `swift` と `gradle` を追加し、Swift の `@available(iOS 13.0, *)` / `@objc` / `@MainActor` や Gradle / Groovy の `@CompileStatic` / `@TaskAction` を `call` ではなく `annotation` として記録するようにした(修正前は引数付きの `@Name(args)` 形は `callers` / `callees` / `hotspots` / `impact` に phantom call edge として漏れ、引数なしの `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` 形はこの集合で gate されていた no-arg annotation 出力から外れてインデックス自体に残らなかった)。回帰テストとして `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError`(theory 4 ケース)、`McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError`(theory 4 ケース)、`ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`、`ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation` を追加。対象: `src/CodeIndex/Cli/QueryCommandRunner.cs`、`src/CodeIndex/Mcp/McpToolHandlers.cs`、`src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 - **`[\n Serializable\n]` のような複数行にまたがる引数なし C# 属性セクションを、属性引数内の識別子を誤って取り込まずに索引化 (#293 追加対応)** — 引数なし C# 属性の正規表現は、これまで `[` / `,` を左端、`]` / `,` を右端として同一行内でアンカーしていたため、行頭 `[` と行頭 `]` の間に書かれた ` Serializable` のような裸識別子が `references --kind attribute` から黙って脱落していた。左側は単語境界 lookbehind、右側は行末(`$`)も許容するように緩和した。一方で `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]` のように属性の引数リスト内で行末に現れる enum / 修飾定数識別子(`AllowNumbers` など)が no-arg 属性として誤分類される回帰を避けるため、`BuildCSharpAttributeRanges` は `(` / `)` で属性セクションを分割した paren 深さ 0 の「top-level ゾーン」テーブルも並列で出力するようにし、no-arg 属性の採用位置はそのテーブルに限定した(既存の引数付き metadata 分類パスは引き続き属性セクション全体のレンジを使う)。paren 深さは section-local に追跡する(各 `[` がその瞬間のグローバル paren 深さを snapshot として保持)ため、`void M([FromServices] IService s)` のようなパラメータ属性、`delegate void D([Attr] int x)` のようなデリゲートパラメータ属性、`Func f = ([Attr] int x) => x` のようなラムダパラメータ属性のように、`[` が開いた時点で既にグローバル paren 深さが 0 より大きいケースでも、属性リストの top-level を正しく認識できる。`SymbolExtractor` 側でも `BuildCSharpMatchLines` に `StripMultiLineCSharpAttributeInterior` を追加し、複数行にまたがる `[...]` セクションの深さを跨行で追跡して内部行を宣言 regex 適用前に空白化するようにした。これにより ` Serializable` がトップレベル `function` シンボルとして誤抽出され、`definitionNames` ガード経由で参照分類が詰まってしまうのを防ぐ。さらに、複数行の空白化起動条件を「同一行内で閉じない `[` があるかどうか」に広げ、行頭の `[` だけでなく `void M([\n FromServices\n] IService s)` のようなパラメータ属性、`class Bar<[\n TypeParamAttr\n] T>` のような型パラメータ属性、`delegate void D([\n DelegateParamAttr\n] int x)` のようなデリゲート / ラムダのパラメータ属性でも内部行が正しく空白化されるようにした — 以前は開口行で `[` が行頭にある場合しかカバーされていなかったため、上記の形の内部識別子が `function` シンボルとして誤抽出され、`references --kind attribute` から属性参照が黙って脱落していた。`[\n Serializable\n]`・`[\n global::System.Obsolete\n]`・`[Required,\n Key]`・`[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]`(`AllowNumbers` / `ConverterStrategy` が `attribute` として取り込まれないことを固定)に加え、上記の行頭でない `[` で開く複数行パラメータ / 型パラメータ / デリゲートパラメータ属性のケースも `SymbolExtractor.Extract` + `ReferenceExtractor.Extract` で end-to-end に回して SymbolExtractor 側の回帰も固定する回帰テストを追加。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Indexer/SymbolExtractor.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 - **`::` 修飾の C# 引数なし属性と JavaScript の `@Decorator` を正しく分類 (#293 追加対応)** — 引数なし C# 属性の正規表現が修飾子セパレータとして `.` に加えて `::` も受け付けるようになり、`[global::System.Obsolete]` や `[Alias::MyAttr]` のような形が `references --kind attribute` から黙って脱落しなくなった。`AnnotationLanguages` にも `javascript` を追加し、JavaScript はもともと graph 対応言語なので、`@sealed`(引数なし)や `@injectable()`(引数付き)のような JS decorator も `annotation` として再分類され、TypeScript と同じ契約で call-graph に phantom `call` 行を残さない。`[global::System.Obsolete]`・`[Alias::MyAttr]`・JavaScript の `@sealed` / `@injectable()` を押さえる回帰テストを追加。あわせて、以前に進めていた `callers` / `callees` の metadata 契約を README(英日)と MCP ツール説明の両方で引き戻し、metadata 行は注釈対象そのものではなく body-range 上の外側シンボル(クラス直下宣言ならクラス、`[assembly: ...]` のようなファイルレベル target では `container_name` が `null`)に帰属するため `kind: "attribute"` / `kind: "annotation"` を `callers` / `callees` に渡すのは信頼できる metadata 調査経路ではなく、metadata 列挙は `references --kind attribute|annotation` を使うよう案内する文言に修正した。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Mcp/McpToolDefinitions.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`、`README.md`。 diff --git a/src/CodeIndex/Database/DbReader.cs b/src/CodeIndex/Database/DbReader.cs index ef03a5a1d6..fab9f1e9ab 100644 --- a/src/CodeIndex/Database/DbReader.cs +++ b/src/CodeIndex/Database/DbReader.cs @@ -1666,16 +1666,20 @@ FROM symbols s " + GetLogicalReferenceKindSql("r.reference_kind") + @" AS logical_reference_kind FROM symbol_references r JOIN files src ON r.file_id = src.id - WHERE src.path != @impactTargetPath - AND r.reference_kind IN " + CallGraphReferenceKindsSql; - // Metadata reference kinds (`attribute`, `annotation`) only touch a type/symbol - // as compile-time metadata and should NOT form a logical file dependency edge - // (e.g. `[JsonConverter(typeof(User))]` or `@Inject(User.class)` must not make - // the annotated file depend on `User`). Restrict to call-graph kinds. - // `attribute` / `annotation` は compile-time metadata で型や記号に触れるだけなので、 - // ファイル間の論理的な依存 edge として扱ってはいけない(例: - // `[JsonConverter(typeof(User))]` / `@Inject(User.class)` で annotated ファイルが - // `User` に依存しているかのように見える)。call-graph kind に絞る。 + WHERE src.path != @impactTargetPath"; + // `impact` heuristic file hints intentionally include metadata-only reference + // kinds (`attribute` / `annotation`). A rename or removal of `User` breaks + // `[JsonConverter(typeof(User))]` / `@Inject(User.class)` at compile time just + // as surely as it breaks `new User()`, so file-level blast-radius analysis + // must surface those sites as real dependencies. `callers` / `callees` still + // reject metadata kinds at the CLI / MCP boundary because those commands model + // the dynamic call graph, not the dependency graph. + // `impact` の heuristic file hint は metadata-only な参照 (`attribute` / + // `annotation`) も意図的に含める。`User` を rename / 削除すると + // `[JsonConverter(typeof(User))]` / `@Inject(User.class)` も compile-time で + // 壊れるため、ファイル単位の blast-radius 分析ではそれらも本物の依存として + // 出す必要がある。`callers` / `callees` は call graph を扱うので、metadata 種別 + // の拒否は引き続き CLI / MCP boundary 側で行う。 innerSql += $" AND {BuildGraphSupportedLanguagePredicate(cmd, "src", "impactDepsLang")}"; if (lang != null) innerSql += " AND src.lang = @lang"; @@ -2326,13 +2330,22 @@ WITH logical_references AS ( " + GetLogicalReferenceKindSql("r.reference_kind") + @" AS logical_reference_kind FROM symbol_references r JOIN files src ON r.file_id = src.id - WHERE 1 = 1 - AND r.reference_kind IN " + CallGraphReferenceKindsSql; - // Same rationale as GetFileDependencyHintsToResolvedType: metadata kinds - // (`attribute` / `annotation`) must not form dependency edges because they - // only record compile-time metadata usage, not logical runtime dependencies. - // `GetFileDependencyHintsToResolvedType` と同じ理由で、`attribute` / `annotation` - // は実行時の論理依存ではないため、file-level dependency edge を作らない。 + WHERE 1 = 1"; + // `deps` intentionally includes metadata-only reference kinds + // (`attribute` / `annotation`). Same rationale as + // `GetFileDependencyHintsToResolvedType`: renaming or removing a type that + // is only referenced via `[JsonConverter(typeof(User))]` or + // `@Inject(User.class)` still breaks the annotated file at compile time, so + // file-level dependency analysis must treat those sites as real edges. + // Call-graph-specific commands (`callers` / `callees`) keep rejecting + // metadata kinds at the CLI / MCP boundary — that is a separate contract. + // `deps` は metadata-only 参照 (`attribute` / `annotation`) も意図的に + // 含める。`GetFileDependencyHintsToResolvedType` と同じ理由で、 + // `[JsonConverter(typeof(User))]` や `@Inject(User.class)` 経由でしか参照 + // されない型でも、rename / 削除すれば annotated ファイルは compile-time + // で壊れるため、ファイル単位の依存分析では本物の edge として扱う必要が + // ある。call-graph 専用コマンド (`callers` / `callees`) 側では metadata + // 種別の拒否を CLI / MCP boundary で引き続き行う — そちらは別契約。 sql += $" AND {BuildGraphSupportedLanguagePredicate(cmd, "src", "depsLang")}"; if (lang != null) sql += " AND src.lang = @lang"; diff --git a/tests/CodeIndex.Tests/DbReaderTests.cs b/tests/CodeIndex.Tests/DbReaderTests.cs index 881d1bf05e..b412b65ccf 100644 --- a/tests/CodeIndex.Tests/DbReaderTests.cs +++ b/tests/CodeIndex.Tests/DbReaderTests.cs @@ -2250,65 +2250,65 @@ def Run(): } [Fact] - public void GetFileDependencies_ExcludesMetadataOnlyReferences() - { - // issue #293 follow-up: `[JsonConverter(typeof(User))]` touches `User` only as - // compile-time metadata and must NOT create a dependency edge from the annotated - // file to `User.cs`. Before the fix, `GetFileDependencies` filtered by graph-supported - // language but not by `reference_kind`, so metadata-only references (attribute/annotation) - // leaked into `cdidx deps` output. - // issue #293 補足: `[JsonConverter(typeof(User))]` は compile-time metadata として - // `User` に触れるだけなので、注釈ファイルから `User.cs` への依存 edge を作ってはいけない。 - // 修正前は `GetFileDependencies` が graph-supported 言語しか絞っておらず - // `reference_kind` で絞っていなかったため、metadata-only 参照 (attribute/annotation) が - // `cdidx deps` 出力に漏れていた。 - InsertIndexedFile("src/User.cs", "csharp", - """ - public class User - { - public string Name { get; set; } = ""; - } - """); - // Metadata-only usage — attribute typeof(User), no real runtime dependency. - // metadata-only の利用 (attribute typeof(User))。実行時の依存関係は無い。 - InsertIndexedFile("src/Serializer.cs", "csharp", + public void GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies() + { + // issue #293 follow-up: the attribute class `JsonConverter` is referenced + // both as a runtime `new JsonConverter(...)` call AND as compile-time + // attribute metadata `[JsonConverter(...)]`. Renaming or removing the + // class breaks both sites, so `cdidx deps` MUST surface both edges as + // real file-level dependencies. (`callers` / `callees` stay call-graph- + // only and reject `--kind attribute|annotation` separately at the CLI / + // MCP boundary — that is a different contract.) + // issue #293 補足: attribute クラス `JsonConverter` は runtime の + // `new JsonConverter(...)` としても、compile-time の `[JsonConverter(...)]` + // 属性 metadata としても参照される。クラスを rename / 削除すれば両方の + // サイトが壊れるため、`cdidx deps` は両方のエッジをファイル単位の本物の + // 依存として出す必要がある。(`callers` / `callees` は call-graph 専用で、 + // metadata 種別は CLI / MCP boundary 側で別途拒否する) + InsertIndexedFile("src/JsonConverterAttribute.cs", "csharp", """ using System; [AttributeUsage(AttributeTargets.Class)] - public class JsonConverterAttribute : Attribute + public class JsonConverter : Attribute { public Type ConverterType { get; } - public JsonConverterAttribute(Type converterType) => ConverterType = converterType; + public JsonConverter(Type converterType) => ConverterType = converterType; } - - [JsonConverter(typeof(User))] + """); + // Metadata-only usage — attribute form. Compile-time dependency: renaming + // `JsonConverter` breaks this file at build time. + // metadata-only の利用 (attribute 形式)。compile-time 依存: + // `JsonConverter` を rename すればこのファイルも build-time で壊れる。 + InsertIndexedFile("src/Serializer.cs", "csharp", + """ + [JsonConverter(typeof(int))] public class SerializerConfig { } """); - // Real runtime dependency — `new User()` is a `call`/`instantiate` kind edge. - // 実際の実行時依存 — `new User()` は `call`/`instantiate` 種別の edge。 + // Runtime dependency — `new JsonConverter(...)` is a `call` / `instantiate` edge. + // 実行時の依存 — `new JsonConverter(...)` は `call` / `instantiate` 種別の edge。 InsertIndexedFile("src/Caller.cs", "csharp", """ public class Caller { public void Do() { - var u = new User(); - System.Console.WriteLine(u.Name); + var c = new JsonConverter(typeof(int)); } } """); var dependencies = _reader.GetFileDependencies(limit: 10, lang: "csharp"); - // Only Caller.cs → User.cs via `new User()` should remain. - // `new User()` 経由の Caller.cs → User.cs のみが残る。 - Assert.Single(dependencies); - Assert.Equal("src/Caller.cs", dependencies[0].SourcePath); - Assert.Equal("src/User.cs", dependencies[0].TargetPath); - Assert.DoesNotContain(dependencies, d => d.SourcePath == "src/Serializer.cs"); + // Both Caller.cs (runtime `instantiate`) and Serializer.cs (attribute + // metadata) must appear as dependencies of JsonConverterAttribute.cs. + // Caller.cs (runtime `instantiate`) と Serializer.cs (attribute metadata) + // の両方が JsonConverterAttribute.cs への依存として現れる。 + Assert.Equal(2, dependencies.Count); + Assert.Contains(dependencies, d => d.SourcePath == "src/Caller.cs" && d.TargetPath == "src/JsonConverterAttribute.cs"); + Assert.Contains(dependencies, d => d.SourcePath == "src/Serializer.cs" && d.TargetPath == "src/JsonConverterAttribute.cs"); } [Fact] @@ -2581,69 +2581,6 @@ public void Run(FolderDiffService service) Assert.Contains("ExecuteFolderDiffAsync", edge.Symbols); } - [Fact] - public void AnalyzeImpact_ExcludesMetadataOnlyReferencesFromHeuristicHints() - { - // issue #293 follow-up: when `impact` falls back to file-dependency hints - // (heuristic mode), metadata-only references must not produce phantom edges. - // `[JsonConverter(typeof(User))]` touches `User` only as attribute metadata - // and must not make the annotated file show up as a file-level impact of `User`. - // Before the fix, `GetFileDependencyHintsToResolvedType` did not filter by - // `reference_kind` so attribute/annotation rows leaked into the hint output. - // issue #293 補足: `impact` が file-dependency hints にフォールバックする heuristic mode でも - // metadata-only 参照は phantom edge を作ってはいけない。`[JsonConverter(typeof(User))]` は - // attribute metadata として `User` に触れるだけで、注釈ファイルを `User` の file-level - // impact として表示してはいけない。修正前は `GetFileDependencyHintsToResolvedType` が - // `reference_kind` で絞っておらず attribute/annotation 行が hint 出力に漏れていた。 - InsertIndexedFile("src/MetadataUser.cs", "csharp", - """ - public class MetadataUser - { - public string Name { get; set; } = ""; - } - """); - // Attribute uses MetadataUser via typeof — metadata only, no runtime dep. - // No other file references MetadataUser, so there are no call-graph callers and - // the impact analysis falls through to the heuristic file-dependency-hint mode. - // Before the fix, the attribute-only reference would leak into that hint output; - // after the fix, it must be filtered out and hint count must be 0. - // 属性が typeof で MetadataUser に触れるだけ。実行時の依存は無い。他のファイルからは - // 参照していないため call-graph caller は 0 件となり、impact 分析は heuristic な - // file-dependency-hint モードに落ちる。修正前はこの hint に attribute-only 参照が - // 漏れていたが、修正後はフィルタで除外され hint 件数は 0 になる。 - InsertIndexedFile("src/AttributeConsumer.cs", "csharp", - """ - using System; - - [AttributeUsage(AttributeTargets.Class)] - public class MetadataAttribute : Attribute - { - public Type T { get; } - public MetadataAttribute(Type t) => T = t; - } - - [Metadata(typeof(MetadataUser))] - public class AttributeConsumer - { - } - """); - - var analysis = _reader.AnalyzeImpact("MetadataUser", maxDepth: 3, limit: 10); - - // With no real runtime caller and the attribute-only reference filtered out by the - // call-graph reference_kind predicate, there are 0 callers AND 0 heuristic file - // hints. Before the fix, the attribute row would leak into the hint output and the - // attribute-only consumer file would show up as a phantom dependency. - // 実行時 caller が無く、attribute-only 参照も call-graph reference_kind 述語で - // 除外されるため、caller は 0 件 / heuristic file hint も 0 件。修正前は attribute - // 行が hint 出力に漏れ、attribute-only consumer が phantom 依存として出ていた。 - Assert.True(analysis.HasClassLikeDefinitions); - Assert.Empty(analysis.Callers); - Assert.DoesNotContain(analysis.FileImpacts, e => e.SourcePath == "src/AttributeConsumer.cs"); - Assert.Empty(analysis.FileImpacts); - Assert.Equal(0, analysis.HintCount); - } - [Fact] public void AnalyzeImpact_ClassAndNamespaceWithSameName_StillReturnsHeuristicHints() { From fb1e37ef3d34b1ddb459dc7d3fc0fecb0baf9b1a Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 18 Apr 2026 11:58:19 +0900 Subject: [PATCH 16/30] Fix #293 canonicalize C# attribute suffix and include metadata consumers in deps/impact - GetFileDependencies SQL now UNION-ALLs a suffix-aliased C# attribute row (symbol_name || 'Attribute') so [MyAudit] joins to the MyAuditAttribute class file without requiring a separate alias resolution pass. - GetFileDependencyHintsToResolvedType aggregates has_metadata_ref per candidate and bypasses SourceFileHasStructuredTypeEvidence for pure-metadata edges, so `impact MyAuditAttribute` surfaces `[MyAudit] class Svc` consumer files even when they have no other structured type usage. - ResolveImpactFallbackNames adds suffix-stripped aliases for C# class names ending in `Attribute`, so the impact heuristic can query via the shorthand symbol_name form that the reference extractor actually stores. - Added DbReaderTests.GetFileDependencies_MatchesCSharpAttributeSuffixConvention pinning the end-to-end [MyAudit] -> MyAuditAttribute deps edge. Co-Authored-By: Claude Opus 4 --- CHANGELOG.md | 4 +- src/CodeIndex/Database/DbReader.cs | 61 ++++++++++++++++++++++++-- tests/CodeIndex.Tests/DbReaderTests.cs | 37 ++++++++++++++++ 3 files changed, 97 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index faa3f3aa3f..258b0367cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), #### Fixed - **MCP `callers` / `callees` `kind` schema, CLI help, and README `--kind` option row no longer advertise metadata kinds as valid filter values (#293 follow-up)** — The `callers` / `callees` handlers reject `kind: "attribute"` / `kind: "annotation"` with a usage error (CLI) / `isError` tool response (MCP), but the MCP tool schema's `kind` property description still listed all five kinds as if they were valid filter values (`"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"`), `cdidx --help` similarly lumped `callers` / `callees` together with `references` for the `--kind` help row, and the README `--kind` option table (EN line 436 / JP line 1307) still directed readers to pass `--kind attribute` or `--kind annotation` on `callers` / `callees`. Schema and handler now agree: the `callers` / `callees` `kind` description now reads `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."`, the CLI `--kind` help row splits `references` (accepts all five kinds) from `callers` / `callees` (call-graph kinds only, metadata rejected, redirects to `references`), and the README `--kind` row (both English and Japanese) now explicitly states that `callers` / `callees` reject `--kind attribute|annotation` with a usage error and points to `references --kind attribute|annotation` as the correct metadata-enumeration path. The MCP `references` tool description is unchanged because it still accepts every indexed kind. Added `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` to pin the corrected schema wording. Affected: `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `src/CodeIndex/Cli/ConsoleUi.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `README.md`. -- **`deps` file-level dependency analysis intentionally retains metadata references (`attribute` / `annotation`) as compile-time dependency edges (#293 follow-up)** — `DbReader.GetFileDependencies` (backing `cdidx deps`) and `DbReader.GetFileDependencyHintsToResolvedType` (backing `cdidx impact` / MCP `impact_analysis` heuristic file-hint fallback) keep metadata references in their logical reference CTE. Renaming or removing an attribute class still breaks annotated sites such as `[JsonConverterAttribute]` at compile time, so file-level blast-radius analysis must surface those edges as real dependencies — now labeled `attribute` / `annotation` in `logical_reference_kind` (previously these same sites leaked in as phantom `call` rows before classification was corrected). The `impact` heuristic file-hint fallback additionally enforces `SourceFileHasStructuredTypeEvidence` on each candidate source file (a pre-existing guard that predates #293 and protects against member-name collisions), so metadata-only consumer files without other structured type evidence will not surface through that heuristic; users should run `cdidx deps --path --reverse` to see the authoritative metadata dependency edges. `callers` / `callees` still reject `--kind attribute|annotation` at the CLI / MCP boundary because those commands model the dynamic call graph, not the dependency graph. Added regression coverage `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies` pinning that both `new JsonConverter(...)` and `[JsonConverter(...)]` produce a `deps` edge to the attribute-class-defining file. Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. +- **`deps` and `impact` retain metadata references as compile-time dependency edges and canonicalize the C# attribute-suffix convention (#293 follow-up)** — `DbReader.GetFileDependencies` (backing `cdidx deps`) and `DbReader.GetFileDependencyHintsToResolvedType` (backing `cdidx impact` / MCP `impact_analysis` heuristic file-hint fallback) keep metadata references in their logical reference CTE. Renaming or removing an attribute class still breaks annotated sites such as `[JsonConverterAttribute]` at compile time, so file-level blast-radius analysis must surface those edges as real dependencies — now labeled `attribute` / `annotation` in `logical_reference_kind` (previously these same sites leaked in as phantom `call` rows before classification was corrected). `GetFileDependencies` additionally canonicalizes the C# attribute naming convention: idiomatic `[Foo]` usage is stored with `symbol_name = "Foo"` but the defining class is `FooAttribute`, so the logical-reference CTE now UNION-ALLs a suffix-aliased copy of every C# `attribute`-kind row with `'Attribute'` appended (only when the original symbol_name does not already end in `Attribute`) so the target-file join reaches the attribute class file. `GetFileDependencyHintsToResolvedType` also aggregates a per-candidate `has_metadata_ref` flag and bypasses the `SourceFileHasStructuredTypeEvidence` guard for edges whose contributing references are purely metadata, so pure-attribute consumer files such as `[MyAudit] class Svc` now surface through `impact MyAuditAttribute` instead of being filtered out. `ResolveImpactFallbackNames` symmetrically adds suffix-stripped aliases for C# class names ending in `Attribute`, so the BFS-less file-hint path can find `[Foo]` sites whose source symbol_name does not match the defining class name. `callers` / `callees` still reject `--kind attribute|annotation` at the CLI / MCP boundary because those commands model the dynamic call graph, not the dependency graph. Added regression coverage `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies` (runtime + metadata edges both surface) and `DbReaderTests.GetFileDependencies_MatchesCSharpAttributeSuffixConvention` (`[MyAudit]` → `MyAuditAttribute` canonicalization). Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. - **`callers` / `callees` now reject `--kind attribute|annotation` at the CLI and MCP boundary, and Swift / Gradle `@` metadata is reclassified as `annotation` (#293 follow-up)** — Metadata rows (`attribute` / `annotation`) are attributed to their enclosing body-range symbol rather than the annotated target, so `callers Obsolete --kind attribute` would silently return `[Obsolete] void M()` under the enclosing class (not `M`), and file-level targets such as `[assembly: CLSCompliant]` or Kotlin `@file:JvmName` would drop entirely because `container_name` is `NULL`. `QueryCommandRunner.RunCallers` / `RunCallees` now reject `--kind attribute` and `--kind annotation` with `CommandExitCodes.UsageError`, and the MCP `callers` / `callees` tools return an `isError` tool response; both paths direct users to `references --kind attribute|annotation`, which IS a correct metadata enumeration path. Separately, `AnnotationLanguages` now includes `swift` and `gradle`, so Swift `@available(iOS 13.0, *)` / `@objc` / `@MainActor` and Gradle/Groovy `@CompileStatic` / `@TaskAction` are recorded as `annotation` instead of `call` (previously the `@Name(args)` forms leaked into `callers` / `callees` / `hotspots` / `impact` as phantom call edges, and the no-arg `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` forms dropped from the index entirely because no-arg annotation emission was gated to languages in this set). Added `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError` (theory, 4 cases), `McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError` (theory, 4 cases), `ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`, and `ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation`. Affected: `src/CodeIndex/Cli/QueryCommandRunner.cs`, `src/CodeIndex/Mcp/McpToolHandlers.cs`, `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. - **Multi-line no-arg C# attribute sections like `[\n Serializable\n]` are now indexed without over-matching attribute argument identifiers (#293 follow-up)** — The no-arg C# attribute regex previously required the match to start at a `[` / `,` boundary and end at `]` / `,` on the same line, so a bare identifier on the interior line of a multi-line attribute section (e.g. ` Serializable` between a line-leading `[` and a line-leading `]`) was silently dropped from `references --kind attribute`. The left anchor is now a word-boundary lookbehind and the right anchor also accepts end-of-line. To avoid misclassifying enum / qualified-constant identifiers that happen to sit at end-of-line inside an attribute argument list (e.g. `ConverterStrategy.AllowNumbers` in `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]`) as no-arg attributes, `BuildCSharpAttributeRanges` now also emits a parallel "top-level zone" table that splits each attribute section by `(` / `)` so only paren-depth-0 positions inside the section qualify as no-arg attribute name positions. Paren depth is tracked section-locally (each `[` snapshots the global depth at open time) so parameter attributes like `void M([FromServices] IService s)`, delegate parameter attributes like `delegate void D([Attr] int x)`, and lambda parameter attributes like `Func f = ([Attr] int x) => x` — whose `[` opens while the global paren depth is already > 0 — still have their attribute-list top level correctly identified. The no-arg attribute path uses that top-level table instead of the full attribute-section ranges, while the existing with-args metadata classification still uses the full ranges. `SymbolExtractor` also now tracks multi-line `[...]` depth across lines via `StripMultiLineCSharpAttributeInterior` in `BuildCSharpMatchLines`, so interior lines of a multi-line attribute section are blanked out before declaration regexes run — this prevents ` Serializable` from being mis-extracted as a top-level `function` symbol (which then masked the reference through the `definitionNames` guard). The multi-line blanker now activates on any `[` that opens without a matching `]` on the same line, not just `[` that sits at the start of the line, so multi-line parameter attributes like `void M([\n FromServices\n] IService s)`, type-parameter attributes like `class Bar<[\n TypeParamAttr\n] T>`, and delegate/lambda parameter attributes like `delegate void D([\n DelegateParamAttr\n] int x)` are also blanked out correctly — previously only the leading-`[` case was covered, so the interior identifiers of those forms were mis-extracted as `function` symbols and their attribute references were silently dropped from `references --kind attribute`. Regression coverage adds `[\n Serializable\n]`, `[\n global::System.Obsolete\n]`, `[Required,\n Key]`, `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]` (pinning that `AllowNumbers` / `ConverterStrategy` are NOT classified as `attribute`), and the non-leading-`[` parameter / type-parameter / delegate-parameter multi-line cases above, running through `SymbolExtractor.Extract` + `ReferenceExtractor.Extract` end-to-end so both sides of the regression stay pinned. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Indexer/SymbolExtractor.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. - **`::`-qualified C# no-arg attributes and JavaScript `@Decorator` usages are now classified correctly (#293 follow-up)** — The no-arg C# attribute regex now accepts `::` alongside `.` as a qualifier separator, so `[global::System.Obsolete]` and `[Alias::MyAttr]` reach `references --kind attribute` instead of being silently dropped. `AnnotationLanguages` additionally includes `javascript`, so JS decorators such as `@sealed` (no-arg) and `@injectable()` (with args) are reclassified to `annotation` rather than leaking into the call-graph as phantom `call` rows — matching the existing TypeScript contract since JavaScript is already a graph-supported language. Added regression tests pinning `[global::System.Obsolete]`, `[Alias::MyAttr]`, and JavaScript `@sealed` / `@injectable()`. Also rewrote the README `callers` row (EN + JP) and the MCP tool descriptions for `callers` / `callees` to pull back the earlier over-promise that `kind: "attribute"` / `kind: "annotation"` on `callers` / `callees` is a supported metadata-inspection path: a metadata row is attributed to its enclosing body-range symbol (for a class-level declaration, that is the class itself) and drops entirely for file-level targets such as `[assembly: ...]` where `container_name` is `null`, so the docs now direct users to `references --kind attribute|annotation` for metadata enumeration. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`, `README.md`. @@ -686,7 +686,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), #### 修正 - **MCP の `callers` / `callees` の `kind` schema・CLI help・README の `--kind` オプション行が metadata kinds を有効フィルタ値として広告しないよう修正 (#293 追加対応)** — `callers` / `callees` の handler は `kind: "attribute"` / `kind: "annotation"` を usage error(CLI)/ `isError` ツールレスポンス(MCP)で拒否するにもかかわらず、MCP ツール schema の `kind` プロパティ説明は 5 種すべてを列挙した `"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"` のままで、`cdidx --help` の `--kind` 行も `callers` / `callees` を `references` と同じ行にまとめていた。さらに README の `--kind` オプション表(英語 436 行目 / 日本語 1307 行目)も、`callers` / `callees` で `--kind attribute` / `--kind annotation` を渡せるかのように案内していた。schema と handler の契約を一致させ、`callers` / `callees` の `kind` description を `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."` に修正し、CLI の `--kind` help 行も `references`(5 種すべて受理)と `callers` / `callees`(call-graph 種別のみ、metadata kinds は拒否して `references` へ誘導)を分けるようにし、README の `--kind` 行(英語・日本語の両方)も `callers` / `callees` が `--kind attribute|annotation` を usage error で拒否すること、metadata 列挙の正規経路が `references --kind attribute|annotation` であることを明示する文言に直した。MCP の `references` ツール説明は全 reference kind を受け付ける契約のままなので変更なし。修正後の schema 文言を固定する `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` を追加。対象: `src/CodeIndex/Mcp/McpToolDefinitions.cs`、`src/CodeIndex/Cli/ConsoleUi.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`、`README.md`。 -- **`deps` のファイルレベル依存分析が metadata 参照 (`attribute` / `annotation`) を compile-time 依存として意図的に保持 (#293 追加対応)** — `cdidx deps` の実装である `DbReader.GetFileDependencies` と、`cdidx impact` / MCP `impact_analysis` の heuristic file-hint フォールバックである `DbReader.GetFileDependencyHintsToResolvedType` は、logical reference CTE に metadata 参照も含める。attribute クラスを rename / 削除すれば `[JsonConverterAttribute]` のような注釈サイトも compile-time で壊れるため、ファイル単位の blast-radius 分析はそれらも本物の依存 edge として表示する必要があり、`logical_reference_kind` では `attribute` / `annotation` として正しくラベル付けされる(分類修正前は同じサイトが phantom `call` として漏れていた)。なお `impact` の heuristic file-hint フォールバックは、`SourceFileHasStructuredTypeEvidence`(#293 以前から存在する、メンバー名衝突を防ぐための別レイヤーの guard)によって各 candidate source ファイルに構造化された型証拠があるかを追加チェックするため、他に該当型への構造化された利用を持たない metadata-only consumer ファイルは heuristic hint には出てこない。authoritative な metadata 依存エッジを見たい場合は `cdidx deps --path --reverse` を使う。`callers` / `callees` は dynamic call graph を扱うコマンドなので、`--kind attribute|annotation` は引き続き CLI / MCP boundary で拒否する(dependency graph とは別契約)。回帰テスト `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies` を追加し、`new JsonConverter(...)` と `[JsonConverter(...)]` の両方が attribute クラス定義ファイルへの `deps` エッジを作ることを固定する。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 +- **`deps` と `impact` が metadata 参照を compile-time 依存として保持し、C# の attribute サフィックス命名規約を正規化 (#293 追加対応)** — `cdidx deps` の実装である `DbReader.GetFileDependencies` と、`cdidx impact` / MCP `impact_analysis` の heuristic file-hint フォールバックである `DbReader.GetFileDependencyHintsToResolvedType` は、logical reference CTE に metadata 参照も含める。attribute クラスを rename / 削除すれば `[JsonConverterAttribute]` のような注釈サイトも compile-time で壊れるため、ファイル単位の blast-radius 分析はそれらも本物の依存 edge として表示する必要があり、`logical_reference_kind` では `attribute` / `annotation` として正しくラベル付けされる(分類修正前は同じサイトが phantom `call` として漏れていた)。さらに `GetFileDependencies` は C# の attribute 命名規約を正規化する: 慣用的な `[Foo]` は `symbol_name = "Foo"` で保存されるが、定義クラスは `FooAttribute` 命名となるため、logical-reference CTE に C# `attribute` 種別の行をサフィックス `'Attribute'` を付与した別名コピーとして UNION ALL で追加する(元の symbol_name が既に `Attribute` で終わっている場合は重複しないようスキップ)。これにより target ファイル join が attribute クラス側のファイルへ到達する。`GetFileDependencyHintsToResolvedType` はさらに candidate ごとに `has_metadata_ref` フラグを集約し、寄与する参照が metadata だけである edge については `SourceFileHasStructuredTypeEvidence` guard をバイパスするため、`[MyAudit] class Svc` のような純 attribute consumer ファイルも `impact MyAuditAttribute` で出るようになった(以前は filter で落ちていた)。`ResolveImpactFallbackNames` も対称的に、`Attribute` で終わる C# クラス名にはサフィックスを外した別名を追加するため、BFS を使わない file-hint 経路でも source symbol_name が定義クラス名と一致しない `[Foo]` サイトを発見できる。`callers` / `callees` は dynamic call graph を扱うコマンドなので、`--kind attribute|annotation` は引き続き CLI / MCP boundary で拒否する(dependency graph とは別契約)。回帰テスト `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies`(runtime と metadata の両方の edge が出ることを固定)と `DbReaderTests.GetFileDependencies_MatchesCSharpAttributeSuffixConvention`(`[MyAudit]` → `MyAuditAttribute` の正規化を固定)を追加。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 - **`callers` / `callees` が CLI と MCP 境界で `--kind attribute|annotation` を拒否し、Swift / Gradle の `@` metadata を `annotation` に再分類 (#293 追加対応)** — metadata 行 (`attribute` / `annotation`) は注釈対象そのものではなく body-range の外側シンボルに帰属するため、`callers Obsolete --kind attribute` は `[Obsolete] void M()` を `M` ではなく外側クラスの下に黙って返し、`[assembly: CLSCompliant]` や Kotlin の `@file:JvmName` のような file-level target は `container_name = NULL` で完全脱落していた。`QueryCommandRunner.RunCallers` / `RunCallees` は `--kind attribute` / `--kind annotation` を `CommandExitCodes.UsageError` で弾き、MCP の `callers` / `callees` ツールは `isError` ツールレスポンスを返すようにした。どちらの経路も、metadata 列挙の正しいパスである `references --kind attribute|annotation` へ誘導する。あわせて `AnnotationLanguages` に `swift` と `gradle` を追加し、Swift の `@available(iOS 13.0, *)` / `@objc` / `@MainActor` や Gradle / Groovy の `@CompileStatic` / `@TaskAction` を `call` ではなく `annotation` として記録するようにした(修正前は引数付きの `@Name(args)` 形は `callers` / `callees` / `hotspots` / `impact` に phantom call edge として漏れ、引数なしの `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` 形はこの集合で gate されていた no-arg annotation 出力から外れてインデックス自体に残らなかった)。回帰テストとして `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError`(theory 4 ケース)、`McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError`(theory 4 ケース)、`ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`、`ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation` を追加。対象: `src/CodeIndex/Cli/QueryCommandRunner.cs`、`src/CodeIndex/Mcp/McpToolHandlers.cs`、`src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 - **`[\n Serializable\n]` のような複数行にまたがる引数なし C# 属性セクションを、属性引数内の識別子を誤って取り込まずに索引化 (#293 追加対応)** — 引数なし C# 属性の正規表現は、これまで `[` / `,` を左端、`]` / `,` を右端として同一行内でアンカーしていたため、行頭 `[` と行頭 `]` の間に書かれた ` Serializable` のような裸識別子が `references --kind attribute` から黙って脱落していた。左側は単語境界 lookbehind、右側は行末(`$`)も許容するように緩和した。一方で `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]` のように属性の引数リスト内で行末に現れる enum / 修飾定数識別子(`AllowNumbers` など)が no-arg 属性として誤分類される回帰を避けるため、`BuildCSharpAttributeRanges` は `(` / `)` で属性セクションを分割した paren 深さ 0 の「top-level ゾーン」テーブルも並列で出力するようにし、no-arg 属性の採用位置はそのテーブルに限定した(既存の引数付き metadata 分類パスは引き続き属性セクション全体のレンジを使う)。paren 深さは section-local に追跡する(各 `[` がその瞬間のグローバル paren 深さを snapshot として保持)ため、`void M([FromServices] IService s)` のようなパラメータ属性、`delegate void D([Attr] int x)` のようなデリゲートパラメータ属性、`Func f = ([Attr] int x) => x` のようなラムダパラメータ属性のように、`[` が開いた時点で既にグローバル paren 深さが 0 より大きいケースでも、属性リストの top-level を正しく認識できる。`SymbolExtractor` 側でも `BuildCSharpMatchLines` に `StripMultiLineCSharpAttributeInterior` を追加し、複数行にまたがる `[...]` セクションの深さを跨行で追跡して内部行を宣言 regex 適用前に空白化するようにした。これにより ` Serializable` がトップレベル `function` シンボルとして誤抽出され、`definitionNames` ガード経由で参照分類が詰まってしまうのを防ぐ。さらに、複数行の空白化起動条件を「同一行内で閉じない `[` があるかどうか」に広げ、行頭の `[` だけでなく `void M([\n FromServices\n] IService s)` のようなパラメータ属性、`class Bar<[\n TypeParamAttr\n] T>` のような型パラメータ属性、`delegate void D([\n DelegateParamAttr\n] int x)` のようなデリゲート / ラムダのパラメータ属性でも内部行が正しく空白化されるようにした — 以前は開口行で `[` が行頭にある場合しかカバーされていなかったため、上記の形の内部識別子が `function` シンボルとして誤抽出され、`references --kind attribute` から属性参照が黙って脱落していた。`[\n Serializable\n]`・`[\n global::System.Obsolete\n]`・`[Required,\n Key]`・`[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]`(`AllowNumbers` / `ConverterStrategy` が `attribute` として取り込まれないことを固定)に加え、上記の行頭でない `[` で開く複数行パラメータ / 型パラメータ / デリゲートパラメータ属性のケースも `SymbolExtractor.Extract` + `ReferenceExtractor.Extract` で end-to-end に回して SymbolExtractor 側の回帰も固定する回帰テストを追加。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Indexer/SymbolExtractor.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 - **`::` 修飾の C# 引数なし属性と JavaScript の `@Decorator` を正しく分類 (#293 追加対応)** — 引数なし C# 属性の正規表現が修飾子セパレータとして `.` に加えて `::` も受け付けるようになり、`[global::System.Obsolete]` や `[Alias::MyAttr]` のような形が `references --kind attribute` から黙って脱落しなくなった。`AnnotationLanguages` にも `javascript` を追加し、JavaScript はもともと graph 対応言語なので、`@sealed`(引数なし)や `@injectable()`(引数付き)のような JS decorator も `annotation` として再分類され、TypeScript と同じ契約で call-graph に phantom `call` 行を残さない。`[global::System.Obsolete]`・`[Alias::MyAttr]`・JavaScript の `@sealed` / `@injectable()` を押さえる回帰テストを追加。あわせて、以前に進めていた `callers` / `callees` の metadata 契約を README(英日)と MCP ツール説明の両方で引き戻し、metadata 行は注釈対象そのものではなく body-range 上の外側シンボル(クラス直下宣言ならクラス、`[assembly: ...]` のようなファイルレベル target では `container_name` が `null`)に帰属するため `kind: "attribute"` / `kind: "annotation"` を `callers` / `callees` に渡すのは信頼できる metadata 調査経路ではなく、metadata 列挙は `references --kind attribute|annotation` を使うよう案内する文言に修正した。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Mcp/McpToolDefinitions.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`、`README.md`。 diff --git a/src/CodeIndex/Database/DbReader.cs b/src/CodeIndex/Database/DbReader.cs index fab9f1e9ab..8a53492171 100644 --- a/src/CodeIndex/Database/DbReader.cs +++ b/src/CodeIndex/Database/DbReader.cs @@ -1649,6 +1649,28 @@ FROM symbols s using var reader = cmd.ExecuteTrackedReader(); while (reader.TrackedRead()) results.Add(reader.GetString(0)); + + // C# attribute naming convention: a class `FooAttribute` is used as `[Foo]` in source, + // so reference sites are stored with symbol_name `Foo`. Add the suffix-stripped alias + // so impact on `FooAttribute` can find metadata-only usage sites. + // C# の属性命名規約: クラス `FooAttribute` はソースで `[Foo]` として使われ、参照サイトは + // symbol_name `Foo` で保存される。`FooAttribute` への impact でも metadata 参照サイトを + // 見つけられるよう、サフィックスを外した別名を追加する。 + if (string.Equals(definition.Lang, "csharp", StringComparison.OrdinalIgnoreCase)) + { + var aliases = new List(); + foreach (var name in results) + { + if (name.Length > "Attribute".Length && name.EndsWith("Attribute", StringComparison.Ordinal)) + { + var stripped = name.Substring(0, name.Length - "Attribute".Length); + if (stripped.Length > 0 && !results.Contains(stripped) && !aliases.Contains(stripped)) + aliases.Add(stripped); + } + } + results.AddRange(aliases); + } + return results; } @@ -1707,7 +1729,8 @@ FROM symbol_references r cmd.CommandText = $@" SELECT source_file_id, source_path, target_path, COUNT(*) AS reference_count, - GROUP_CONCAT(DISTINCT symbol_name) AS symbols + GROUP_CONCAT(DISTINCT symbol_name) AS symbols, + MAX(CASE WHEN logical_reference_kind IN ('attribute','annotation') THEN 1 ELSE 0 END) AS has_metadata_ref FROM ({innerSql}) edges GROUP BY source_file_id, source_path, target_path ORDER BY reference_count DESC, source_path, target_path"; @@ -1718,12 +1741,13 @@ FROM symbol_references r cmd.Parameters.AddWithValue($"@impactFallbackName{i}", fallbackNames[i]); AddPathFilterParameters(cmd, pathPatterns, excludePathPatterns); - var candidates = new List<(long SourceFileId, FileDependencyResult Edge)>(); + var candidates = new List<(long SourceFileId, bool HasMetadataRef, FileDependencyResult Edge)>(); using var reader = cmd.ExecuteTrackedReader(); while (reader.TrackedRead()) { candidates.Add(( reader.GetInt64(0), + reader.GetInt32(5) == 1, new FileDependencyResult { SourcePath = reader.GetString(1), @@ -1737,6 +1761,19 @@ FROM symbol_references r var filtered = new List(); foreach (var candidate in candidates) { + // Metadata-only consumers (attribute / annotation sites like `[MyAudit]` or + // `@Inject(User.class)`) legitimately lack structured type evidence in the + // source file. Bypass the evidence guard for those edges so deps/impact can + // still surface pure-attribute consumers. + // metadata 専用の参照 (`[MyAudit]` や `@Inject(User.class)` のような attribute / + // annotation 利用) は、source 側のファイルに structured な型利用が無くても + // 正当な依存となる。そのため、metadata 参照を含むエッジでは evidence guard を + // スキップし、純粋な attribute 消費側ファイルも deps/impact に出せるようにする。 + if (candidate.HasMetadataRef) + { + filtered.Add(candidate.Edge); + continue; + } if (!evidenceCache.TryGetValue(candidate.SourceFileId, out var hasEvidence)) { hasEvidence = SourceFileHasStructuredTypeEvidence(candidate.SourceFileId, definition.Name); @@ -2320,7 +2357,7 @@ public List GetFileDependencies(int limit = 50, string? la var sourceFilterAlias = "src"; var targetFilterAlias = "dst"; var sql = @" - WITH logical_references AS ( + WITH logical_references_primary AS ( SELECT src.id AS source_file_id, src.path AS source_path, src.lang AS source_lang, @@ -2366,6 +2403,24 @@ FROM symbol_references r sql += @" GROUP BY src.id, src.path, src.lang, r.symbol_name, r.line, r.column_number, logical_reference_kind ), + logical_references AS ( + SELECT source_file_id, source_path, source_lang, symbol_name, line, column_number, logical_reference_kind + FROM logical_references_primary + UNION ALL + -- C# attribute suffix alias: [Foo] in source is stored with symbol_name='Foo', + -- but the defining class is named 'FooAttribute'. Emit the canonical 'Foo' + 'Attribute' + -- form so deps can match the class file as a target. + -- C# 属性のサフィックス別名: ソース上の [Foo] は symbol_name='Foo' で保存されるが、 + -- 定義クラスは 'FooAttribute' 命名になるため、正規形 'Foo' + 'Attribute' を補って + -- deps がクラス側のファイルを target として join できるようにする。 + SELECT source_file_id, source_path, source_lang, + symbol_name || 'Attribute' AS symbol_name, + line, column_number, logical_reference_kind + FROM logical_references_primary + WHERE source_lang = 'csharp' + AND logical_reference_kind = 'attribute' + AND symbol_name NOT LIKE '%Attribute' + ), source_name_counts AS ( SELECT source_file_id, source_path, diff --git a/tests/CodeIndex.Tests/DbReaderTests.cs b/tests/CodeIndex.Tests/DbReaderTests.cs index b412b65ccf..1f4e0e45f9 100644 --- a/tests/CodeIndex.Tests/DbReaderTests.cs +++ b/tests/CodeIndex.Tests/DbReaderTests.cs @@ -2311,6 +2311,43 @@ public void Do() Assert.Contains(dependencies, d => d.SourcePath == "src/Serializer.cs" && d.TargetPath == "src/JsonConverterAttribute.cs"); } + [Fact] + public void GetFileDependencies_MatchesCSharpAttributeSuffixConvention() + { + // issue #293 follow-up: C# convention — a class `FooAttribute` is used in + // source as `[Foo]`, so the reference site is stored with symbol_name `Foo`. + // `deps` must canonicalize these so the attribute class file is still + // recognized as a dependency target for pure-attribute consumers. + // issue #293 補足: C# の規約では、クラス `FooAttribute` はソース中で `[Foo]` + // として使われるため、参照サイトは symbol_name `Foo` として保存される。 + // `deps` はこれを正規化し、attribute 専用の consumer でも attribute クラスの + // ファイルを依存 target として認識できるようにする。 + InsertIndexedFile("src/MyAuditAttribute.cs", "csharp", + """ + using System; + + [AttributeUsage(AttributeTargets.Class)] + public sealed class MyAuditAttribute : Attribute + { + } + """); + // Idiomatic `[MyAudit]` usage — symbol_name recorded as `MyAudit` but target + // class is `MyAuditAttribute`. + // 慣用的な `[MyAudit]` 利用 — symbol_name は `MyAudit` として記録されるが + // target クラスは `MyAuditAttribute`。 + InsertIndexedFile("src/Svc.cs", "csharp", + """ + [MyAudit] + public class Svc + { + } + """); + + var dependencies = _reader.GetFileDependencies(limit: 10, lang: "csharp"); + + Assert.Contains(dependencies, d => d.SourcePath == "src/Svc.cs" && d.TargetPath == "src/MyAuditAttribute.cs"); + } + [Fact] public void GetGroupedSymbolHotspots_CollapsesDuplicateNamesWithoutBareJoinInflation() { From 2f37dbacae6df8c2d893cbf7a94d2c3d7ba0abdf Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 18 Apr 2026 12:30:25 +0900 Subject: [PATCH 17/30] Fix #293 propagate C# attribute suffix canonicalization to references/inspect/analyze_symbol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adversarial review round 6 flagged inconsistency: `deps` / `impact` canonicalize the C# attribute-suffix convention (e.g. `[MyAudit]` → `MyAuditAttribute`) but `references` / `inspect` / `analyze_symbol` did not. `references MyAuditAttribute` missed the `[MyAudit]` call site that `deps MyAuditAttribute` now surfaces, which is a correctness / consistency bug for idiomatic C# attribute usage. Added `ComputeCSharpAttributeSuffixAlias` helper in `DbReader` and OR'd a C#-only `@queryAttributeAlias` parameter into the WHERE clauses of `SearchReferences`, `CountSearchReferences`, and `CountSearchReferencesTotal`. Applied to both exact (folded and NOCASE) and substring (LIKE) paths; substring mode uses exact-OR for the alias to avoid over-matching unrelated names like `FooAuditLog`. The alias is C# only, so Java / Kotlin / Scala / TypeScript annotation sites are unaffected. `callers` / `callees` intentionally skip the alias because they filter to the shared call-graph kinds and reject `--kind attribute|annotation` at the CLI / MCP boundary. Added regression tests covering substring mode, exact mode, and the Java scope-guard (alias must not bleed across languages). Co-Authored-By: Claude Opus 4 --- CHANGELOG.md | 4 +- src/CodeIndex/Database/DbReader.cs | 94 +++++++++++++++++++++++--- tests/CodeIndex.Tests/DbReaderTests.cs | 82 ++++++++++++++++++++++ 3 files changed, 169 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 258b0367cc..215ad432cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), #### Fixed - **MCP `callers` / `callees` `kind` schema, CLI help, and README `--kind` option row no longer advertise metadata kinds as valid filter values (#293 follow-up)** — The `callers` / `callees` handlers reject `kind: "attribute"` / `kind: "annotation"` with a usage error (CLI) / `isError` tool response (MCP), but the MCP tool schema's `kind` property description still listed all five kinds as if they were valid filter values (`"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"`), `cdidx --help` similarly lumped `callers` / `callees` together with `references` for the `--kind` help row, and the README `--kind` option table (EN line 436 / JP line 1307) still directed readers to pass `--kind attribute` or `--kind annotation` on `callers` / `callees`. Schema and handler now agree: the `callers` / `callees` `kind` description now reads `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."`, the CLI `--kind` help row splits `references` (accepts all five kinds) from `callers` / `callees` (call-graph kinds only, metadata rejected, redirects to `references`), and the README `--kind` row (both English and Japanese) now explicitly states that `callers` / `callees` reject `--kind attribute|annotation` with a usage error and points to `references --kind attribute|annotation` as the correct metadata-enumeration path. The MCP `references` tool description is unchanged because it still accepts every indexed kind. Added `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` to pin the corrected schema wording. Affected: `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `src/CodeIndex/Cli/ConsoleUi.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `README.md`. -- **`deps` and `impact` retain metadata references as compile-time dependency edges and canonicalize the C# attribute-suffix convention (#293 follow-up)** — `DbReader.GetFileDependencies` (backing `cdidx deps`) and `DbReader.GetFileDependencyHintsToResolvedType` (backing `cdidx impact` / MCP `impact_analysis` heuristic file-hint fallback) keep metadata references in their logical reference CTE. Renaming or removing an attribute class still breaks annotated sites such as `[JsonConverterAttribute]` at compile time, so file-level blast-radius analysis must surface those edges as real dependencies — now labeled `attribute` / `annotation` in `logical_reference_kind` (previously these same sites leaked in as phantom `call` rows before classification was corrected). `GetFileDependencies` additionally canonicalizes the C# attribute naming convention: idiomatic `[Foo]` usage is stored with `symbol_name = "Foo"` but the defining class is `FooAttribute`, so the logical-reference CTE now UNION-ALLs a suffix-aliased copy of every C# `attribute`-kind row with `'Attribute'` appended (only when the original symbol_name does not already end in `Attribute`) so the target-file join reaches the attribute class file. `GetFileDependencyHintsToResolvedType` also aggregates a per-candidate `has_metadata_ref` flag and bypasses the `SourceFileHasStructuredTypeEvidence` guard for edges whose contributing references are purely metadata, so pure-attribute consumer files such as `[MyAudit] class Svc` now surface through `impact MyAuditAttribute` instead of being filtered out. `ResolveImpactFallbackNames` symmetrically adds suffix-stripped aliases for C# class names ending in `Attribute`, so the BFS-less file-hint path can find `[Foo]` sites whose source symbol_name does not match the defining class name. `callers` / `callees` still reject `--kind attribute|annotation` at the CLI / MCP boundary because those commands model the dynamic call graph, not the dependency graph. Added regression coverage `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies` (runtime + metadata edges both surface) and `DbReaderTests.GetFileDependencies_MatchesCSharpAttributeSuffixConvention` (`[MyAudit]` → `MyAuditAttribute` canonicalization). Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. +- **`deps`, `impact`, `references`, `inspect`, and `analyze_symbol` retain metadata references as compile-time dependency edges and canonicalize the C# attribute-suffix convention (#293 follow-up)** — `DbReader.GetFileDependencies` (backing `cdidx deps`) and `DbReader.GetFileDependencyHintsToResolvedType` (backing `cdidx impact` / MCP `impact_analysis` heuristic file-hint fallback) keep metadata references in their logical reference CTE. Renaming or removing an attribute class still breaks annotated sites such as `[JsonConverterAttribute]` at compile time, so file-level blast-radius analysis must surface those edges as real dependencies — now labeled `attribute` / `annotation` in `logical_reference_kind` (previously these same sites leaked in as phantom `call` rows before classification was corrected). `GetFileDependencies` additionally canonicalizes the C# attribute naming convention: idiomatic `[Foo]` usage is stored with `symbol_name = "Foo"` but the defining class is `FooAttribute`, so the logical-reference CTE now UNION-ALLs a suffix-aliased copy of every C# `attribute`-kind row with `'Attribute'` appended (only when the original symbol_name does not already end in `Attribute`) so the target-file join reaches the attribute class file. `GetFileDependencyHintsToResolvedType` also aggregates a per-candidate `has_metadata_ref` flag and bypasses the `SourceFileHasStructuredTypeEvidence` guard for edges whose contributing references are purely metadata, so pure-attribute consumer files such as `[MyAudit] class Svc` now surface through `impact MyAuditAttribute` instead of being filtered out. `ResolveImpactFallbackNames` symmetrically adds suffix-stripped aliases for C# class names ending in `Attribute`, so the BFS-less file-hint path can find `[Foo]` sites whose source symbol_name does not match the defining class name. The same canonicalization is propagated to the symbol-level reference reader: `DbReader.SearchReferences`, `CountSearchReferences`, and `CountSearchReferencesTotal` (backing `references`, `inspect`, MCP `analyze_symbol`) now compute a C#-only `queryAttributeAlias` (defined in the new `ComputeCSharpAttributeSuffixAlias` helper) and OR it into both exact (folded and NOCASE) and substring WHERE clauses, so `references MyAuditAttribute` (and `inspect MyAuditAttribute` / `analyze_symbol MyAuditAttribute`) surface the idiomatic `[MyAudit]` call site consistently with `deps` / `impact`. Substring mode uses an exact-OR for the alias (not a second LIKE) so user queries like `MyAuditAttribute` do not over-match unrelated names such as `FooAuditLog`. The alias is C# only, so Java / Kotlin / Scala / TypeScript annotation sites remain unaffected. `callers` / `callees` intentionally skip the alias because they filter to the shared call-graph kinds (`call`, `instantiate`, `subscribe`) and reject `--kind attribute|annotation` at the CLI / MCP boundary — those commands model the dynamic call graph, not the dependency graph. Added regression coverage `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies` (runtime + metadata edges both surface), `DbReaderTests.GetFileDependencies_MatchesCSharpAttributeSuffixConvention` (`[MyAudit]` → `MyAuditAttribute` canonicalization through `deps`), and `DbReaderTests.SearchReferences_MatchesCSharpAttributeSuffixConvention_Substring` / `_Exact` / `_CSharpAttributeSuffixAliasDoesNotBleedToOtherLanguages` (same canonicalization through `references` / `inspect` / `analyze_symbol`, plus C#-only scope guard). Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. - **`callers` / `callees` now reject `--kind attribute|annotation` at the CLI and MCP boundary, and Swift / Gradle `@` metadata is reclassified as `annotation` (#293 follow-up)** — Metadata rows (`attribute` / `annotation`) are attributed to their enclosing body-range symbol rather than the annotated target, so `callers Obsolete --kind attribute` would silently return `[Obsolete] void M()` under the enclosing class (not `M`), and file-level targets such as `[assembly: CLSCompliant]` or Kotlin `@file:JvmName` would drop entirely because `container_name` is `NULL`. `QueryCommandRunner.RunCallers` / `RunCallees` now reject `--kind attribute` and `--kind annotation` with `CommandExitCodes.UsageError`, and the MCP `callers` / `callees` tools return an `isError` tool response; both paths direct users to `references --kind attribute|annotation`, which IS a correct metadata enumeration path. Separately, `AnnotationLanguages` now includes `swift` and `gradle`, so Swift `@available(iOS 13.0, *)` / `@objc` / `@MainActor` and Gradle/Groovy `@CompileStatic` / `@TaskAction` are recorded as `annotation` instead of `call` (previously the `@Name(args)` forms leaked into `callers` / `callees` / `hotspots` / `impact` as phantom call edges, and the no-arg `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` forms dropped from the index entirely because no-arg annotation emission was gated to languages in this set). Added `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError` (theory, 4 cases), `McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError` (theory, 4 cases), `ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`, and `ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation`. Affected: `src/CodeIndex/Cli/QueryCommandRunner.cs`, `src/CodeIndex/Mcp/McpToolHandlers.cs`, `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. - **Multi-line no-arg C# attribute sections like `[\n Serializable\n]` are now indexed without over-matching attribute argument identifiers (#293 follow-up)** — The no-arg C# attribute regex previously required the match to start at a `[` / `,` boundary and end at `]` / `,` on the same line, so a bare identifier on the interior line of a multi-line attribute section (e.g. ` Serializable` between a line-leading `[` and a line-leading `]`) was silently dropped from `references --kind attribute`. The left anchor is now a word-boundary lookbehind and the right anchor also accepts end-of-line. To avoid misclassifying enum / qualified-constant identifiers that happen to sit at end-of-line inside an attribute argument list (e.g. `ConverterStrategy.AllowNumbers` in `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]`) as no-arg attributes, `BuildCSharpAttributeRanges` now also emits a parallel "top-level zone" table that splits each attribute section by `(` / `)` so only paren-depth-0 positions inside the section qualify as no-arg attribute name positions. Paren depth is tracked section-locally (each `[` snapshots the global depth at open time) so parameter attributes like `void M([FromServices] IService s)`, delegate parameter attributes like `delegate void D([Attr] int x)`, and lambda parameter attributes like `Func f = ([Attr] int x) => x` — whose `[` opens while the global paren depth is already > 0 — still have their attribute-list top level correctly identified. The no-arg attribute path uses that top-level table instead of the full attribute-section ranges, while the existing with-args metadata classification still uses the full ranges. `SymbolExtractor` also now tracks multi-line `[...]` depth across lines via `StripMultiLineCSharpAttributeInterior` in `BuildCSharpMatchLines`, so interior lines of a multi-line attribute section are blanked out before declaration regexes run — this prevents ` Serializable` from being mis-extracted as a top-level `function` symbol (which then masked the reference through the `definitionNames` guard). The multi-line blanker now activates on any `[` that opens without a matching `]` on the same line, not just `[` that sits at the start of the line, so multi-line parameter attributes like `void M([\n FromServices\n] IService s)`, type-parameter attributes like `class Bar<[\n TypeParamAttr\n] T>`, and delegate/lambda parameter attributes like `delegate void D([\n DelegateParamAttr\n] int x)` are also blanked out correctly — previously only the leading-`[` case was covered, so the interior identifiers of those forms were mis-extracted as `function` symbols and their attribute references were silently dropped from `references --kind attribute`. Regression coverage adds `[\n Serializable\n]`, `[\n global::System.Obsolete\n]`, `[Required,\n Key]`, `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]` (pinning that `AllowNumbers` / `ConverterStrategy` are NOT classified as `attribute`), and the non-leading-`[` parameter / type-parameter / delegate-parameter multi-line cases above, running through `SymbolExtractor.Extract` + `ReferenceExtractor.Extract` end-to-end so both sides of the regression stay pinned. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Indexer/SymbolExtractor.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. - **`::`-qualified C# no-arg attributes and JavaScript `@Decorator` usages are now classified correctly (#293 follow-up)** — The no-arg C# attribute regex now accepts `::` alongside `.` as a qualifier separator, so `[global::System.Obsolete]` and `[Alias::MyAttr]` reach `references --kind attribute` instead of being silently dropped. `AnnotationLanguages` additionally includes `javascript`, so JS decorators such as `@sealed` (no-arg) and `@injectable()` (with args) are reclassified to `annotation` rather than leaking into the call-graph as phantom `call` rows — matching the existing TypeScript contract since JavaScript is already a graph-supported language. Added regression tests pinning `[global::System.Obsolete]`, `[Alias::MyAttr]`, and JavaScript `@sealed` / `@injectable()`. Also rewrote the README `callers` row (EN + JP) and the MCP tool descriptions for `callers` / `callees` to pull back the earlier over-promise that `kind: "attribute"` / `kind: "annotation"` on `callers` / `callees` is a supported metadata-inspection path: a metadata row is attributed to its enclosing body-range symbol (for a class-level declaration, that is the class itself) and drops entirely for file-level targets such as `[assembly: ...]` where `container_name` is `null`, so the docs now direct users to `references --kind attribute|annotation` for metadata enumeration. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`, `README.md`. @@ -686,7 +686,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), #### 修正 - **MCP の `callers` / `callees` の `kind` schema・CLI help・README の `--kind` オプション行が metadata kinds を有効フィルタ値として広告しないよう修正 (#293 追加対応)** — `callers` / `callees` の handler は `kind: "attribute"` / `kind: "annotation"` を usage error(CLI)/ `isError` ツールレスポンス(MCP)で拒否するにもかかわらず、MCP ツール schema の `kind` プロパティ説明は 5 種すべてを列挙した `"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"` のままで、`cdidx --help` の `--kind` 行も `callers` / `callees` を `references` と同じ行にまとめていた。さらに README の `--kind` オプション表(英語 436 行目 / 日本語 1307 行目)も、`callers` / `callees` で `--kind attribute` / `--kind annotation` を渡せるかのように案内していた。schema と handler の契約を一致させ、`callers` / `callees` の `kind` description を `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."` に修正し、CLI の `--kind` help 行も `references`(5 種すべて受理)と `callers` / `callees`(call-graph 種別のみ、metadata kinds は拒否して `references` へ誘導)を分けるようにし、README の `--kind` 行(英語・日本語の両方)も `callers` / `callees` が `--kind attribute|annotation` を usage error で拒否すること、metadata 列挙の正規経路が `references --kind attribute|annotation` であることを明示する文言に直した。MCP の `references` ツール説明は全 reference kind を受け付ける契約のままなので変更なし。修正後の schema 文言を固定する `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` を追加。対象: `src/CodeIndex/Mcp/McpToolDefinitions.cs`、`src/CodeIndex/Cli/ConsoleUi.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`、`README.md`。 -- **`deps` と `impact` が metadata 参照を compile-time 依存として保持し、C# の attribute サフィックス命名規約を正規化 (#293 追加対応)** — `cdidx deps` の実装である `DbReader.GetFileDependencies` と、`cdidx impact` / MCP `impact_analysis` の heuristic file-hint フォールバックである `DbReader.GetFileDependencyHintsToResolvedType` は、logical reference CTE に metadata 参照も含める。attribute クラスを rename / 削除すれば `[JsonConverterAttribute]` のような注釈サイトも compile-time で壊れるため、ファイル単位の blast-radius 分析はそれらも本物の依存 edge として表示する必要があり、`logical_reference_kind` では `attribute` / `annotation` として正しくラベル付けされる(分類修正前は同じサイトが phantom `call` として漏れていた)。さらに `GetFileDependencies` は C# の attribute 命名規約を正規化する: 慣用的な `[Foo]` は `symbol_name = "Foo"` で保存されるが、定義クラスは `FooAttribute` 命名となるため、logical-reference CTE に C# `attribute` 種別の行をサフィックス `'Attribute'` を付与した別名コピーとして UNION ALL で追加する(元の symbol_name が既に `Attribute` で終わっている場合は重複しないようスキップ)。これにより target ファイル join が attribute クラス側のファイルへ到達する。`GetFileDependencyHintsToResolvedType` はさらに candidate ごとに `has_metadata_ref` フラグを集約し、寄与する参照が metadata だけである edge については `SourceFileHasStructuredTypeEvidence` guard をバイパスするため、`[MyAudit] class Svc` のような純 attribute consumer ファイルも `impact MyAuditAttribute` で出るようになった(以前は filter で落ちていた)。`ResolveImpactFallbackNames` も対称的に、`Attribute` で終わる C# クラス名にはサフィックスを外した別名を追加するため、BFS を使わない file-hint 経路でも source symbol_name が定義クラス名と一致しない `[Foo]` サイトを発見できる。`callers` / `callees` は dynamic call graph を扱うコマンドなので、`--kind attribute|annotation` は引き続き CLI / MCP boundary で拒否する(dependency graph とは別契約)。回帰テスト `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies`(runtime と metadata の両方の edge が出ることを固定)と `DbReaderTests.GetFileDependencies_MatchesCSharpAttributeSuffixConvention`(`[MyAudit]` → `MyAuditAttribute` の正規化を固定)を追加。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 +- **`deps`・`impact`・`references`・`inspect`・`analyze_symbol` が metadata 参照を compile-time 依存として保持し、C# の attribute サフィックス命名規約を正規化 (#293 追加対応)** — `cdidx deps` の実装である `DbReader.GetFileDependencies` と、`cdidx impact` / MCP `impact_analysis` の heuristic file-hint フォールバックである `DbReader.GetFileDependencyHintsToResolvedType` は、logical reference CTE に metadata 参照も含める。attribute クラスを rename / 削除すれば `[JsonConverterAttribute]` のような注釈サイトも compile-time で壊れるため、ファイル単位の blast-radius 分析はそれらも本物の依存 edge として表示する必要があり、`logical_reference_kind` では `attribute` / `annotation` として正しくラベル付けされる(分類修正前は同じサイトが phantom `call` として漏れていた)。さらに `GetFileDependencies` は C# の attribute 命名規約を正規化する: 慣用的な `[Foo]` は `symbol_name = "Foo"` で保存されるが、定義クラスは `FooAttribute` 命名となるため、logical-reference CTE に C# `attribute` 種別の行をサフィックス `'Attribute'` を付与した別名コピーとして UNION ALL で追加する(元の symbol_name が既に `Attribute` で終わっている場合は重複しないようスキップ)。これにより target ファイル join が attribute クラス側のファイルへ到達する。`GetFileDependencyHintsToResolvedType` はさらに candidate ごとに `has_metadata_ref` フラグを集約し、寄与する参照が metadata だけである edge については `SourceFileHasStructuredTypeEvidence` guard をバイパスするため、`[MyAudit] class Svc` のような純 attribute consumer ファイルも `impact MyAuditAttribute` で出るようになった(以前は filter で落ちていた)。`ResolveImpactFallbackNames` も対称的に、`Attribute` で終わる C# クラス名にはサフィックスを外した別名を追加するため、BFS を使わない file-hint 経路でも source symbol_name が定義クラス名と一致しない `[Foo]` サイトを発見できる。同じ正規化をシンボル単位の参照 reader にも伝播する: `references`・`inspect`・MCP `analyze_symbol` を支える `DbReader.SearchReferences`・`CountSearchReferences`・`CountSearchReferencesTotal` は、新しいヘルパー `ComputeCSharpAttributeSuffixAlias` で C# 限定の `queryAttributeAlias` を計算し、exact(folded と NOCASE)・substring の両方の WHERE 句に OR で追加する。これにより `references MyAuditAttribute`(および `inspect MyAuditAttribute` / `analyze_symbol MyAuditAttribute`)でも慣用的な `[MyAudit]` 参照サイトが `deps` / `impact` と一貫して surface する。substring モードでは alias を二つ目の LIKE にせず exact-OR で束ねているため、ユーザーが `MyAuditAttribute` と打った時に `FooAuditLog` のような無関係な名前に over-match しない。alias は C# 限定なので、Java / Kotlin / Scala / TypeScript の annotation サイトには影響しない。`callers` / `callees` は共有の call-graph 種別 (`call` / `instantiate` / `subscribe`) に絞って `--kind attribute|annotation` を CLI / MCP 境界で拒否する契約なので、alias は意図的に適用しない(dynamic call graph と dependency graph は別契約)。回帰テスト `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies`(runtime と metadata の両方の edge が出ることを固定)、`DbReaderTests.GetFileDependencies_MatchesCSharpAttributeSuffixConvention`(`deps` 経由での `[MyAudit]` → `MyAuditAttribute` 正規化)、`DbReaderTests.SearchReferences_MatchesCSharpAttributeSuffixConvention_Substring` / `_Exact` / `_CSharpAttributeSuffixAliasDoesNotBleedToOtherLanguages`(`references` / `inspect` / `analyze_symbol` での同一正規化と C# 限定スコープ)を追加。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 - **`callers` / `callees` が CLI と MCP 境界で `--kind attribute|annotation` を拒否し、Swift / Gradle の `@` metadata を `annotation` に再分類 (#293 追加対応)** — metadata 行 (`attribute` / `annotation`) は注釈対象そのものではなく body-range の外側シンボルに帰属するため、`callers Obsolete --kind attribute` は `[Obsolete] void M()` を `M` ではなく外側クラスの下に黙って返し、`[assembly: CLSCompliant]` や Kotlin の `@file:JvmName` のような file-level target は `container_name = NULL` で完全脱落していた。`QueryCommandRunner.RunCallers` / `RunCallees` は `--kind attribute` / `--kind annotation` を `CommandExitCodes.UsageError` で弾き、MCP の `callers` / `callees` ツールは `isError` ツールレスポンスを返すようにした。どちらの経路も、metadata 列挙の正しいパスである `references --kind attribute|annotation` へ誘導する。あわせて `AnnotationLanguages` に `swift` と `gradle` を追加し、Swift の `@available(iOS 13.0, *)` / `@objc` / `@MainActor` や Gradle / Groovy の `@CompileStatic` / `@TaskAction` を `call` ではなく `annotation` として記録するようにした(修正前は引数付きの `@Name(args)` 形は `callers` / `callees` / `hotspots` / `impact` に phantom call edge として漏れ、引数なしの `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` 形はこの集合で gate されていた no-arg annotation 出力から外れてインデックス自体に残らなかった)。回帰テストとして `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError`(theory 4 ケース)、`McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError`(theory 4 ケース)、`ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`、`ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation` を追加。対象: `src/CodeIndex/Cli/QueryCommandRunner.cs`、`src/CodeIndex/Mcp/McpToolHandlers.cs`、`src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 - **`[\n Serializable\n]` のような複数行にまたがる引数なし C# 属性セクションを、属性引数内の識別子を誤って取り込まずに索引化 (#293 追加対応)** — 引数なし C# 属性の正規表現は、これまで `[` / `,` を左端、`]` / `,` を右端として同一行内でアンカーしていたため、行頭 `[` と行頭 `]` の間に書かれた ` Serializable` のような裸識別子が `references --kind attribute` から黙って脱落していた。左側は単語境界 lookbehind、右側は行末(`$`)も許容するように緩和した。一方で `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]` のように属性の引数リスト内で行末に現れる enum / 修飾定数識別子(`AllowNumbers` など)が no-arg 属性として誤分類される回帰を避けるため、`BuildCSharpAttributeRanges` は `(` / `)` で属性セクションを分割した paren 深さ 0 の「top-level ゾーン」テーブルも並列で出力するようにし、no-arg 属性の採用位置はそのテーブルに限定した(既存の引数付き metadata 分類パスは引き続き属性セクション全体のレンジを使う)。paren 深さは section-local に追跡する(各 `[` がその瞬間のグローバル paren 深さを snapshot として保持)ため、`void M([FromServices] IService s)` のようなパラメータ属性、`delegate void D([Attr] int x)` のようなデリゲートパラメータ属性、`Func f = ([Attr] int x) => x` のようなラムダパラメータ属性のように、`[` が開いた時点で既にグローバル paren 深さが 0 より大きいケースでも、属性リストの top-level を正しく認識できる。`SymbolExtractor` 側でも `BuildCSharpMatchLines` に `StripMultiLineCSharpAttributeInterior` を追加し、複数行にまたがる `[...]` セクションの深さを跨行で追跡して内部行を宣言 regex 適用前に空白化するようにした。これにより ` Serializable` がトップレベル `function` シンボルとして誤抽出され、`definitionNames` ガード経由で参照分類が詰まってしまうのを防ぐ。さらに、複数行の空白化起動条件を「同一行内で閉じない `[` があるかどうか」に広げ、行頭の `[` だけでなく `void M([\n FromServices\n] IService s)` のようなパラメータ属性、`class Bar<[\n TypeParamAttr\n] T>` のような型パラメータ属性、`delegate void D([\n DelegateParamAttr\n] int x)` のようなデリゲート / ラムダのパラメータ属性でも内部行が正しく空白化されるようにした — 以前は開口行で `[` が行頭にある場合しかカバーされていなかったため、上記の形の内部識別子が `function` シンボルとして誤抽出され、`references --kind attribute` から属性参照が黙って脱落していた。`[\n Serializable\n]`・`[\n global::System.Obsolete\n]`・`[Required,\n Key]`・`[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]`(`AllowNumbers` / `ConverterStrategy` が `attribute` として取り込まれないことを固定)に加え、上記の行頭でない `[` で開く複数行パラメータ / 型パラメータ / デリゲートパラメータ属性のケースも `SymbolExtractor.Extract` + `ReferenceExtractor.Extract` で end-to-end に回して SymbolExtractor 側の回帰も固定する回帰テストを追加。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Indexer/SymbolExtractor.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 - **`::` 修飾の C# 引数なし属性と JavaScript の `@Decorator` を正しく分類 (#293 追加対応)** — 引数なし C# 属性の正規表現が修飾子セパレータとして `.` に加えて `::` も受け付けるようになり、`[global::System.Obsolete]` や `[Alias::MyAttr]` のような形が `references --kind attribute` から黙って脱落しなくなった。`AnnotationLanguages` にも `javascript` を追加し、JavaScript はもともと graph 対応言語なので、`@sealed`(引数なし)や `@injectable()`(引数付き)のような JS decorator も `annotation` として再分類され、TypeScript と同じ契約で call-graph に phantom `call` 行を残さない。`[global::System.Obsolete]`・`[Alias::MyAttr]`・JavaScript の `@sealed` / `@injectable()` を押さえる回帰テストを追加。あわせて、以前に進めていた `callers` / `callees` の metadata 契約を README(英日)と MCP ツール説明の両方で引き戻し、metadata 行は注釈対象そのものではなく body-range 上の外側シンボル(クラス直下宣言ならクラス、`[assembly: ...]` のようなファイルレベル target では `container_name` が `null`)に帰属するため `kind: "attribute"` / `kind: "annotation"` を `callers` / `callees` に渡すのは信頼できる metadata 調査経路ではなく、metadata 列挙は `references --kind attribute|annotation` を使うよう案内する文言に修正した。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Mcp/McpToolDefinitions.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`、`README.md`。 diff --git a/src/CodeIndex/Database/DbReader.cs b/src/CodeIndex/Database/DbReader.cs index 8a53492171..1fa10309ad 100644 --- a/src/CodeIndex/Database/DbReader.cs +++ b/src/CodeIndex/Database/DbReader.cs @@ -677,18 +677,33 @@ FROM symbol_references r if (referenceKind != null) sql += $" AND {BuildGraphSupportedLanguagePredicate(cmd, "f", "graphLang")}"; + var referencesSuffixAlias = ComputeCSharpAttributeSuffixAlias(query, lang); if (query != null) { // --exact: Unicode-aware equality when FoldReady (#86), else ASCII COLLATE NOCASE. // Fold path: r.symbol_name_folded = @qFolded (indexed), query pre-folded in .NET. // Fallback: r.symbol_name = @q COLLATE NOCASE (indexed by idx_symbol_refs_name_nocase). + // When the query ends with C# attribute suffix `Attribute`, also OR against the + // suffix-stripped alias so `references FooAttribute --exact` reaches the idiomatic + // `[Foo]` reference site stored with `symbol_name = "Foo"`. In substring mode we + // still LIKE-match `%FooAttribute%` and add only the exact stripped alias to avoid + // overmatching unrelated names (e.g. `FooAuditLog`) that share the stripped prefix. // --exact: FoldReady なら Unicode 折り畳み経路、未 ready なら ASCII NOCASE へ fallback。 + // C# の `Attribute` suffix が付いたクエリは、suffix を外した別名とも照合する。 + // 部分一致モードでは `%FooAttribute%` をそのまま使い、別名側は exact 照合だけを OR + // することで `FooAuditLog` など無関係な名前を巻き込まないようにする。 if (exact && _foldReady) - sql += " AND r.symbol_name_folded = @query"; + sql += referencesSuffixAlias != null + ? " AND (r.symbol_name_folded = @query OR r.symbol_name_folded = @queryAttributeAlias)" + : " AND r.symbol_name_folded = @query"; else if (exact) - sql += " AND r.symbol_name = @query COLLATE NOCASE"; + sql += referencesSuffixAlias != null + ? " AND (r.symbol_name = @query COLLATE NOCASE OR r.symbol_name = @queryAttributeAlias COLLATE NOCASE)" + : " AND r.symbol_name = @query COLLATE NOCASE"; else - sql += " AND r.symbol_name LIKE @query ESCAPE '\\'"; + sql += referencesSuffixAlias != null + ? " AND (r.symbol_name LIKE @query ESCAPE '\\' OR r.symbol_name = @queryAttributeAlias COLLATE NOCASE)" + : " AND r.symbol_name LIKE @query ESCAPE '\\'"; } if (referenceKind != null) sql += " AND r.reference_kind = @referenceKind"; @@ -717,6 +732,20 @@ FROM symbol_references r else queryParam = query; cmd.Parameters.AddWithValue("@query", queryParam); + if (referencesSuffixAlias != null) + { + // Exact-match alias value is used both in --exact paths (folded / NOCASE) + // and in the substring path (COLLATE NOCASE exact OR to bypass LIKE noise). + // In the folded --exact branch the alias is pre-folded; the substring branch + // uses the raw stripped form because the OR clause is a literal `=` comparison. + // exact 用の別名値は --exact 経路(folded / NOCASE)と部分一致経路(LIKE ノイズを + // 避けるための COLLATE NOCASE の等値 OR)の両方で使う。folded 経路だけは事前に + // 折りたたみ、部分一致経路は生の stripped 形をそのまま使う。 + var aliasParam = exact && _foldReady + ? NameFold.Fold(referencesSuffixAlias) ?? referencesSuffixAlias + : referencesSuffixAlias; + cmd.Parameters.AddWithValue("@queryAttributeAlias", aliasParam); + } cmd.Parameters.AddWithValue("@rankingQuery", query.Trim()); cmd.Parameters.AddWithValue("@rankingQueryPrefix", $"{EscapeLikeQuery(query.Trim())}%"); } @@ -770,14 +799,21 @@ FROM symbol_references r WHERE 1=1"; innerSql += $" AND {BuildGraphSupportedLanguagePredicate(cmd, "f", "graphLang")}"; + var countSuffixAlias = ComputeCSharpAttributeSuffixAlias(query, lang); if (query != null) { if (exact && _foldReady) - innerSql += " AND r.symbol_name_folded = @query"; + innerSql += countSuffixAlias != null + ? " AND (r.symbol_name_folded = @query OR r.symbol_name_folded = @queryAttributeAlias)" + : " AND r.symbol_name_folded = @query"; else if (exact) - innerSql += " AND r.symbol_name = @query COLLATE NOCASE"; + innerSql += countSuffixAlias != null + ? " AND (r.symbol_name = @query COLLATE NOCASE OR r.symbol_name = @queryAttributeAlias COLLATE NOCASE)" + : " AND r.symbol_name = @query COLLATE NOCASE"; else - innerSql += " AND r.symbol_name LIKE @query ESCAPE '\\'"; + innerSql += countSuffixAlias != null + ? " AND (r.symbol_name LIKE @query ESCAPE '\\' OR r.symbol_name = @queryAttributeAlias COLLATE NOCASE)" + : " AND r.symbol_name LIKE @query ESCAPE '\\'"; } if (referenceKind != null) innerSql += " AND r.reference_kind = @referenceKind"; @@ -797,6 +833,13 @@ FROM symbol_references r ? NameFold.Fold(query) ?? query : query; cmd.Parameters.AddWithValue("@query", value); + if (countSuffixAlias != null) + { + var aliasParam = exact && _foldReady + ? NameFold.Fold(countSuffixAlias) ?? countSuffixAlias + : countSuffixAlias; + cmd.Parameters.AddWithValue("@queryAttributeAlias", aliasParam); + } } if (referenceKind != null) cmd.Parameters.AddWithValue("@referenceKind", referenceKind); @@ -825,14 +868,21 @@ FROM symbol_references r WHERE 1=1"; innerSql += $" AND {BuildGraphSupportedLanguagePredicate(cmd, "f", "graphLang")}"; + var totalSuffixAlias = ComputeCSharpAttributeSuffixAlias(query, lang); if (query != null) { if (exact && _foldReady) - innerSql += " AND r.symbol_name_folded = @query"; + innerSql += totalSuffixAlias != null + ? " AND (r.symbol_name_folded = @query OR r.symbol_name_folded = @queryAttributeAlias)" + : " AND r.symbol_name_folded = @query"; else if (exact) - innerSql += " AND r.symbol_name = @query COLLATE NOCASE"; + innerSql += totalSuffixAlias != null + ? " AND (r.symbol_name = @query COLLATE NOCASE OR r.symbol_name = @queryAttributeAlias COLLATE NOCASE)" + : " AND r.symbol_name = @query COLLATE NOCASE"; else - innerSql += " AND r.symbol_name LIKE @query ESCAPE '\\'"; + innerSql += totalSuffixAlias != null + ? " AND (r.symbol_name LIKE @query ESCAPE '\\' OR r.symbol_name = @queryAttributeAlias COLLATE NOCASE)" + : " AND r.symbol_name LIKE @query ESCAPE '\\'"; } if (referenceKind != null) innerSql += " AND r.reference_kind = @referenceKind"; @@ -852,6 +902,13 @@ FROM symbol_references r ? NameFold.Fold(query) ?? query : query; cmd.Parameters.AddWithValue("@query", value); + if (totalSuffixAlias != null) + { + var aliasParam = exact && _foldReady + ? NameFold.Fold(totalSuffixAlias) ?? totalSuffixAlias + : totalSuffixAlias; + cmd.Parameters.AddWithValue("@queryAttributeAlias", aliasParam); + } } if (referenceKind != null) cmd.Parameters.AddWithValue("@referenceKind", referenceKind); @@ -1623,6 +1680,25 @@ FROM symbols s return results; } + // C# convention: a class `FooAttribute` is used in source as `[Foo]`, so the reference + // site is stored with `symbol_name = "Foo"`. When a user queries with the class name + // (`references FooAttribute`, `inspect FooAttribute`, `analyze_symbol("FooAttribute")`), + // return the suffix-stripped form as an alias so the query still reaches the idiomatic + // use site. Only applies for C# scope — other languages do not share the convention. + // C# の規約: クラス `FooAttribute` はソース中で `[Foo]` として使われるため、参照サイトは + // `symbol_name = "Foo"` で保存される。ユーザーがクラス名で問い合わせたとき + // (`references FooAttribute` 等) でも慣用的な利用サイトに到達できるよう、 + // suffix を外した別名を返す。C# 以外の言語ではこの規約を持たないので適用しない。 + private static string? ComputeCSharpAttributeSuffixAlias(string? query, string? lang) + { + if (string.IsNullOrEmpty(query)) return null; + if (lang != null && !lang.Equals("csharp", StringComparison.OrdinalIgnoreCase)) return null; + const string suffix = "Attribute"; + if (!query!.EndsWith(suffix, StringComparison.Ordinal)) return null; + if (query.Length <= suffix.Length) return null; + return query.Substring(0, query.Length - suffix.Length); + } + private List ResolveImpactFallbackNames(SymbolResult definition) { if (string.IsNullOrWhiteSpace(definition.Path) || string.IsNullOrWhiteSpace(definition.Name)) diff --git a/tests/CodeIndex.Tests/DbReaderTests.cs b/tests/CodeIndex.Tests/DbReaderTests.cs index 1f4e0e45f9..630802d99b 100644 --- a/tests/CodeIndex.Tests/DbReaderTests.cs +++ b/tests/CodeIndex.Tests/DbReaderTests.cs @@ -2348,6 +2348,88 @@ public class Svc Assert.Contains(dependencies, d => d.SourcePath == "src/Svc.cs" && d.TargetPath == "src/MyAuditAttribute.cs"); } + [Fact] + public void SearchReferences_MatchesCSharpAttributeSuffixConvention_Substring() + { + // issue #293 follow-up: `references MyAuditAttribute` (substring mode) must + // find `[MyAudit]` call sites so `references` / `inspect` / `analyze_symbol` + // stay consistent with `deps` / `impact` canonicalization. + // issue #293 補足: `references MyAuditAttribute`(部分一致モード)が `[MyAudit]` + // 参照サイトを見つけられなければならず、`references` / `inspect` / + // `analyze_symbol` が `deps` / `impact` の正規化と整合する必要がある。 + InsertIndexedFile("src/MyAuditAttribute.cs", "csharp", + """ + using System; + + [AttributeUsage(AttributeTargets.Class)] + public sealed class MyAuditAttribute : Attribute + { + } + """); + InsertIndexedFile("src/Svc.cs", "csharp", + """ + [MyAudit] + public class Svc + { + } + """); + + var results = _reader.SearchReferences("MyAuditAttribute", lang: "csharp"); + + Assert.Contains(results, r => r.Path == "src/Svc.cs" && r.ReferenceKind == "attribute"); + } + + [Fact] + public void SearchReferences_MatchesCSharpAttributeSuffixConvention_Exact() + { + // Same scenario under `--exact` — the suffix alias must be applied even when + // exact-name matching is requested, otherwise `references MyAuditAttribute + // --exact` loses the attribute call site. + // `--exact` 指定下でも同様 — exact match の場合でも suffix alias を適用しない + // と、`references MyAuditAttribute --exact` は attribute 参照サイトを取りこぼす。 + InsertIndexedFile("src/MyAuditAttribute.cs", "csharp", + """ + using System; + + [AttributeUsage(AttributeTargets.Class)] + public sealed class MyAuditAttribute : Attribute + { + } + """); + InsertIndexedFile("src/Svc.cs", "csharp", + """ + [MyAudit] + public class Svc + { + } + """); + + var results = _reader.SearchReferences("MyAuditAttribute", lang: "csharp", exact: true); + + Assert.Contains(results, r => r.Path == "src/Svc.cs" && r.ReferenceKind == "attribute"); + } + + [Fact] + public void SearchReferences_CSharpAttributeSuffixAliasDoesNotBleedToOtherLanguages() + { + // Alias must be C# only — a Java `@MyAudit(...)` annotation using the + // suffix convention is not part of the Java ecosystem, so querying for + // `MyAuditAttribute` under Java scope must not spuriously match `MyAudit`. + // alias は C# 限定 — Java の `@MyAudit(...)` annotation は suffix 規約を使わない + // ので、Java スコープで `MyAuditAttribute` を指定したときに `MyAudit` に + // 誤って match してはならない。 + InsertIndexedFile("src/Svc.java", "java", + """ + @MyAudit + public class Svc { + } + """); + + var results = _reader.SearchReferences("MyAuditAttribute", lang: "java"); + + Assert.Empty(results); + } + [Fact] public void GetGroupedSymbolHotspots_CollapsesDuplicateNamesWithoutBareJoinInflation() { From 6607b381e25278ce83edf47d68c66bd74552bf95 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 18 Apr 2026 12:39:34 +0900 Subject: [PATCH 18/30] Fix #293 scope C# attribute suffix alias to attribute rows and make case-insensitive Adversarial review round 7 flagged two issues with the newly-introduced alias: 1. [high] The alias broadened queries to the wrong rows. An unscoped `references FooAttribute` or `references FooAttribute --kind call --lang csharp` could match plain `Foo()` call rows because the alias OR did not constrain itself to C# attribute rows. When `--lang` was omitted, a Java `@Foo(...)` annotation or any non-C# `Foo` reference could also leak in. 2. [medium] Suffix detection was case-sensitive (`StringComparison.Ordinal`) while surrounding query paths are case-insensitive (folded / NOCASE). `references myauditattribute` and `inspect MyAuditATTRIBUTE` silently lost the `MyAudit` alias. Fixes: - `ComputeCSharpAttributeSuffixAlias` now returns `null` when `referenceKind` is a non-`attribute` call-graph kind, so `--kind call` etc. stay strict. - Suffix detection now uses `OrdinalIgnoreCase` so lowercase / mixed-case queries still produce the alias. The stripped prefix preserves the original case for the `COLLATE NOCASE` / folded comparison. - All three reader methods (`SearchReferences`, `CountSearchReferences`, `CountSearchReferencesTotal`) clamp the alias SQL disjunct to `f.lang = 'csharp' AND r.reference_kind = 'attribute'`, so unscoped queries cannot bleed into Java annotations or C# call rows. Added regression tests: - `_NotAppliedToCallKind` (alias must not fire under `--kind call`) - `_UnscopedLangStillLimitsToCSharpAttributeRows` (no Java / no C# call) - `_CaseInsensitiveQuery` (lowercase + mixed-case still match) Co-Authored-By: Claude Opus 4 --- CHANGELOG.md | 4 +- src/CodeIndex/Database/DbReader.cs | 60 ++++++++++++---- tests/CodeIndex.Tests/DbReaderTests.cs | 97 ++++++++++++++++++++++++++ 3 files changed, 145 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 215ad432cf..fbf3706d4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), #### Fixed - **MCP `callers` / `callees` `kind` schema, CLI help, and README `--kind` option row no longer advertise metadata kinds as valid filter values (#293 follow-up)** — The `callers` / `callees` handlers reject `kind: "attribute"` / `kind: "annotation"` with a usage error (CLI) / `isError` tool response (MCP), but the MCP tool schema's `kind` property description still listed all five kinds as if they were valid filter values (`"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"`), `cdidx --help` similarly lumped `callers` / `callees` together with `references` for the `--kind` help row, and the README `--kind` option table (EN line 436 / JP line 1307) still directed readers to pass `--kind attribute` or `--kind annotation` on `callers` / `callees`. Schema and handler now agree: the `callers` / `callees` `kind` description now reads `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."`, the CLI `--kind` help row splits `references` (accepts all five kinds) from `callers` / `callees` (call-graph kinds only, metadata rejected, redirects to `references`), and the README `--kind` row (both English and Japanese) now explicitly states that `callers` / `callees` reject `--kind attribute|annotation` with a usage error and points to `references --kind attribute|annotation` as the correct metadata-enumeration path. The MCP `references` tool description is unchanged because it still accepts every indexed kind. Added `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` to pin the corrected schema wording. Affected: `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `src/CodeIndex/Cli/ConsoleUi.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `README.md`. -- **`deps`, `impact`, `references`, `inspect`, and `analyze_symbol` retain metadata references as compile-time dependency edges and canonicalize the C# attribute-suffix convention (#293 follow-up)** — `DbReader.GetFileDependencies` (backing `cdidx deps`) and `DbReader.GetFileDependencyHintsToResolvedType` (backing `cdidx impact` / MCP `impact_analysis` heuristic file-hint fallback) keep metadata references in their logical reference CTE. Renaming or removing an attribute class still breaks annotated sites such as `[JsonConverterAttribute]` at compile time, so file-level blast-radius analysis must surface those edges as real dependencies — now labeled `attribute` / `annotation` in `logical_reference_kind` (previously these same sites leaked in as phantom `call` rows before classification was corrected). `GetFileDependencies` additionally canonicalizes the C# attribute naming convention: idiomatic `[Foo]` usage is stored with `symbol_name = "Foo"` but the defining class is `FooAttribute`, so the logical-reference CTE now UNION-ALLs a suffix-aliased copy of every C# `attribute`-kind row with `'Attribute'` appended (only when the original symbol_name does not already end in `Attribute`) so the target-file join reaches the attribute class file. `GetFileDependencyHintsToResolvedType` also aggregates a per-candidate `has_metadata_ref` flag and bypasses the `SourceFileHasStructuredTypeEvidence` guard for edges whose contributing references are purely metadata, so pure-attribute consumer files such as `[MyAudit] class Svc` now surface through `impact MyAuditAttribute` instead of being filtered out. `ResolveImpactFallbackNames` symmetrically adds suffix-stripped aliases for C# class names ending in `Attribute`, so the BFS-less file-hint path can find `[Foo]` sites whose source symbol_name does not match the defining class name. The same canonicalization is propagated to the symbol-level reference reader: `DbReader.SearchReferences`, `CountSearchReferences`, and `CountSearchReferencesTotal` (backing `references`, `inspect`, MCP `analyze_symbol`) now compute a C#-only `queryAttributeAlias` (defined in the new `ComputeCSharpAttributeSuffixAlias` helper) and OR it into both exact (folded and NOCASE) and substring WHERE clauses, so `references MyAuditAttribute` (and `inspect MyAuditAttribute` / `analyze_symbol MyAuditAttribute`) surface the idiomatic `[MyAudit]` call site consistently with `deps` / `impact`. Substring mode uses an exact-OR for the alias (not a second LIKE) so user queries like `MyAuditAttribute` do not over-match unrelated names such as `FooAuditLog`. The alias is C# only, so Java / Kotlin / Scala / TypeScript annotation sites remain unaffected. `callers` / `callees` intentionally skip the alias because they filter to the shared call-graph kinds (`call`, `instantiate`, `subscribe`) and reject `--kind attribute|annotation` at the CLI / MCP boundary — those commands model the dynamic call graph, not the dependency graph. Added regression coverage `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies` (runtime + metadata edges both surface), `DbReaderTests.GetFileDependencies_MatchesCSharpAttributeSuffixConvention` (`[MyAudit]` → `MyAuditAttribute` canonicalization through `deps`), and `DbReaderTests.SearchReferences_MatchesCSharpAttributeSuffixConvention_Substring` / `_Exact` / `_CSharpAttributeSuffixAliasDoesNotBleedToOtherLanguages` (same canonicalization through `references` / `inspect` / `analyze_symbol`, plus C#-only scope guard). Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. +- **`deps`, `impact`, `references`, `inspect`, and `analyze_symbol` retain metadata references as compile-time dependency edges and canonicalize the C# attribute-suffix convention (#293 follow-up)** — `DbReader.GetFileDependencies` (backing `cdidx deps`) and `DbReader.GetFileDependencyHintsToResolvedType` (backing `cdidx impact` / MCP `impact_analysis` heuristic file-hint fallback) keep metadata references in their logical reference CTE. Renaming or removing an attribute class still breaks annotated sites such as `[JsonConverterAttribute]` at compile time, so file-level blast-radius analysis must surface those edges as real dependencies — now labeled `attribute` / `annotation` in `logical_reference_kind` (previously these same sites leaked in as phantom `call` rows before classification was corrected). `GetFileDependencies` additionally canonicalizes the C# attribute naming convention: idiomatic `[Foo]` usage is stored with `symbol_name = "Foo"` but the defining class is `FooAttribute`, so the logical-reference CTE now UNION-ALLs a suffix-aliased copy of every C# `attribute`-kind row with `'Attribute'` appended (only when the original symbol_name does not already end in `Attribute`) so the target-file join reaches the attribute class file. `GetFileDependencyHintsToResolvedType` also aggregates a per-candidate `has_metadata_ref` flag and bypasses the `SourceFileHasStructuredTypeEvidence` guard for edges whose contributing references are purely metadata, so pure-attribute consumer files such as `[MyAudit] class Svc` now surface through `impact MyAuditAttribute` instead of being filtered out. `ResolveImpactFallbackNames` symmetrically adds suffix-stripped aliases for C# class names ending in `Attribute`, so the BFS-less file-hint path can find `[Foo]` sites whose source symbol_name does not match the defining class name. The same canonicalization is propagated to the symbol-level reference reader: `DbReader.SearchReferences`, `CountSearchReferences`, and `CountSearchReferencesTotal` (backing `references`, `inspect`, MCP `analyze_symbol`) now compute a C#-only `queryAttributeAlias` (defined in the new `ComputeCSharpAttributeSuffixAlias` helper) and OR it into both exact (folded and NOCASE) and substring WHERE clauses, so `references MyAuditAttribute` (and `inspect MyAuditAttribute` / `analyze_symbol MyAuditAttribute`) surface the idiomatic `[MyAudit]` call site consistently with `deps` / `impact`. Substring mode uses an exact-OR for the alias (not a second LIKE) so user queries like `MyAuditAttribute` do not over-match unrelated names such as `FooAuditLog`. The alias is C# only, so Java / Kotlin / Scala / TypeScript annotation sites remain unaffected. `callers` / `callees` intentionally skip the alias because they filter to the shared call-graph kinds (`call`, `instantiate`, `subscribe`) and reject `--kind attribute|annotation` at the CLI / MCP boundary — those commands model the dynamic call graph, not the dependency graph. The alias branch is additionally scoped so it cannot produce false positives: `ComputeCSharpAttributeSuffixAlias` returns `null` when `referenceKind` is a non-`attribute` call-graph kind (so `references FooAttribute --kind call` stays strict and does not match `Foo()`), and the SQL alias disjunct in all three reader methods is clamped to `f.lang = 'csharp' AND r.reference_kind = 'attribute'` so an unscoped `references FooAttribute` (no `--lang` / no `--kind`) still cannot bleed into Java annotations or C# call rows. Suffix detection is also case-insensitive (`OrdinalIgnoreCase`) so `references myauditattribute` and `inspect MyAuditATTRIBUTE` still produce the `MyAudit` alias, consistent with the NOCASE / folded contract of the surrounding exact / substring query paths. Added regression coverage `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies` (runtime + metadata edges both surface), `DbReaderTests.GetFileDependencies_MatchesCSharpAttributeSuffixConvention` (`[MyAudit]` → `MyAuditAttribute` canonicalization through `deps`), `DbReaderTests.SearchReferences_MatchesCSharpAttributeSuffixConvention_Substring` / `_Exact` / `_CSharpAttributeSuffixAliasDoesNotBleedToOtherLanguages` (same canonicalization through `references` / `inspect` / `analyze_symbol`, plus C#-only scope guard), `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_NotAppliedToCallKind` (alias must not fire under `--kind call`), `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_UnscopedLangStillLimitsToCSharpAttributeRows` (unscoped query must not match Java annotations or C# call rows), and `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_CaseInsensitiveQuery` (lowercase / mixed-case queries still produce the alias). Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. - **`callers` / `callees` now reject `--kind attribute|annotation` at the CLI and MCP boundary, and Swift / Gradle `@` metadata is reclassified as `annotation` (#293 follow-up)** — Metadata rows (`attribute` / `annotation`) are attributed to their enclosing body-range symbol rather than the annotated target, so `callers Obsolete --kind attribute` would silently return `[Obsolete] void M()` under the enclosing class (not `M`), and file-level targets such as `[assembly: CLSCompliant]` or Kotlin `@file:JvmName` would drop entirely because `container_name` is `NULL`. `QueryCommandRunner.RunCallers` / `RunCallees` now reject `--kind attribute` and `--kind annotation` with `CommandExitCodes.UsageError`, and the MCP `callers` / `callees` tools return an `isError` tool response; both paths direct users to `references --kind attribute|annotation`, which IS a correct metadata enumeration path. Separately, `AnnotationLanguages` now includes `swift` and `gradle`, so Swift `@available(iOS 13.0, *)` / `@objc` / `@MainActor` and Gradle/Groovy `@CompileStatic` / `@TaskAction` are recorded as `annotation` instead of `call` (previously the `@Name(args)` forms leaked into `callers` / `callees` / `hotspots` / `impact` as phantom call edges, and the no-arg `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` forms dropped from the index entirely because no-arg annotation emission was gated to languages in this set). Added `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError` (theory, 4 cases), `McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError` (theory, 4 cases), `ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`, and `ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation`. Affected: `src/CodeIndex/Cli/QueryCommandRunner.cs`, `src/CodeIndex/Mcp/McpToolHandlers.cs`, `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. - **Multi-line no-arg C# attribute sections like `[\n Serializable\n]` are now indexed without over-matching attribute argument identifiers (#293 follow-up)** — The no-arg C# attribute regex previously required the match to start at a `[` / `,` boundary and end at `]` / `,` on the same line, so a bare identifier on the interior line of a multi-line attribute section (e.g. ` Serializable` between a line-leading `[` and a line-leading `]`) was silently dropped from `references --kind attribute`. The left anchor is now a word-boundary lookbehind and the right anchor also accepts end-of-line. To avoid misclassifying enum / qualified-constant identifiers that happen to sit at end-of-line inside an attribute argument list (e.g. `ConverterStrategy.AllowNumbers` in `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]`) as no-arg attributes, `BuildCSharpAttributeRanges` now also emits a parallel "top-level zone" table that splits each attribute section by `(` / `)` so only paren-depth-0 positions inside the section qualify as no-arg attribute name positions. Paren depth is tracked section-locally (each `[` snapshots the global depth at open time) so parameter attributes like `void M([FromServices] IService s)`, delegate parameter attributes like `delegate void D([Attr] int x)`, and lambda parameter attributes like `Func f = ([Attr] int x) => x` — whose `[` opens while the global paren depth is already > 0 — still have their attribute-list top level correctly identified. The no-arg attribute path uses that top-level table instead of the full attribute-section ranges, while the existing with-args metadata classification still uses the full ranges. `SymbolExtractor` also now tracks multi-line `[...]` depth across lines via `StripMultiLineCSharpAttributeInterior` in `BuildCSharpMatchLines`, so interior lines of a multi-line attribute section are blanked out before declaration regexes run — this prevents ` Serializable` from being mis-extracted as a top-level `function` symbol (which then masked the reference through the `definitionNames` guard). The multi-line blanker now activates on any `[` that opens without a matching `]` on the same line, not just `[` that sits at the start of the line, so multi-line parameter attributes like `void M([\n FromServices\n] IService s)`, type-parameter attributes like `class Bar<[\n TypeParamAttr\n] T>`, and delegate/lambda parameter attributes like `delegate void D([\n DelegateParamAttr\n] int x)` are also blanked out correctly — previously only the leading-`[` case was covered, so the interior identifiers of those forms were mis-extracted as `function` symbols and their attribute references were silently dropped from `references --kind attribute`. Regression coverage adds `[\n Serializable\n]`, `[\n global::System.Obsolete\n]`, `[Required,\n Key]`, `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]` (pinning that `AllowNumbers` / `ConverterStrategy` are NOT classified as `attribute`), and the non-leading-`[` parameter / type-parameter / delegate-parameter multi-line cases above, running through `SymbolExtractor.Extract` + `ReferenceExtractor.Extract` end-to-end so both sides of the regression stay pinned. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Indexer/SymbolExtractor.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. - **`::`-qualified C# no-arg attributes and JavaScript `@Decorator` usages are now classified correctly (#293 follow-up)** — The no-arg C# attribute regex now accepts `::` alongside `.` as a qualifier separator, so `[global::System.Obsolete]` and `[Alias::MyAttr]` reach `references --kind attribute` instead of being silently dropped. `AnnotationLanguages` additionally includes `javascript`, so JS decorators such as `@sealed` (no-arg) and `@injectable()` (with args) are reclassified to `annotation` rather than leaking into the call-graph as phantom `call` rows — matching the existing TypeScript contract since JavaScript is already a graph-supported language. Added regression tests pinning `[global::System.Obsolete]`, `[Alias::MyAttr]`, and JavaScript `@sealed` / `@injectable()`. Also rewrote the README `callers` row (EN + JP) and the MCP tool descriptions for `callers` / `callees` to pull back the earlier over-promise that `kind: "attribute"` / `kind: "annotation"` on `callers` / `callees` is a supported metadata-inspection path: a metadata row is attributed to its enclosing body-range symbol (for a class-level declaration, that is the class itself) and drops entirely for file-level targets such as `[assembly: ...]` where `container_name` is `null`, so the docs now direct users to `references --kind attribute|annotation` for metadata enumeration. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`, `README.md`. @@ -686,7 +686,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), #### 修正 - **MCP の `callers` / `callees` の `kind` schema・CLI help・README の `--kind` オプション行が metadata kinds を有効フィルタ値として広告しないよう修正 (#293 追加対応)** — `callers` / `callees` の handler は `kind: "attribute"` / `kind: "annotation"` を usage error(CLI)/ `isError` ツールレスポンス(MCP)で拒否するにもかかわらず、MCP ツール schema の `kind` プロパティ説明は 5 種すべてを列挙した `"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"` のままで、`cdidx --help` の `--kind` 行も `callers` / `callees` を `references` と同じ行にまとめていた。さらに README の `--kind` オプション表(英語 436 行目 / 日本語 1307 行目)も、`callers` / `callees` で `--kind attribute` / `--kind annotation` を渡せるかのように案内していた。schema と handler の契約を一致させ、`callers` / `callees` の `kind` description を `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."` に修正し、CLI の `--kind` help 行も `references`(5 種すべて受理)と `callers` / `callees`(call-graph 種別のみ、metadata kinds は拒否して `references` へ誘導)を分けるようにし、README の `--kind` 行(英語・日本語の両方)も `callers` / `callees` が `--kind attribute|annotation` を usage error で拒否すること、metadata 列挙の正規経路が `references --kind attribute|annotation` であることを明示する文言に直した。MCP の `references` ツール説明は全 reference kind を受け付ける契約のままなので変更なし。修正後の schema 文言を固定する `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` を追加。対象: `src/CodeIndex/Mcp/McpToolDefinitions.cs`、`src/CodeIndex/Cli/ConsoleUi.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`、`README.md`。 -- **`deps`・`impact`・`references`・`inspect`・`analyze_symbol` が metadata 参照を compile-time 依存として保持し、C# の attribute サフィックス命名規約を正規化 (#293 追加対応)** — `cdidx deps` の実装である `DbReader.GetFileDependencies` と、`cdidx impact` / MCP `impact_analysis` の heuristic file-hint フォールバックである `DbReader.GetFileDependencyHintsToResolvedType` は、logical reference CTE に metadata 参照も含める。attribute クラスを rename / 削除すれば `[JsonConverterAttribute]` のような注釈サイトも compile-time で壊れるため、ファイル単位の blast-radius 分析はそれらも本物の依存 edge として表示する必要があり、`logical_reference_kind` では `attribute` / `annotation` として正しくラベル付けされる(分類修正前は同じサイトが phantom `call` として漏れていた)。さらに `GetFileDependencies` は C# の attribute 命名規約を正規化する: 慣用的な `[Foo]` は `symbol_name = "Foo"` で保存されるが、定義クラスは `FooAttribute` 命名となるため、logical-reference CTE に C# `attribute` 種別の行をサフィックス `'Attribute'` を付与した別名コピーとして UNION ALL で追加する(元の symbol_name が既に `Attribute` で終わっている場合は重複しないようスキップ)。これにより target ファイル join が attribute クラス側のファイルへ到達する。`GetFileDependencyHintsToResolvedType` はさらに candidate ごとに `has_metadata_ref` フラグを集約し、寄与する参照が metadata だけである edge については `SourceFileHasStructuredTypeEvidence` guard をバイパスするため、`[MyAudit] class Svc` のような純 attribute consumer ファイルも `impact MyAuditAttribute` で出るようになった(以前は filter で落ちていた)。`ResolveImpactFallbackNames` も対称的に、`Attribute` で終わる C# クラス名にはサフィックスを外した別名を追加するため、BFS を使わない file-hint 経路でも source symbol_name が定義クラス名と一致しない `[Foo]` サイトを発見できる。同じ正規化をシンボル単位の参照 reader にも伝播する: `references`・`inspect`・MCP `analyze_symbol` を支える `DbReader.SearchReferences`・`CountSearchReferences`・`CountSearchReferencesTotal` は、新しいヘルパー `ComputeCSharpAttributeSuffixAlias` で C# 限定の `queryAttributeAlias` を計算し、exact(folded と NOCASE)・substring の両方の WHERE 句に OR で追加する。これにより `references MyAuditAttribute`(および `inspect MyAuditAttribute` / `analyze_symbol MyAuditAttribute`)でも慣用的な `[MyAudit]` 参照サイトが `deps` / `impact` と一貫して surface する。substring モードでは alias を二つ目の LIKE にせず exact-OR で束ねているため、ユーザーが `MyAuditAttribute` と打った時に `FooAuditLog` のような無関係な名前に over-match しない。alias は C# 限定なので、Java / Kotlin / Scala / TypeScript の annotation サイトには影響しない。`callers` / `callees` は共有の call-graph 種別 (`call` / `instantiate` / `subscribe`) に絞って `--kind attribute|annotation` を CLI / MCP 境界で拒否する契約なので、alias は意図的に適用しない(dynamic call graph と dependency graph は別契約)。回帰テスト `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies`(runtime と metadata の両方の edge が出ることを固定)、`DbReaderTests.GetFileDependencies_MatchesCSharpAttributeSuffixConvention`(`deps` 経由での `[MyAudit]` → `MyAuditAttribute` 正規化)、`DbReaderTests.SearchReferences_MatchesCSharpAttributeSuffixConvention_Substring` / `_Exact` / `_CSharpAttributeSuffixAliasDoesNotBleedToOtherLanguages`(`references` / `inspect` / `analyze_symbol` での同一正規化と C# 限定スコープ)を追加。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 +- **`deps`・`impact`・`references`・`inspect`・`analyze_symbol` が metadata 参照を compile-time 依存として保持し、C# の attribute サフィックス命名規約を正規化 (#293 追加対応)** — `cdidx deps` の実装である `DbReader.GetFileDependencies` と、`cdidx impact` / MCP `impact_analysis` の heuristic file-hint フォールバックである `DbReader.GetFileDependencyHintsToResolvedType` は、logical reference CTE に metadata 参照も含める。attribute クラスを rename / 削除すれば `[JsonConverterAttribute]` のような注釈サイトも compile-time で壊れるため、ファイル単位の blast-radius 分析はそれらも本物の依存 edge として表示する必要があり、`logical_reference_kind` では `attribute` / `annotation` として正しくラベル付けされる(分類修正前は同じサイトが phantom `call` として漏れていた)。さらに `GetFileDependencies` は C# の attribute 命名規約を正規化する: 慣用的な `[Foo]` は `symbol_name = "Foo"` で保存されるが、定義クラスは `FooAttribute` 命名となるため、logical-reference CTE に C# `attribute` 種別の行をサフィックス `'Attribute'` を付与した別名コピーとして UNION ALL で追加する(元の symbol_name が既に `Attribute` で終わっている場合は重複しないようスキップ)。これにより target ファイル join が attribute クラス側のファイルへ到達する。`GetFileDependencyHintsToResolvedType` はさらに candidate ごとに `has_metadata_ref` フラグを集約し、寄与する参照が metadata だけである edge については `SourceFileHasStructuredTypeEvidence` guard をバイパスするため、`[MyAudit] class Svc` のような純 attribute consumer ファイルも `impact MyAuditAttribute` で出るようになった(以前は filter で落ちていた)。`ResolveImpactFallbackNames` も対称的に、`Attribute` で終わる C# クラス名にはサフィックスを外した別名を追加するため、BFS を使わない file-hint 経路でも source symbol_name が定義クラス名と一致しない `[Foo]` サイトを発見できる。同じ正規化をシンボル単位の参照 reader にも伝播する: `references`・`inspect`・MCP `analyze_symbol` を支える `DbReader.SearchReferences`・`CountSearchReferences`・`CountSearchReferencesTotal` は、新しいヘルパー `ComputeCSharpAttributeSuffixAlias` で C# 限定の `queryAttributeAlias` を計算し、exact(folded と NOCASE)・substring の両方の WHERE 句に OR で追加する。これにより `references MyAuditAttribute`(および `inspect MyAuditAttribute` / `analyze_symbol MyAuditAttribute`)でも慣用的な `[MyAudit]` 参照サイトが `deps` / `impact` と一貫して surface する。substring モードでは alias を二つ目の LIKE にせず exact-OR で束ねているため、ユーザーが `MyAuditAttribute` と打った時に `FooAuditLog` のような無関係な名前に over-match しない。alias は C# 限定なので、Java / Kotlin / Scala / TypeScript の annotation サイトには影響しない。`callers` / `callees` は共有の call-graph 種別 (`call` / `instantiate` / `subscribe`) に絞って `--kind attribute|annotation` を CLI / MCP 境界で拒否する契約なので、alias は意図的に適用しない(dynamic call graph と dependency graph は別契約)。さらに alias 節が誤一致を生まないようスコープする: `ComputeCSharpAttributeSuffixAlias` は `referenceKind` が `attribute` 以外の call-graph 種別のときは `null` を返す(`references FooAttribute --kind call` は厳格一致のままで `Foo()` に一致しない)、3 つの reader メソッドの SQL alias 節は `f.lang = 'csharp' AND r.reference_kind = 'attribute'` に限定するため、`--lang` / `--kind` 無指定の `references FooAttribute` でも Java の annotation や C# の call 行を拾わない。suffix 検出も `OrdinalIgnoreCase` にして case-insensitive 化したため、`references myauditattribute` / `inspect MyAuditATTRIBUTE` のような小文字・混在ケースでも alias `MyAudit` を生成でき、周辺の NOCASE / folded 契約と整合する。回帰テスト `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies`(runtime と metadata の両方の edge が出ることを固定)、`DbReaderTests.GetFileDependencies_MatchesCSharpAttributeSuffixConvention`(`deps` 経由での `[MyAudit]` → `MyAuditAttribute` 正規化)、`DbReaderTests.SearchReferences_MatchesCSharpAttributeSuffixConvention_Substring` / `_Exact` / `_CSharpAttributeSuffixAliasDoesNotBleedToOtherLanguages`(`references` / `inspect` / `analyze_symbol` での同一正規化と C# 限定スコープ)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_NotAppliedToCallKind`(`--kind call` で alias が発火しないこと)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_UnscopedLangStillLimitsToCSharpAttributeRows`(無指定でも Java annotation や C# call 行に一致しないこと)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_CaseInsensitiveQuery`(小文字・混在ケースのクエリでも alias を生成)を追加。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 - **`callers` / `callees` が CLI と MCP 境界で `--kind attribute|annotation` を拒否し、Swift / Gradle の `@` metadata を `annotation` に再分類 (#293 追加対応)** — metadata 行 (`attribute` / `annotation`) は注釈対象そのものではなく body-range の外側シンボルに帰属するため、`callers Obsolete --kind attribute` は `[Obsolete] void M()` を `M` ではなく外側クラスの下に黙って返し、`[assembly: CLSCompliant]` や Kotlin の `@file:JvmName` のような file-level target は `container_name = NULL` で完全脱落していた。`QueryCommandRunner.RunCallers` / `RunCallees` は `--kind attribute` / `--kind annotation` を `CommandExitCodes.UsageError` で弾き、MCP の `callers` / `callees` ツールは `isError` ツールレスポンスを返すようにした。どちらの経路も、metadata 列挙の正しいパスである `references --kind attribute|annotation` へ誘導する。あわせて `AnnotationLanguages` に `swift` と `gradle` を追加し、Swift の `@available(iOS 13.0, *)` / `@objc` / `@MainActor` や Gradle / Groovy の `@CompileStatic` / `@TaskAction` を `call` ではなく `annotation` として記録するようにした(修正前は引数付きの `@Name(args)` 形は `callers` / `callees` / `hotspots` / `impact` に phantom call edge として漏れ、引数なしの `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` 形はこの集合で gate されていた no-arg annotation 出力から外れてインデックス自体に残らなかった)。回帰テストとして `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError`(theory 4 ケース)、`McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError`(theory 4 ケース)、`ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`、`ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation` を追加。対象: `src/CodeIndex/Cli/QueryCommandRunner.cs`、`src/CodeIndex/Mcp/McpToolHandlers.cs`、`src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 - **`[\n Serializable\n]` のような複数行にまたがる引数なし C# 属性セクションを、属性引数内の識別子を誤って取り込まずに索引化 (#293 追加対応)** — 引数なし C# 属性の正規表現は、これまで `[` / `,` を左端、`]` / `,` を右端として同一行内でアンカーしていたため、行頭 `[` と行頭 `]` の間に書かれた ` Serializable` のような裸識別子が `references --kind attribute` から黙って脱落していた。左側は単語境界 lookbehind、右側は行末(`$`)も許容するように緩和した。一方で `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]` のように属性の引数リスト内で行末に現れる enum / 修飾定数識別子(`AllowNumbers` など)が no-arg 属性として誤分類される回帰を避けるため、`BuildCSharpAttributeRanges` は `(` / `)` で属性セクションを分割した paren 深さ 0 の「top-level ゾーン」テーブルも並列で出力するようにし、no-arg 属性の採用位置はそのテーブルに限定した(既存の引数付き metadata 分類パスは引き続き属性セクション全体のレンジを使う)。paren 深さは section-local に追跡する(各 `[` がその瞬間のグローバル paren 深さを snapshot として保持)ため、`void M([FromServices] IService s)` のようなパラメータ属性、`delegate void D([Attr] int x)` のようなデリゲートパラメータ属性、`Func f = ([Attr] int x) => x` のようなラムダパラメータ属性のように、`[` が開いた時点で既にグローバル paren 深さが 0 より大きいケースでも、属性リストの top-level を正しく認識できる。`SymbolExtractor` 側でも `BuildCSharpMatchLines` に `StripMultiLineCSharpAttributeInterior` を追加し、複数行にまたがる `[...]` セクションの深さを跨行で追跡して内部行を宣言 regex 適用前に空白化するようにした。これにより ` Serializable` がトップレベル `function` シンボルとして誤抽出され、`definitionNames` ガード経由で参照分類が詰まってしまうのを防ぐ。さらに、複数行の空白化起動条件を「同一行内で閉じない `[` があるかどうか」に広げ、行頭の `[` だけでなく `void M([\n FromServices\n] IService s)` のようなパラメータ属性、`class Bar<[\n TypeParamAttr\n] T>` のような型パラメータ属性、`delegate void D([\n DelegateParamAttr\n] int x)` のようなデリゲート / ラムダのパラメータ属性でも内部行が正しく空白化されるようにした — 以前は開口行で `[` が行頭にある場合しかカバーされていなかったため、上記の形の内部識別子が `function` シンボルとして誤抽出され、`references --kind attribute` から属性参照が黙って脱落していた。`[\n Serializable\n]`・`[\n global::System.Obsolete\n]`・`[Required,\n Key]`・`[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]`(`AllowNumbers` / `ConverterStrategy` が `attribute` として取り込まれないことを固定)に加え、上記の行頭でない `[` で開く複数行パラメータ / 型パラメータ / デリゲートパラメータ属性のケースも `SymbolExtractor.Extract` + `ReferenceExtractor.Extract` で end-to-end に回して SymbolExtractor 側の回帰も固定する回帰テストを追加。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Indexer/SymbolExtractor.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 - **`::` 修飾の C# 引数なし属性と JavaScript の `@Decorator` を正しく分類 (#293 追加対応)** — 引数なし C# 属性の正規表現が修飾子セパレータとして `.` に加えて `::` も受け付けるようになり、`[global::System.Obsolete]` や `[Alias::MyAttr]` のような形が `references --kind attribute` から黙って脱落しなくなった。`AnnotationLanguages` にも `javascript` を追加し、JavaScript はもともと graph 対応言語なので、`@sealed`(引数なし)や `@injectable()`(引数付き)のような JS decorator も `annotation` として再分類され、TypeScript と同じ契約で call-graph に phantom `call` 行を残さない。`[global::System.Obsolete]`・`[Alias::MyAttr]`・JavaScript の `@sealed` / `@injectable()` を押さえる回帰テストを追加。あわせて、以前に進めていた `callers` / `callees` の metadata 契約を README(英日)と MCP ツール説明の両方で引き戻し、metadata 行は注釈対象そのものではなく body-range 上の外側シンボル(クラス直下宣言ならクラス、`[assembly: ...]` のようなファイルレベル target では `container_name` が `null`)に帰属するため `kind: "attribute"` / `kind: "annotation"` を `callers` / `callees` に渡すのは信頼できる metadata 調査経路ではなく、metadata 列挙は `references --kind attribute|annotation` を使うよう案内する文言に修正した。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Mcp/McpToolDefinitions.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`、`README.md`。 diff --git a/src/CodeIndex/Database/DbReader.cs b/src/CodeIndex/Database/DbReader.cs index 1fa10309ad..ef377a9f0d 100644 --- a/src/CodeIndex/Database/DbReader.cs +++ b/src/CodeIndex/Database/DbReader.cs @@ -677,7 +677,17 @@ FROM symbol_references r if (referenceKind != null) sql += $" AND {BuildGraphSupportedLanguagePredicate(cmd, "f", "graphLang")}"; - var referencesSuffixAlias = ComputeCSharpAttributeSuffixAlias(query, lang); + var referencesSuffixAlias = ComputeCSharpAttributeSuffixAlias(query, lang, referenceKind); + // When the alias fires without an explicit `lang` / `--kind` scope we still need + // to keep it from bleeding into non-C# rows or non-attribute rows. The SQL guard + // clamps the alias disjunct to `f.lang = 'csharp' AND r.reference_kind = 'attribute'` + // so unscoped `references FooAttribute` only picks up real C# attribute sites. + // alias が `--lang` / `--kind` スコープなしで発火するときも、C# 以外の行や + // attribute 以外の行を拾わないように、SQL 側で `f.lang = 'csharp' AND + // r.reference_kind = 'attribute'` に限定する。 + var referencesAliasScope = referencesSuffixAlias != null + ? " AND f.lang = 'csharp' AND r.reference_kind = 'attribute'" + : string.Empty; if (query != null) { // --exact: Unicode-aware equality when FoldReady (#86), else ASCII COLLATE NOCASE. @@ -688,21 +698,23 @@ FROM symbol_references r // `[Foo]` reference site stored with `symbol_name = "Foo"`. In substring mode we // still LIKE-match `%FooAttribute%` and add only the exact stripped alias to avoid // overmatching unrelated names (e.g. `FooAuditLog`) that share the stripped prefix. + // The alias disjunct is scoped to C# attribute rows to avoid false positives. // --exact: FoldReady なら Unicode 折り畳み経路、未 ready なら ASCII NOCASE へ fallback。 // C# の `Attribute` suffix が付いたクエリは、suffix を外した別名とも照合する。 // 部分一致モードでは `%FooAttribute%` をそのまま使い、別名側は exact 照合だけを OR // することで `FooAuditLog` など無関係な名前を巻き込まないようにする。 + // 別名節は C# の attribute 行に限定し、誤一致を避ける。 if (exact && _foldReady) sql += referencesSuffixAlias != null - ? " AND (r.symbol_name_folded = @query OR r.symbol_name_folded = @queryAttributeAlias)" + ? $" AND (r.symbol_name_folded = @query OR (r.symbol_name_folded = @queryAttributeAlias{referencesAliasScope}))" : " AND r.symbol_name_folded = @query"; else if (exact) sql += referencesSuffixAlias != null - ? " AND (r.symbol_name = @query COLLATE NOCASE OR r.symbol_name = @queryAttributeAlias COLLATE NOCASE)" + ? $" AND (r.symbol_name = @query COLLATE NOCASE OR (r.symbol_name = @queryAttributeAlias COLLATE NOCASE{referencesAliasScope}))" : " AND r.symbol_name = @query COLLATE NOCASE"; else sql += referencesSuffixAlias != null - ? " AND (r.symbol_name LIKE @query ESCAPE '\\' OR r.symbol_name = @queryAttributeAlias COLLATE NOCASE)" + ? $" AND (r.symbol_name LIKE @query ESCAPE '\\' OR (r.symbol_name = @queryAttributeAlias COLLATE NOCASE{referencesAliasScope}))" : " AND r.symbol_name LIKE @query ESCAPE '\\'"; } if (referenceKind != null) @@ -799,20 +811,23 @@ FROM symbol_references r WHERE 1=1"; innerSql += $" AND {BuildGraphSupportedLanguagePredicate(cmd, "f", "graphLang")}"; - var countSuffixAlias = ComputeCSharpAttributeSuffixAlias(query, lang); + var countSuffixAlias = ComputeCSharpAttributeSuffixAlias(query, lang, referenceKind); + var countAliasScope = countSuffixAlias != null + ? " AND f.lang = 'csharp' AND r.reference_kind = 'attribute'" + : string.Empty; if (query != null) { if (exact && _foldReady) innerSql += countSuffixAlias != null - ? " AND (r.symbol_name_folded = @query OR r.symbol_name_folded = @queryAttributeAlias)" + ? $" AND (r.symbol_name_folded = @query OR (r.symbol_name_folded = @queryAttributeAlias{countAliasScope}))" : " AND r.symbol_name_folded = @query"; else if (exact) innerSql += countSuffixAlias != null - ? " AND (r.symbol_name = @query COLLATE NOCASE OR r.symbol_name = @queryAttributeAlias COLLATE NOCASE)" + ? $" AND (r.symbol_name = @query COLLATE NOCASE OR (r.symbol_name = @queryAttributeAlias COLLATE NOCASE{countAliasScope}))" : " AND r.symbol_name = @query COLLATE NOCASE"; else innerSql += countSuffixAlias != null - ? " AND (r.symbol_name LIKE @query ESCAPE '\\' OR r.symbol_name = @queryAttributeAlias COLLATE NOCASE)" + ? $" AND (r.symbol_name LIKE @query ESCAPE '\\' OR (r.symbol_name = @queryAttributeAlias COLLATE NOCASE{countAliasScope}))" : " AND r.symbol_name LIKE @query ESCAPE '\\'"; } if (referenceKind != null) @@ -868,20 +883,23 @@ FROM symbol_references r WHERE 1=1"; innerSql += $" AND {BuildGraphSupportedLanguagePredicate(cmd, "f", "graphLang")}"; - var totalSuffixAlias = ComputeCSharpAttributeSuffixAlias(query, lang); + var totalSuffixAlias = ComputeCSharpAttributeSuffixAlias(query, lang, referenceKind); + var totalAliasScope = totalSuffixAlias != null + ? " AND f.lang = 'csharp' AND r.reference_kind = 'attribute'" + : string.Empty; if (query != null) { if (exact && _foldReady) innerSql += totalSuffixAlias != null - ? " AND (r.symbol_name_folded = @query OR r.symbol_name_folded = @queryAttributeAlias)" + ? $" AND (r.symbol_name_folded = @query OR (r.symbol_name_folded = @queryAttributeAlias{totalAliasScope}))" : " AND r.symbol_name_folded = @query"; else if (exact) innerSql += totalSuffixAlias != null - ? " AND (r.symbol_name = @query COLLATE NOCASE OR r.symbol_name = @queryAttributeAlias COLLATE NOCASE)" + ? $" AND (r.symbol_name = @query COLLATE NOCASE OR (r.symbol_name = @queryAttributeAlias COLLATE NOCASE{totalAliasScope}))" : " AND r.symbol_name = @query COLLATE NOCASE"; else innerSql += totalSuffixAlias != null - ? " AND (r.symbol_name LIKE @query ESCAPE '\\' OR r.symbol_name = @queryAttributeAlias COLLATE NOCASE)" + ? $" AND (r.symbol_name LIKE @query ESCAPE '\\' OR (r.symbol_name = @queryAttributeAlias COLLATE NOCASE{totalAliasScope}))" : " AND r.symbol_name LIKE @query ESCAPE '\\'"; } if (referenceKind != null) @@ -1689,12 +1707,26 @@ FROM symbols s // `symbol_name = "Foo"` で保存される。ユーザーがクラス名で問い合わせたとき // (`references FooAttribute` 等) でも慣用的な利用サイトに到達できるよう、 // suffix を外した別名を返す。C# 以外の言語ではこの規約を持たないので適用しない。 - private static string? ComputeCSharpAttributeSuffixAlias(string? query, string? lang) + private static string? ComputeCSharpAttributeSuffixAlias(string? query, string? lang, string? referenceKind) { if (string.IsNullOrEmpty(query)) return null; if (lang != null && !lang.Equals("csharp", StringComparison.OrdinalIgnoreCase)) return null; + // Only metadata lookups should apply the suffix alias: ordinary call-graph + // queries (`--kind call` / `instantiate` / `subscribe`) must not match `Foo()` + // call rows when the user typed `FooAttribute`. When `referenceKind` is null, + // the SQL side additionally constrains the alias clause to attribute rows only. + // metadata 参照の問い合わせ時だけ alias を適用する: `--kind call` などの call-graph + // クエリは `FooAttribute` と入力されたときに `Foo()` の call 行に一致してはならない。 + // referenceKind が null のときは SQL 側でも alias 節を attribute 行に限定する。 + if (referenceKind != null && !referenceKind.Equals("attribute", StringComparison.OrdinalIgnoreCase)) + return null; const string suffix = "Attribute"; - if (!query!.EndsWith(suffix, StringComparison.Ordinal)) return null; + // Case-insensitive suffix detection so `references myauditattribute` and + // `inspect MyAuditATTRIBUTE` still produce the `MyAudit` alias, matching the + // NOCASE / folded contract of the surrounding exact/substring query paths. + // 大文字小文字を無視して suffix を検出することで、`myauditattribute` や + // `MyAuditATTRIBUTE` のような形でも alias を生成できる。 + if (!query!.EndsWith(suffix, StringComparison.OrdinalIgnoreCase)) return null; if (query.Length <= suffix.Length) return null; return query.Substring(0, query.Length - suffix.Length); } diff --git a/tests/CodeIndex.Tests/DbReaderTests.cs b/tests/CodeIndex.Tests/DbReaderTests.cs index 630802d99b..046daa8108 100644 --- a/tests/CodeIndex.Tests/DbReaderTests.cs +++ b/tests/CodeIndex.Tests/DbReaderTests.cs @@ -2430,6 +2430,103 @@ public class Svc { Assert.Empty(results); } + [Fact] + public void SearchReferences_CSharpAttributeSuffixAlias_NotAppliedToCallKind() + { + // Adversarial review #7 follow-up: the suffix alias must NOT bleed into + // `--kind call` queries. `references FooAttribute --kind call --lang csharp` + // must not match a plain `Foo()` call — that would be a false positive. + // adversarial review #7 補足: suffix alias を `--kind call` クエリに波及させない。 + // `references FooAttribute --kind call --lang csharp` が素の `Foo()` 呼び出しに + // 一致してはならない(誤一致になる)。 + InsertIndexedFile("src/Svc.cs", "csharp", + """ + public class Svc + { + public void Call() + { + MyAudit(); + } + } + """); + + var results = _reader.SearchReferences("MyAuditAttribute", lang: "csharp", referenceKind: "call"); + + Assert.DoesNotContain(results, r => r.SymbolName == "MyAudit"); + } + + [Fact] + public void SearchReferences_CSharpAttributeSuffixAlias_UnscopedLangStillLimitsToCSharpAttributeRows() + { + // When `--lang` is omitted, the alias must still only match C# attribute rows. + // A Java `@MyAudit(...)` or a bare `MyAudit()` call must not leak through. + // `--lang` を省略したときも、alias は C# の attribute 行にしか一致してはならない。 + // Java の `@MyAudit(...)` や素の `MyAudit()` 呼び出しが漏れてはならない。 + InsertIndexedFile("src/Svc.java", "java", + """ + @MyAudit + public class Svc { + } + """); + InsertIndexedFile("src/Caller.cs", "csharp", + """ + public class Caller + { + public void Go() + { + MyAudit(); + } + } + """); + InsertIndexedFile("src/Target.cs", "csharp", + """ + [MyAudit] + public class Target + { + } + """); + + var results = _reader.SearchReferences("MyAuditAttribute"); + + // Should include the C# attribute site on Target.cs … + Assert.Contains(results, r => r.Path == "src/Target.cs" && r.ReferenceKind == "attribute"); + // … but must NOT include the Java annotation nor the C# call row via alias. + Assert.DoesNotContain(results, r => r.Path == "src/Svc.java"); + Assert.DoesNotContain(results, r => r.Path == "src/Caller.cs" && r.ReferenceKind == "call"); + } + + [Fact] + public void SearchReferences_CSharpAttributeSuffixAlias_CaseInsensitiveQuery() + { + // The surrounding exact / substring paths are case-insensitive (folded or + // NOCASE), so the suffix-stripping step must also be case-insensitive — + // `references myauditattribute` / `MyAuditATTRIBUTE --exact` / etc. must + // still produce the `MyAudit` alias and reach the `[MyAudit]` site. + // 周辺の exact / substring 経路は case-insensitive(folded or NOCASE)なので、 + // suffix 除去も case-insensitive であるべき。 + InsertIndexedFile("src/MyAuditAttribute.cs", "csharp", + """ + using System; + + public sealed class MyAuditAttribute : Attribute + { + } + """); + InsertIndexedFile("src/Svc.cs", "csharp", + """ + [MyAudit] + public class Svc + { + } + """); + + var lowercaseResults = _reader.SearchReferences("myauditattribute", lang: "csharp"); + Assert.Contains(lowercaseResults, r => r.Path == "src/Svc.cs" && r.ReferenceKind == "attribute"); + + var mixedCaseExactResults = _reader.SearchReferences("MyAuditATTRIBUTE", lang: "csharp", exact: true); + Assert.Contains(mixedCaseExactResults, r => r.Path == "src/Svc.cs" && r.ReferenceKind == "attribute"); + } + [Fact] public void GetGroupedSymbolHotspots_CollapsesDuplicateNamesWithoutBareJoinInflation() { From c3e62ef6231747cf5ef100289bba13a82dee4dfb Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 18 Apr 2026 12:51:14 +0900 Subject: [PATCH 19/30] Fix #293 make multi-line C# attribute blanker attribute-position aware Adversarial review round 8 flagged that `StripMultiLineCSharpAttributeInterior` was too aggressive: it blanked ANY `[` without a matching `]` on the same line, including legitimate multi-line indexer declarations like: public int this[ int i ] => _items[i]; The first line got truncated to `public int this`, so the downstream indexer regex (which expects `this\s*\[...\]` on the same line) could not recognize it. The indexer then silently disappeared from `symbols` / `definition` / `outline` / `inspect` / `unused` / `hotspots`, and a different code path mis-extracted it as a `property` named `this`. Fix: when scanning for the opening `[`, look back past whitespace at the preceding character. Treat `[` as an attribute opener only when the preceding non-whitespace character is NOT a word character (letter / digit / underscore) and NOT `)` or `]`. `this[`, `arr[`, `(expr)[`, and `arr[i][` are now left untouched; attribute positions (`[Attr]`, `void M([Attr] int x)`, `class C<[Attr] T>`, lambda `[Attr] () =>`, and line-leading `[`) are still handled correctly. Added `SymbolExtractorTests.Extract_CSharp_DetectsMultiLineIndexer` to pin the regression. Verified end-to-end via the built binary that both the multi-line indexer and existing multi-line attribute cases produce the correct symbols. Co-Authored-By: Claude Opus 4 --- CHANGELOG.md | 4 +-- src/CodeIndex/Indexer/SymbolExtractor.cs | 31 +++++++++++++++++++ tests/CodeIndex.Tests/SymbolExtractorTests.cs | 28 +++++++++++++++++ 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbf3706d4d..8cf0948233 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **MCP `callers` / `callees` `kind` schema, CLI help, and README `--kind` option row no longer advertise metadata kinds as valid filter values (#293 follow-up)** — The `callers` / `callees` handlers reject `kind: "attribute"` / `kind: "annotation"` with a usage error (CLI) / `isError` tool response (MCP), but the MCP tool schema's `kind` property description still listed all five kinds as if they were valid filter values (`"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"`), `cdidx --help` similarly lumped `callers` / `callees` together with `references` for the `--kind` help row, and the README `--kind` option table (EN line 436 / JP line 1307) still directed readers to pass `--kind attribute` or `--kind annotation` on `callers` / `callees`. Schema and handler now agree: the `callers` / `callees` `kind` description now reads `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."`, the CLI `--kind` help row splits `references` (accepts all five kinds) from `callers` / `callees` (call-graph kinds only, metadata rejected, redirects to `references`), and the README `--kind` row (both English and Japanese) now explicitly states that `callers` / `callees` reject `--kind attribute|annotation` with a usage error and points to `references --kind attribute|annotation` as the correct metadata-enumeration path. The MCP `references` tool description is unchanged because it still accepts every indexed kind. Added `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` to pin the corrected schema wording. Affected: `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `src/CodeIndex/Cli/ConsoleUi.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `README.md`. - **`deps`, `impact`, `references`, `inspect`, and `analyze_symbol` retain metadata references as compile-time dependency edges and canonicalize the C# attribute-suffix convention (#293 follow-up)** — `DbReader.GetFileDependencies` (backing `cdidx deps`) and `DbReader.GetFileDependencyHintsToResolvedType` (backing `cdidx impact` / MCP `impact_analysis` heuristic file-hint fallback) keep metadata references in their logical reference CTE. Renaming or removing an attribute class still breaks annotated sites such as `[JsonConverterAttribute]` at compile time, so file-level blast-radius analysis must surface those edges as real dependencies — now labeled `attribute` / `annotation` in `logical_reference_kind` (previously these same sites leaked in as phantom `call` rows before classification was corrected). `GetFileDependencies` additionally canonicalizes the C# attribute naming convention: idiomatic `[Foo]` usage is stored with `symbol_name = "Foo"` but the defining class is `FooAttribute`, so the logical-reference CTE now UNION-ALLs a suffix-aliased copy of every C# `attribute`-kind row with `'Attribute'` appended (only when the original symbol_name does not already end in `Attribute`) so the target-file join reaches the attribute class file. `GetFileDependencyHintsToResolvedType` also aggregates a per-candidate `has_metadata_ref` flag and bypasses the `SourceFileHasStructuredTypeEvidence` guard for edges whose contributing references are purely metadata, so pure-attribute consumer files such as `[MyAudit] class Svc` now surface through `impact MyAuditAttribute` instead of being filtered out. `ResolveImpactFallbackNames` symmetrically adds suffix-stripped aliases for C# class names ending in `Attribute`, so the BFS-less file-hint path can find `[Foo]` sites whose source symbol_name does not match the defining class name. The same canonicalization is propagated to the symbol-level reference reader: `DbReader.SearchReferences`, `CountSearchReferences`, and `CountSearchReferencesTotal` (backing `references`, `inspect`, MCP `analyze_symbol`) now compute a C#-only `queryAttributeAlias` (defined in the new `ComputeCSharpAttributeSuffixAlias` helper) and OR it into both exact (folded and NOCASE) and substring WHERE clauses, so `references MyAuditAttribute` (and `inspect MyAuditAttribute` / `analyze_symbol MyAuditAttribute`) surface the idiomatic `[MyAudit]` call site consistently with `deps` / `impact`. Substring mode uses an exact-OR for the alias (not a second LIKE) so user queries like `MyAuditAttribute` do not over-match unrelated names such as `FooAuditLog`. The alias is C# only, so Java / Kotlin / Scala / TypeScript annotation sites remain unaffected. `callers` / `callees` intentionally skip the alias because they filter to the shared call-graph kinds (`call`, `instantiate`, `subscribe`) and reject `--kind attribute|annotation` at the CLI / MCP boundary — those commands model the dynamic call graph, not the dependency graph. The alias branch is additionally scoped so it cannot produce false positives: `ComputeCSharpAttributeSuffixAlias` returns `null` when `referenceKind` is a non-`attribute` call-graph kind (so `references FooAttribute --kind call` stays strict and does not match `Foo()`), and the SQL alias disjunct in all three reader methods is clamped to `f.lang = 'csharp' AND r.reference_kind = 'attribute'` so an unscoped `references FooAttribute` (no `--lang` / no `--kind`) still cannot bleed into Java annotations or C# call rows. Suffix detection is also case-insensitive (`OrdinalIgnoreCase`) so `references myauditattribute` and `inspect MyAuditATTRIBUTE` still produce the `MyAudit` alias, consistent with the NOCASE / folded contract of the surrounding exact / substring query paths. Added regression coverage `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies` (runtime + metadata edges both surface), `DbReaderTests.GetFileDependencies_MatchesCSharpAttributeSuffixConvention` (`[MyAudit]` → `MyAuditAttribute` canonicalization through `deps`), `DbReaderTests.SearchReferences_MatchesCSharpAttributeSuffixConvention_Substring` / `_Exact` / `_CSharpAttributeSuffixAliasDoesNotBleedToOtherLanguages` (same canonicalization through `references` / `inspect` / `analyze_symbol`, plus C#-only scope guard), `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_NotAppliedToCallKind` (alias must not fire under `--kind call`), `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_UnscopedLangStillLimitsToCSharpAttributeRows` (unscoped query must not match Java annotations or C# call rows), and `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_CaseInsensitiveQuery` (lowercase / mixed-case queries still produce the alias). Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. - **`callers` / `callees` now reject `--kind attribute|annotation` at the CLI and MCP boundary, and Swift / Gradle `@` metadata is reclassified as `annotation` (#293 follow-up)** — Metadata rows (`attribute` / `annotation`) are attributed to their enclosing body-range symbol rather than the annotated target, so `callers Obsolete --kind attribute` would silently return `[Obsolete] void M()` under the enclosing class (not `M`), and file-level targets such as `[assembly: CLSCompliant]` or Kotlin `@file:JvmName` would drop entirely because `container_name` is `NULL`. `QueryCommandRunner.RunCallers` / `RunCallees` now reject `--kind attribute` and `--kind annotation` with `CommandExitCodes.UsageError`, and the MCP `callers` / `callees` tools return an `isError` tool response; both paths direct users to `references --kind attribute|annotation`, which IS a correct metadata enumeration path. Separately, `AnnotationLanguages` now includes `swift` and `gradle`, so Swift `@available(iOS 13.0, *)` / `@objc` / `@MainActor` and Gradle/Groovy `@CompileStatic` / `@TaskAction` are recorded as `annotation` instead of `call` (previously the `@Name(args)` forms leaked into `callers` / `callees` / `hotspots` / `impact` as phantom call edges, and the no-arg `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` forms dropped from the index entirely because no-arg annotation emission was gated to languages in this set). Added `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError` (theory, 4 cases), `McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError` (theory, 4 cases), `ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`, and `ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation`. Affected: `src/CodeIndex/Cli/QueryCommandRunner.cs`, `src/CodeIndex/Mcp/McpToolHandlers.cs`, `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. -- **Multi-line no-arg C# attribute sections like `[\n Serializable\n]` are now indexed without over-matching attribute argument identifiers (#293 follow-up)** — The no-arg C# attribute regex previously required the match to start at a `[` / `,` boundary and end at `]` / `,` on the same line, so a bare identifier on the interior line of a multi-line attribute section (e.g. ` Serializable` between a line-leading `[` and a line-leading `]`) was silently dropped from `references --kind attribute`. The left anchor is now a word-boundary lookbehind and the right anchor also accepts end-of-line. To avoid misclassifying enum / qualified-constant identifiers that happen to sit at end-of-line inside an attribute argument list (e.g. `ConverterStrategy.AllowNumbers` in `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]`) as no-arg attributes, `BuildCSharpAttributeRanges` now also emits a parallel "top-level zone" table that splits each attribute section by `(` / `)` so only paren-depth-0 positions inside the section qualify as no-arg attribute name positions. Paren depth is tracked section-locally (each `[` snapshots the global depth at open time) so parameter attributes like `void M([FromServices] IService s)`, delegate parameter attributes like `delegate void D([Attr] int x)`, and lambda parameter attributes like `Func f = ([Attr] int x) => x` — whose `[` opens while the global paren depth is already > 0 — still have their attribute-list top level correctly identified. The no-arg attribute path uses that top-level table instead of the full attribute-section ranges, while the existing with-args metadata classification still uses the full ranges. `SymbolExtractor` also now tracks multi-line `[...]` depth across lines via `StripMultiLineCSharpAttributeInterior` in `BuildCSharpMatchLines`, so interior lines of a multi-line attribute section are blanked out before declaration regexes run — this prevents ` Serializable` from being mis-extracted as a top-level `function` symbol (which then masked the reference through the `definitionNames` guard). The multi-line blanker now activates on any `[` that opens without a matching `]` on the same line, not just `[` that sits at the start of the line, so multi-line parameter attributes like `void M([\n FromServices\n] IService s)`, type-parameter attributes like `class Bar<[\n TypeParamAttr\n] T>`, and delegate/lambda parameter attributes like `delegate void D([\n DelegateParamAttr\n] int x)` are also blanked out correctly — previously only the leading-`[` case was covered, so the interior identifiers of those forms were mis-extracted as `function` symbols and their attribute references were silently dropped from `references --kind attribute`. Regression coverage adds `[\n Serializable\n]`, `[\n global::System.Obsolete\n]`, `[Required,\n Key]`, `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]` (pinning that `AllowNumbers` / `ConverterStrategy` are NOT classified as `attribute`), and the non-leading-`[` parameter / type-parameter / delegate-parameter multi-line cases above, running through `SymbolExtractor.Extract` + `ReferenceExtractor.Extract` end-to-end so both sides of the regression stay pinned. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Indexer/SymbolExtractor.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. +- **Multi-line no-arg C# attribute sections like `[\n Serializable\n]` are now indexed without over-matching attribute argument identifiers (#293 follow-up)** — The no-arg C# attribute regex previously required the match to start at a `[` / `,` boundary and end at `]` / `,` on the same line, so a bare identifier on the interior line of a multi-line attribute section (e.g. ` Serializable` between a line-leading `[` and a line-leading `]`) was silently dropped from `references --kind attribute`. The left anchor is now a word-boundary lookbehind and the right anchor also accepts end-of-line. To avoid misclassifying enum / qualified-constant identifiers that happen to sit at end-of-line inside an attribute argument list (e.g. `ConverterStrategy.AllowNumbers` in `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]`) as no-arg attributes, `BuildCSharpAttributeRanges` now also emits a parallel "top-level zone" table that splits each attribute section by `(` / `)` so only paren-depth-0 positions inside the section qualify as no-arg attribute name positions. Paren depth is tracked section-locally (each `[` snapshots the global depth at open time) so parameter attributes like `void M([FromServices] IService s)`, delegate parameter attributes like `delegate void D([Attr] int x)`, and lambda parameter attributes like `Func f = ([Attr] int x) => x` — whose `[` opens while the global paren depth is already > 0 — still have their attribute-list top level correctly identified. The no-arg attribute path uses that top-level table instead of the full attribute-section ranges, while the existing with-args metadata classification still uses the full ranges. `SymbolExtractor` also now tracks multi-line `[...]` depth across lines via `StripMultiLineCSharpAttributeInterior` in `BuildCSharpMatchLines`, so interior lines of a multi-line attribute section are blanked out before declaration regexes run — this prevents ` Serializable` from being mis-extracted as a top-level `function` symbol (which then masked the reference through the `definitionNames` guard). The multi-line blanker now activates on any `[` that opens without a matching `]` on the same line, not just `[` that sits at the start of the line, so multi-line parameter attributes like `void M([\n FromServices\n] IService s)`, type-parameter attributes like `class Bar<[\n TypeParamAttr\n] T>`, and delegate/lambda parameter attributes like `delegate void D([\n DelegateParamAttr\n] int x)` are also blanked out correctly — previously only the leading-`[` case was covered, so the interior identifiers of those forms were mis-extracted as `function` symbols and their attribute references were silently dropped from `references --kind attribute`. The blanker is now attribute-position aware as well: it only treats `[` as an attribute opener when the immediately preceding non-whitespace character is not a word character (`[_A-Za-z0-9]`) and not `)` / `]`, so multi-line indexer declarations like `public int this[\n int i\n] => _items[i];` (where `[` follows the `this` keyword) are correctly left as-is and the indexer still surfaces as a `function` with the canonical `Item` name in `symbols` / `definition` / `outline` / `inspect` / `unused` / `hotspots`. Regression coverage adds `[\n Serializable\n]`, `[\n global::System.Obsolete\n]`, `[Required,\n Key]`, `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]` (pinning that `AllowNumbers` / `ConverterStrategy` are NOT classified as `attribute`), the non-leading-`[` parameter / type-parameter / delegate-parameter multi-line cases above, and the multi-line indexer declaration (`SymbolExtractorTests.Extract_CSharp_DetectsMultiLineIndexer`), running through `SymbolExtractor.Extract` + `ReferenceExtractor.Extract` end-to-end so both sides of the regression stay pinned. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Indexer/SymbolExtractor.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`, `tests/CodeIndex.Tests/SymbolExtractorTests.cs`. - **`::`-qualified C# no-arg attributes and JavaScript `@Decorator` usages are now classified correctly (#293 follow-up)** — The no-arg C# attribute regex now accepts `::` alongside `.` as a qualifier separator, so `[global::System.Obsolete]` and `[Alias::MyAttr]` reach `references --kind attribute` instead of being silently dropped. `AnnotationLanguages` additionally includes `javascript`, so JS decorators such as `@sealed` (no-arg) and `@injectable()` (with args) are reclassified to `annotation` rather than leaking into the call-graph as phantom `call` rows — matching the existing TypeScript contract since JavaScript is already a graph-supported language. Added regression tests pinning `[global::System.Obsolete]`, `[Alias::MyAttr]`, and JavaScript `@sealed` / `@injectable()`. Also rewrote the README `callers` row (EN + JP) and the MCP tool descriptions for `callers` / `callees` to pull back the earlier over-promise that `kind: "attribute"` / `kind: "annotation"` on `callers` / `callees` is a supported metadata-inspection path: a metadata row is attributed to its enclosing body-range symbol (for a class-level declaration, that is the class itself) and drops entirely for file-level targets such as `[assembly: ...]` where `container_name` is `null`, so the docs now direct users to `references --kind attribute|annotation` for metadata enumeration. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`, `README.md`. - **`impact` BFS now follows C# event subscribe edges (#293 follow-up)** — `DbReader.GetCallersExact`, the only caller site used by the `impact` / `impact_analysis` BFS, now filters references through the shared `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` set instead of `InvokeReferenceKindsSql = ('call', 'instantiate')`. Previously `impact` quietly dropped `Changed += OnChanged` subscribe rows while `callers` / `callees` / `hotspots` kept them, so an event-driven transitive caller chain appeared shorter under `impact` than under `callers`. A new `DbReaderTests.GetTransitiveCallers_FollowsSubscribeEdges` regression pins the subscribe edge into the BFS output. Metadata edges (`attribute`, `annotation`) remain excluded. Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. - **C# parameter, type-parameter, lambda, and multi-line `[...]` attribute lists are now classified as `attribute`, no-arg attributes and annotations are indexed, and MCP tool descriptions document the default metadata filter (#293 follow-up)** — `ReferenceExtractor` now runs a pre-pass that tokenizes every C# `[...]` section across the whole file, so parameter attributes such as `void M([FromServices] IService s)` (where `[` is preceded by `(` or `,` rather than a declaration boundary), type-parameter attributes such as `class C<[Attr("x")] T>` (preceded by `<`), tuple-typed parameter attributes such as `void M([Attr("x")] (int a, int b) value)` (where the next token after `]` is `(`), lambda attributes such as `var f = [Attr("x")] () => 0;` (where `[` is preceded by `=`), and multi-line forms such as `[\n Foo("x")\n]` or `public void M(\n [Attr("x")]\n int a)` are recognized alongside the existing same-line cases. The pre-pass disambiguates the `(` / `,` / `<` / `=` entry point against C# 12 collection expressions in argument position (`Consume([Make()])`), pattern expressions (`[Make()] is int[] xs`), `as` casts (`[Make()] as int[]`), and switch expressions (`[Make()] switch { _ => true }`) by scanning forward from `[` to the matching `]` and checking whether the next meaningful token begins a declaration — identifiers are accepted unless they are the expression-continuation keywords `is` / `as` / `switch` / `with` / `when`, `@` is accepted as a verbatim-identifier lead, `(` is accepted for tuple-typed parameters and lambda parameter lists, and chained `[A][B]` recurses through the inner bracket. Bare no-arg C# attributes such as `[Serializable]`, `[Obsolete]`, `[System.Obsolete]`, `[assembly: CLSCompliant]`, and `[Required, Key]`, plus bare no-arg Java-family annotations such as `@Deprecated`, `@Override`, `@org.junit.Test`, and `@field:Deprecated`, are now emitted through dedicated no-arg regexes since the original `CallRegex` requires `(` — the C# path still gates on the attribute-range pre-pass so indexer access such as `arr[i]` stays unclassified, and the annotation path uses a lookbehind `(?`, tuple-typed parameter `void M([Attr("x")] (int a, int b) value)`, lambda `var f = [Attr("x")] () => 0;`, the no-arg C# attribute set above, the no-arg Java `@Deprecated` / `@Override` / `@org.junit.Test` set, Kotlin `@field:Deprecated` plus the `return@foo` negative, indexer access staying unclassified, `[\n Foo("x")\n]`, the cross-line parameter attribute shape, the targeted `[return: ...]` shape, the defense-in-depth collection-expression-in-argument case, and the three expression-continuation cases `[Make()] is ...`, `[Make()] as int[]`, and `[Make()] switch { ... }`. Affected: `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. @@ -688,7 +688,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **MCP の `callers` / `callees` の `kind` schema・CLI help・README の `--kind` オプション行が metadata kinds を有効フィルタ値として広告しないよう修正 (#293 追加対応)** — `callers` / `callees` の handler は `kind: "attribute"` / `kind: "annotation"` を usage error(CLI)/ `isError` ツールレスポンス(MCP)で拒否するにもかかわらず、MCP ツール schema の `kind` プロパティ説明は 5 種すべてを列挙した `"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"` のままで、`cdidx --help` の `--kind` 行も `callers` / `callees` を `references` と同じ行にまとめていた。さらに README の `--kind` オプション表(英語 436 行目 / 日本語 1307 行目)も、`callers` / `callees` で `--kind attribute` / `--kind annotation` を渡せるかのように案内していた。schema と handler の契約を一致させ、`callers` / `callees` の `kind` description を `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."` に修正し、CLI の `--kind` help 行も `references`(5 種すべて受理)と `callers` / `callees`(call-graph 種別のみ、metadata kinds は拒否して `references` へ誘導)を分けるようにし、README の `--kind` 行(英語・日本語の両方)も `callers` / `callees` が `--kind attribute|annotation` を usage error で拒否すること、metadata 列挙の正規経路が `references --kind attribute|annotation` であることを明示する文言に直した。MCP の `references` ツール説明は全 reference kind を受け付ける契約のままなので変更なし。修正後の schema 文言を固定する `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` を追加。対象: `src/CodeIndex/Mcp/McpToolDefinitions.cs`、`src/CodeIndex/Cli/ConsoleUi.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`、`README.md`。 - **`deps`・`impact`・`references`・`inspect`・`analyze_symbol` が metadata 参照を compile-time 依存として保持し、C# の attribute サフィックス命名規約を正規化 (#293 追加対応)** — `cdidx deps` の実装である `DbReader.GetFileDependencies` と、`cdidx impact` / MCP `impact_analysis` の heuristic file-hint フォールバックである `DbReader.GetFileDependencyHintsToResolvedType` は、logical reference CTE に metadata 参照も含める。attribute クラスを rename / 削除すれば `[JsonConverterAttribute]` のような注釈サイトも compile-time で壊れるため、ファイル単位の blast-radius 分析はそれらも本物の依存 edge として表示する必要があり、`logical_reference_kind` では `attribute` / `annotation` として正しくラベル付けされる(分類修正前は同じサイトが phantom `call` として漏れていた)。さらに `GetFileDependencies` は C# の attribute 命名規約を正規化する: 慣用的な `[Foo]` は `symbol_name = "Foo"` で保存されるが、定義クラスは `FooAttribute` 命名となるため、logical-reference CTE に C# `attribute` 種別の行をサフィックス `'Attribute'` を付与した別名コピーとして UNION ALL で追加する(元の symbol_name が既に `Attribute` で終わっている場合は重複しないようスキップ)。これにより target ファイル join が attribute クラス側のファイルへ到達する。`GetFileDependencyHintsToResolvedType` はさらに candidate ごとに `has_metadata_ref` フラグを集約し、寄与する参照が metadata だけである edge については `SourceFileHasStructuredTypeEvidence` guard をバイパスするため、`[MyAudit] class Svc` のような純 attribute consumer ファイルも `impact MyAuditAttribute` で出るようになった(以前は filter で落ちていた)。`ResolveImpactFallbackNames` も対称的に、`Attribute` で終わる C# クラス名にはサフィックスを外した別名を追加するため、BFS を使わない file-hint 経路でも source symbol_name が定義クラス名と一致しない `[Foo]` サイトを発見できる。同じ正規化をシンボル単位の参照 reader にも伝播する: `references`・`inspect`・MCP `analyze_symbol` を支える `DbReader.SearchReferences`・`CountSearchReferences`・`CountSearchReferencesTotal` は、新しいヘルパー `ComputeCSharpAttributeSuffixAlias` で C# 限定の `queryAttributeAlias` を計算し、exact(folded と NOCASE)・substring の両方の WHERE 句に OR で追加する。これにより `references MyAuditAttribute`(および `inspect MyAuditAttribute` / `analyze_symbol MyAuditAttribute`)でも慣用的な `[MyAudit]` 参照サイトが `deps` / `impact` と一貫して surface する。substring モードでは alias を二つ目の LIKE にせず exact-OR で束ねているため、ユーザーが `MyAuditAttribute` と打った時に `FooAuditLog` のような無関係な名前に over-match しない。alias は C# 限定なので、Java / Kotlin / Scala / TypeScript の annotation サイトには影響しない。`callers` / `callees` は共有の call-graph 種別 (`call` / `instantiate` / `subscribe`) に絞って `--kind attribute|annotation` を CLI / MCP 境界で拒否する契約なので、alias は意図的に適用しない(dynamic call graph と dependency graph は別契約)。さらに alias 節が誤一致を生まないようスコープする: `ComputeCSharpAttributeSuffixAlias` は `referenceKind` が `attribute` 以外の call-graph 種別のときは `null` を返す(`references FooAttribute --kind call` は厳格一致のままで `Foo()` に一致しない)、3 つの reader メソッドの SQL alias 節は `f.lang = 'csharp' AND r.reference_kind = 'attribute'` に限定するため、`--lang` / `--kind` 無指定の `references FooAttribute` でも Java の annotation や C# の call 行を拾わない。suffix 検出も `OrdinalIgnoreCase` にして case-insensitive 化したため、`references myauditattribute` / `inspect MyAuditATTRIBUTE` のような小文字・混在ケースでも alias `MyAudit` を生成でき、周辺の NOCASE / folded 契約と整合する。回帰テスト `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies`(runtime と metadata の両方の edge が出ることを固定)、`DbReaderTests.GetFileDependencies_MatchesCSharpAttributeSuffixConvention`(`deps` 経由での `[MyAudit]` → `MyAuditAttribute` 正規化)、`DbReaderTests.SearchReferences_MatchesCSharpAttributeSuffixConvention_Substring` / `_Exact` / `_CSharpAttributeSuffixAliasDoesNotBleedToOtherLanguages`(`references` / `inspect` / `analyze_symbol` での同一正規化と C# 限定スコープ)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_NotAppliedToCallKind`(`--kind call` で alias が発火しないこと)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_UnscopedLangStillLimitsToCSharpAttributeRows`(無指定でも Java annotation や C# call 行に一致しないこと)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_CaseInsensitiveQuery`(小文字・混在ケースのクエリでも alias を生成)を追加。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 - **`callers` / `callees` が CLI と MCP 境界で `--kind attribute|annotation` を拒否し、Swift / Gradle の `@` metadata を `annotation` に再分類 (#293 追加対応)** — metadata 行 (`attribute` / `annotation`) は注釈対象そのものではなく body-range の外側シンボルに帰属するため、`callers Obsolete --kind attribute` は `[Obsolete] void M()` を `M` ではなく外側クラスの下に黙って返し、`[assembly: CLSCompliant]` や Kotlin の `@file:JvmName` のような file-level target は `container_name = NULL` で完全脱落していた。`QueryCommandRunner.RunCallers` / `RunCallees` は `--kind attribute` / `--kind annotation` を `CommandExitCodes.UsageError` で弾き、MCP の `callers` / `callees` ツールは `isError` ツールレスポンスを返すようにした。どちらの経路も、metadata 列挙の正しいパスである `references --kind attribute|annotation` へ誘導する。あわせて `AnnotationLanguages` に `swift` と `gradle` を追加し、Swift の `@available(iOS 13.0, *)` / `@objc` / `@MainActor` や Gradle / Groovy の `@CompileStatic` / `@TaskAction` を `call` ではなく `annotation` として記録するようにした(修正前は引数付きの `@Name(args)` 形は `callers` / `callees` / `hotspots` / `impact` に phantom call edge として漏れ、引数なしの `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` 形はこの集合で gate されていた no-arg annotation 出力から外れてインデックス自体に残らなかった)。回帰テストとして `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError`(theory 4 ケース)、`McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError`(theory 4 ケース)、`ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`、`ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation` を追加。対象: `src/CodeIndex/Cli/QueryCommandRunner.cs`、`src/CodeIndex/Mcp/McpToolHandlers.cs`、`src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 -- **`[\n Serializable\n]` のような複数行にまたがる引数なし C# 属性セクションを、属性引数内の識別子を誤って取り込まずに索引化 (#293 追加対応)** — 引数なし C# 属性の正規表現は、これまで `[` / `,` を左端、`]` / `,` を右端として同一行内でアンカーしていたため、行頭 `[` と行頭 `]` の間に書かれた ` Serializable` のような裸識別子が `references --kind attribute` から黙って脱落していた。左側は単語境界 lookbehind、右側は行末(`$`)も許容するように緩和した。一方で `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]` のように属性の引数リスト内で行末に現れる enum / 修飾定数識別子(`AllowNumbers` など)が no-arg 属性として誤分類される回帰を避けるため、`BuildCSharpAttributeRanges` は `(` / `)` で属性セクションを分割した paren 深さ 0 の「top-level ゾーン」テーブルも並列で出力するようにし、no-arg 属性の採用位置はそのテーブルに限定した(既存の引数付き metadata 分類パスは引き続き属性セクション全体のレンジを使う)。paren 深さは section-local に追跡する(各 `[` がその瞬間のグローバル paren 深さを snapshot として保持)ため、`void M([FromServices] IService s)` のようなパラメータ属性、`delegate void D([Attr] int x)` のようなデリゲートパラメータ属性、`Func f = ([Attr] int x) => x` のようなラムダパラメータ属性のように、`[` が開いた時点で既にグローバル paren 深さが 0 より大きいケースでも、属性リストの top-level を正しく認識できる。`SymbolExtractor` 側でも `BuildCSharpMatchLines` に `StripMultiLineCSharpAttributeInterior` を追加し、複数行にまたがる `[...]` セクションの深さを跨行で追跡して内部行を宣言 regex 適用前に空白化するようにした。これにより ` Serializable` がトップレベル `function` シンボルとして誤抽出され、`definitionNames` ガード経由で参照分類が詰まってしまうのを防ぐ。さらに、複数行の空白化起動条件を「同一行内で閉じない `[` があるかどうか」に広げ、行頭の `[` だけでなく `void M([\n FromServices\n] IService s)` のようなパラメータ属性、`class Bar<[\n TypeParamAttr\n] T>` のような型パラメータ属性、`delegate void D([\n DelegateParamAttr\n] int x)` のようなデリゲート / ラムダのパラメータ属性でも内部行が正しく空白化されるようにした — 以前は開口行で `[` が行頭にある場合しかカバーされていなかったため、上記の形の内部識別子が `function` シンボルとして誤抽出され、`references --kind attribute` から属性参照が黙って脱落していた。`[\n Serializable\n]`・`[\n global::System.Obsolete\n]`・`[Required,\n Key]`・`[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]`(`AllowNumbers` / `ConverterStrategy` が `attribute` として取り込まれないことを固定)に加え、上記の行頭でない `[` で開く複数行パラメータ / 型パラメータ / デリゲートパラメータ属性のケースも `SymbolExtractor.Extract` + `ReferenceExtractor.Extract` で end-to-end に回して SymbolExtractor 側の回帰も固定する回帰テストを追加。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Indexer/SymbolExtractor.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 +- **`[\n Serializable\n]` のような複数行にまたがる引数なし C# 属性セクションを、属性引数内の識別子を誤って取り込まずに索引化 (#293 追加対応)** — 引数なし C# 属性の正規表現は、これまで `[` / `,` を左端、`]` / `,` を右端として同一行内でアンカーしていたため、行頭 `[` と行頭 `]` の間に書かれた ` Serializable` のような裸識別子が `references --kind attribute` から黙って脱落していた。左側は単語境界 lookbehind、右側は行末(`$`)も許容するように緩和した。一方で `[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]` のように属性の引数リスト内で行末に現れる enum / 修飾定数識別子(`AllowNumbers` など)が no-arg 属性として誤分類される回帰を避けるため、`BuildCSharpAttributeRanges` は `(` / `)` で属性セクションを分割した paren 深さ 0 の「top-level ゾーン」テーブルも並列で出力するようにし、no-arg 属性の採用位置はそのテーブルに限定した(既存の引数付き metadata 分類パスは引き続き属性セクション全体のレンジを使う)。paren 深さは section-local に追跡する(各 `[` がその瞬間のグローバル paren 深さを snapshot として保持)ため、`void M([FromServices] IService s)` のようなパラメータ属性、`delegate void D([Attr] int x)` のようなデリゲートパラメータ属性、`Func f = ([Attr] int x) => x` のようなラムダパラメータ属性のように、`[` が開いた時点で既にグローバル paren 深さが 0 より大きいケースでも、属性リストの top-level を正しく認識できる。`SymbolExtractor` 側でも `BuildCSharpMatchLines` に `StripMultiLineCSharpAttributeInterior` を追加し、複数行にまたがる `[...]` セクションの深さを跨行で追跡して内部行を宣言 regex 適用前に空白化するようにした。これにより ` Serializable` がトップレベル `function` シンボルとして誤抽出され、`definitionNames` ガード経由で参照分類が詰まってしまうのを防ぐ。さらに、複数行の空白化起動条件を「同一行内で閉じない `[` があるかどうか」に広げ、行頭の `[` だけでなく `void M([\n FromServices\n] IService s)` のようなパラメータ属性、`class Bar<[\n TypeParamAttr\n] T>` のような型パラメータ属性、`delegate void D([\n DelegateParamAttr\n] int x)` のようなデリゲート / ラムダのパラメータ属性でも内部行が正しく空白化されるようにした — 以前は開口行で `[` が行頭にある場合しかカバーされていなかったため、上記の形の内部識別子が `function` シンボルとして誤抽出され、`references --kind attribute` から属性参照が黙って脱落していた。同時に空白化ロジックを「属性位置」判定付きに変更し、直前の非空白文字が語文字(`[_A-Za-z0-9]`)や `)` / `]` の場合は属性開口と扱わないようにした。これにより `public int this[\n int i\n] => _items[i];` のような複数行インデクサ宣言(`[` 直前が `this` キーワード)が誤って空白化されなくなり、インデクサが `symbols` / `definition` / `outline` / `inspect` / `unused` / `hotspots` で `function`(正準名 `Item`)として正しく現れる。`[\n Serializable\n]`・`[\n global::System.Obsolete\n]`・`[Required,\n Key]`・`[\n JsonConverter(\n ConverterStrategy.AllowNumbers\n )\n]`(`AllowNumbers` / `ConverterStrategy` が `attribute` として取り込まれないことを固定)に加え、上記の行頭でない `[` で開く複数行パラメータ / 型パラメータ / デリゲートパラメータ属性のケースと複数行インデクサ宣言 (`SymbolExtractorTests.Extract_CSharp_DetectsMultiLineIndexer`) も `SymbolExtractor.Extract` + `ReferenceExtractor.Extract` で end-to-end に回して SymbolExtractor 側の回帰も固定する回帰テストを追加。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Indexer/SymbolExtractor.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`、`tests/CodeIndex.Tests/SymbolExtractorTests.cs`。 - **`::` 修飾の C# 引数なし属性と JavaScript の `@Decorator` を正しく分類 (#293 追加対応)** — 引数なし C# 属性の正規表現が修飾子セパレータとして `.` に加えて `::` も受け付けるようになり、`[global::System.Obsolete]` や `[Alias::MyAttr]` のような形が `references --kind attribute` から黙って脱落しなくなった。`AnnotationLanguages` にも `javascript` を追加し、JavaScript はもともと graph 対応言語なので、`@sealed`(引数なし)や `@injectable()`(引数付き)のような JS decorator も `annotation` として再分類され、TypeScript と同じ契約で call-graph に phantom `call` 行を残さない。`[global::System.Obsolete]`・`[Alias::MyAttr]`・JavaScript の `@sealed` / `@injectable()` を押さえる回帰テストを追加。あわせて、以前に進めていた `callers` / `callees` の metadata 契約を README(英日)と MCP ツール説明の両方で引き戻し、metadata 行は注釈対象そのものではなく body-range 上の外側シンボル(クラス直下宣言ならクラス、`[assembly: ...]` のようなファイルレベル target では `container_name` が `null`)に帰属するため `kind: "attribute"` / `kind: "annotation"` を `callers` / `callees` に渡すのは信頼できる metadata 調査経路ではなく、metadata 列挙は `references --kind attribute|annotation` を使うよう案内する文言に修正した。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Mcp/McpToolDefinitions.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`、`README.md`。 - **`impact` BFS が C# の event subscribe edge を辿るよう修正 (#293 追加対応)** — `impact` / `impact_analysis` BFS の唯一の呼び出し箇所である `DbReader.GetCallersExact` が、共有 filter `CallGraphReferenceKindsSql = ('call', 'instantiate', 'subscribe')` で参照を絞り込むようになった(以前は `InvokeReferenceKindsSql = ('call', 'instantiate')` を使っていたため、`Changed += OnChanged` のような subscribe 行を静かに捨てていた)。これにより `callers` / `callees` / `hotspots` と同じ call-graph 契約を `impact` も共有し、イベント駆動の transitive caller chain が `impact` でだけ短くなる不整合が解消する。`DbReaderTests.GetTransitiveCallers_FollowsSubscribeEdges` の回帰テストで subscribe edge が BFS 出力に含まれることを固定。Metadata edge (`attribute` / `annotation`) の除外は従来通り。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 - **C# のパラメータ属性・型パラメータ属性・ラムダ属性・複数行 `[...]` 属性を `attribute` として分類し、引数なし属性とアノテーションも索引化、MCP ツール説明に既定の metadata フィルタを反映 (#293 追加対応)** — `ReferenceExtractor` がファイル全体の C# `[...]` セクションを事前パスでトークン化するようになり、`void M([FromServices] IService s)` のように `[` の直前が宣言境界ではなく `(` / `,` で始まるパラメータ属性、`class C<[Attr("x")] T>` のように `<` で始まる型パラメータ属性、`void M([Attr("x")] (int a, int b) value)` のように `]` の直後が `(` のまま続く tuple 型パラメータ属性、`var f = [Attr("x")] () => 0;` のように `=` の直後にあるラムダ属性、さらに `[\n Foo("x")\n]` や `public void M(\n [Attr("x")]\n int a)` のような複数行形式も、既存の同一行ケースと同じように属性として扱われるようになった。`(` / `,` / `<` / `=` 直後の曖昧さは、`[` から対応する `]` まで前方走査し、`]` の直後の非空白トークンが宣言を開始するかで判定する — 識別子は原則受理するが、式継続キーワード `is` / `as` / `switch` / `with` / `when` は宣言扱いしないので `Consume([Make()] is int[] xs)` / `([Make()] as int[])` / `Consume([Make()] switch { _ => true })` のような collection expression は `call` のまま残り、`@` は verbatim 識別子の先頭として、`(` は tuple 型パラメータとラムダ仮引数リストの先頭として受理し、`[A][B]` のような連続 bracket は内側 `[` に再帰して判定する。さらに `[Serializable]`、`[Obsolete]`、`[System.Obsolete]`、`[assembly: CLSCompliant]`、`[Required, Key]` のような引数なし C# 属性と、`@Deprecated`・`@Override`・`@org.junit.Test`・`@field:Deprecated` のような引数なし Java 系アノテーションは、`(` を要求する既存の `CallRegex` では拾えないため専用の no-arg 正規表現から emit するようになった。C# 側では引き続き `[...]` 事前パスのレンジに含まれるかで gate するので `arr[i]` のような indexer アクセスは属性化されず、アノテーション側は lookbehind `(?` 型パラメータ属性、`void M([Attr("x")] (int a, int b) value)` tuple 型パラメータ属性、`var f = [Attr("x")] () => 0;` ラムダ属性、上記 no-arg C# 属性群、Java `@Deprecated` / `@Override` / `@org.junit.Test` の no-arg アノテーション群、Kotlin `@field:Deprecated` と `return@foo` の negative ケース、indexer アクセスが属性化されないこと、`[\n Foo("x")\n]`、改行を挟んだパラメータ属性、`[return: NotNullWhen(true)]`、防御的に `Consume([Make()])` の collection expression-in-argument、および `[Make()] is ...` / `[Make()] as int[]` / `[Make()] switch { ... }` の 3 つの式継続ケースを押さえる回帰テストを追加。対象: `src/CodeIndex/Indexer/ReferenceExtractor.cs`、`src/CodeIndex/Mcp/McpToolDefinitions.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 diff --git a/src/CodeIndex/Indexer/SymbolExtractor.cs b/src/CodeIndex/Indexer/SymbolExtractor.cs index ab1652734f..f7e777df17 100644 --- a/src/CodeIndex/Indexer/SymbolExtractor.cs +++ b/src/CodeIndex/Indexer/SymbolExtractor.cs @@ -5388,10 +5388,22 @@ private static string StripMultiLineCSharpAttributeInterior(string line, ref int // `class C<`, etc.) and must be preserved so downstream declaration regexes can // still recognize the surrounding construct. Everything from the unclosed `[` // onward is blanked, and subsequent lines are blanked until the matching `]`. + // Only attribute-position `[` should trigger blanking — a multi-line indexer + // declaration such as `public int this[\n int i\n] => _items[i];` opens `[` + // immediately after the identifier `this`, which is NOT an attribute and must + // not be stripped (otherwise the indexer regex sees only `public int this` and + // the indexer silently disappears from symbols / definition / outline). Treat + // `[` as an attribute opener only when the immediately preceding non-whitespace + // character is not a word character (`[_A-Za-z0-9]`) and not `)` / `]` (which + // indicate indexer / array access on an expression result or chained indexer). // 行内を走査し、同一行で閉じない `[` を探す。その `[` より前は通常のコード // (`void M(` のようなメソッドヘッダ、`class C<` のようなジェネリック開口など) // であり、下流の宣言 regex が外側の構文を認識できるように残す必要がある。 // 閉じない `[` 以降は空白化し、対応する `]` が現れるまで後続行も空白化する。 + // `[` が属性位置にあるときだけ空白化する — `public int this[\n int i\n]` + // のような複数行インデクサ宣言では `this` 直後の `[` が属性でないため、 + // ここを削ってしまうとインデクサがシンボルから消える。直前の非空白文字が + // 語文字(`[_A-Za-z0-9]`)でも `)` / `]` でもない場合にのみ属性開口と判定する。 int openIndex = -1; int localDepth = 0; for (int i = 0; i < line.Length; i++) @@ -5399,7 +5411,26 @@ private static string StripMultiLineCSharpAttributeInterior(string line, ref int if (line[i] == '[') { if (localDepth == 0) + { + // Look back past whitespace for the character that introduces the `[`. + // 先行する非空白文字を探して `[` の導入子を判定する。 + int p = i - 1; + while (p >= 0 && (line[p] == ' ' || line[p] == '\t')) + p--; + if (p >= 0) + { + char prev = line[p]; + if (prev == '_' || (prev >= 'A' && prev <= 'Z') || (prev >= 'a' && prev <= 'z') || (prev >= '0' && prev <= '9') || prev == ')' || prev == ']') + { + // Not an attribute opener (e.g. `this[`, `arr[`, `(expr)[`, `arr[i][`). + // Treat this `[` as opaque — do not track depth, do not blank. + // 属性開口ではない(`this[`・`arr[`・`(expr)[`・`arr[i][` など)。 + // この `[` は追跡も空白化もしない。 + continue; + } + } openIndex = i; + } localDepth++; } else if (line[i] == ']') diff --git a/tests/CodeIndex.Tests/SymbolExtractorTests.cs b/tests/CodeIndex.Tests/SymbolExtractorTests.cs index f129825983..1e2a5080f6 100644 --- a/tests/CodeIndex.Tests/SymbolExtractorTests.cs +++ b/tests/CodeIndex.Tests/SymbolExtractorTests.cs @@ -4094,6 +4094,34 @@ public void Extract_CSharp_DetectsIndexer() Assert.Equal("string", indexer.ReturnType); } + [Fact] + public void Extract_CSharp_DetectsMultiLineIndexer() + { + // #293 follow-up: `StripMultiLineCSharpAttributeInterior` must only blank + // attribute-position `[`. `public int this[\n int i\n] => _items[i];` opens `[` + // right after the `this` keyword, which is an indexer parameter list, not + // an attribute. If that `[` were blanked, the indexer would silently drop + // out of symbol extraction. + // #293 追加対応: `StripMultiLineCSharpAttributeInterior` は属性位置の `[` だけを + // 空白化する必要がある。`public int this[\n int i\n] => _items[i];` の `[` は + // インデクサのパラメータリストであり属性ではない。ここを空白化するとインデクサが + // シンボル抽出から静かに消える。 + var content = + "public class Collection\n" + + "{\n" + + " private int[] _items = new int[10];\n" + + " public int this[\n" + + " int i\n" + + " ] => _items[i];\n" + + "}"; + var symbols = SymbolExtractor.Extract(1, "csharp", content); + + var indexer = symbols.FirstOrDefault(s => s.Name == "Item"); + Assert.NotNull(indexer); + Assert.Equal("function", indexer.Kind); + Assert.Equal("int", indexer.ReturnType); + } + [Fact] public void Extract_CSharp_DetectsOperatorOverloads() { From a071f748b42f9ff94202a5799cace30174fdd107 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 18 Apr 2026 13:45:49 +0900 Subject: [PATCH 20/30] Fix #293 restrict deps attribute alias and impact metadata bypass to class-like unambiguous targets - DbReader.GetFileDependencies tags synthetic C# attribute-suffix alias rows with is_attribute_alias = 1 and propagates the flag via MIN in source_name_counts; edges CTE requires has_class_like_kind on alias rows so [Foo] no longer joins same-named functions/properties. - target_files CTE switches from DISTINCT over (path, lang, name, kind) to GROUP BY (path, lang, name) with MAX(has_class_like_kind), so a class plus its constructor do not fan out into two target rows and inflate deps reference counts. - DbReader.GetFileDependencyHintsToResolvedType checks new IsMetadataTargetUnambiguous helper before applying the attribute metadata-bypass; ambiguous names (multiple class-like defs across namespaces) fall through to SourceFileHasStructuredTypeEvidence. - Adds three regression tests covering the alias class-kind guard and both sides (ambiguous / unambiguous) of the metadata-bypass guard. --- CHANGELOG.md | 2 + src/CodeIndex/Database/DbReader.cs | 97 +++++++++++++++-- tests/CodeIndex.Tests/DbReaderTests.cs | 137 +++++++++++++++++++++++++ 3 files changed, 225 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cf0948233..b7687da475 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/), - **Developer Guide now distinguishes SQLite's role from "zero dependencies" and labels release numbers as examples** — Clarified that cdidx remains a zero-configuration, single-file CLI with exactly one production dependency (`Microsoft.Data.Sqlite`), and made the release-workflow checklist explicitly state that `1.9.0` is only an example version to substitute during an actual release. Affected: `DEVELOPER_GUIDE.md`. #### Fixed +- **`deps` C# attribute suffix alias now only matches class-like targets, and `impact` metadata bypass is suppressed when the class-like target name is ambiguous across namespaces (#293 follow-up)** — The earlier `#293` follow-up made `deps` UNION a C#-only `symbol_name || 'Attribute'` suffix alias into the logical-reference CTE so `[Foo]` attribute consumers could reach `FooAttribute` class files, and made `impact` (`GetFileDependencyHintsToResolvedType`) bypass the structured-evidence guard whenever any contributing reference was `attribute` / `annotation`. Both changes were too broad. On `deps` the alias joined purely by name, so `[Foo]` would also depend on any file that merely defined a function / property / variable named `FooAttribute` (e.g. `void MyAuditAttribute()` in an unrelated helper). On `impact` the metadata bypass did not check whether the target name was uniquely a class-like definition, so when the same unqualified name (e.g. `MyAuditAttribute`) existed in multiple namespaces the rename/remove blast radius was over-reported — attribute / annotation reference rows only keep the short name and cannot be disambiguated. `DbReader.GetFileDependencies` now tags synthetic alias rows with `is_attribute_alias = 1` (real rows are `0`), propagates the flag through `source_name_counts` via `MIN`, and restricts alias edges to class-like target kinds (`class` / `struct` / `interface`) in the `edges` CTE join. The `target_files` CTE is also switched from `DISTINCT` over `(path, lang, name, kind)` to `GROUP BY (path, lang, name)` with a `MAX(...)` `has_class_like_kind` flag, so files that define both a class and a same-named function (e.g. the C# constructor `Target()` inside `class Target`) no longer fan out into two target rows and inflate the `deps` reference count. `DbReader.GetFileDependencyHintsToResolvedType` now calls a new `IsMetadataTargetUnambiguous(definition)` helper that counts class-like definitions with the same unqualified name across graph-supported languages; the metadata evidence-guard bypass is only applied when that count is ≤ 1. If the name is ambiguous, metadata-only edges fall through to the normal `SourceFileHasStructuredTypeEvidence` check, so pure `[MyAudit]` consumers are dropped instead of being mis-attributed. The C# attribute deps test `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`, the unambiguous single-definition `impact` test, and the pre-existing graph-queries dedupe test all still pass. Added `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets` (ambiguous same-name function must not produce a spurious edge), `DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget` (two `MyAuditAttribute` classes across namespaces suppress the metadata heuristic hint), and `DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous` (single class definition still surfaces the `[MyAudit]` consumer). Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. - **MCP `callers` / `callees` `kind` schema, CLI help, and README `--kind` option row no longer advertise metadata kinds as valid filter values (#293 follow-up)** — The `callers` / `callees` handlers reject `kind: "attribute"` / `kind: "annotation"` with a usage error (CLI) / `isError` tool response (MCP), but the MCP tool schema's `kind` property description still listed all five kinds as if they were valid filter values (`"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"`), `cdidx --help` similarly lumped `callers` / `callees` together with `references` for the `--kind` help row, and the README `--kind` option table (EN line 436 / JP line 1307) still directed readers to pass `--kind attribute` or `--kind annotation` on `callers` / `callees`. Schema and handler now agree: the `callers` / `callees` `kind` description now reads `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."`, the CLI `--kind` help row splits `references` (accepts all five kinds) from `callers` / `callees` (call-graph kinds only, metadata rejected, redirects to `references`), and the README `--kind` row (both English and Japanese) now explicitly states that `callers` / `callees` reject `--kind attribute|annotation` with a usage error and points to `references --kind attribute|annotation` as the correct metadata-enumeration path. The MCP `references` tool description is unchanged because it still accepts every indexed kind. Added `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` to pin the corrected schema wording. Affected: `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `src/CodeIndex/Cli/ConsoleUi.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `README.md`. - **`deps`, `impact`, `references`, `inspect`, and `analyze_symbol` retain metadata references as compile-time dependency edges and canonicalize the C# attribute-suffix convention (#293 follow-up)** — `DbReader.GetFileDependencies` (backing `cdidx deps`) and `DbReader.GetFileDependencyHintsToResolvedType` (backing `cdidx impact` / MCP `impact_analysis` heuristic file-hint fallback) keep metadata references in their logical reference CTE. Renaming or removing an attribute class still breaks annotated sites such as `[JsonConverterAttribute]` at compile time, so file-level blast-radius analysis must surface those edges as real dependencies — now labeled `attribute` / `annotation` in `logical_reference_kind` (previously these same sites leaked in as phantom `call` rows before classification was corrected). `GetFileDependencies` additionally canonicalizes the C# attribute naming convention: idiomatic `[Foo]` usage is stored with `symbol_name = "Foo"` but the defining class is `FooAttribute`, so the logical-reference CTE now UNION-ALLs a suffix-aliased copy of every C# `attribute`-kind row with `'Attribute'` appended (only when the original symbol_name does not already end in `Attribute`) so the target-file join reaches the attribute class file. `GetFileDependencyHintsToResolvedType` also aggregates a per-candidate `has_metadata_ref` flag and bypasses the `SourceFileHasStructuredTypeEvidence` guard for edges whose contributing references are purely metadata, so pure-attribute consumer files such as `[MyAudit] class Svc` now surface through `impact MyAuditAttribute` instead of being filtered out. `ResolveImpactFallbackNames` symmetrically adds suffix-stripped aliases for C# class names ending in `Attribute`, so the BFS-less file-hint path can find `[Foo]` sites whose source symbol_name does not match the defining class name. The same canonicalization is propagated to the symbol-level reference reader: `DbReader.SearchReferences`, `CountSearchReferences`, and `CountSearchReferencesTotal` (backing `references`, `inspect`, MCP `analyze_symbol`) now compute a C#-only `queryAttributeAlias` (defined in the new `ComputeCSharpAttributeSuffixAlias` helper) and OR it into both exact (folded and NOCASE) and substring WHERE clauses, so `references MyAuditAttribute` (and `inspect MyAuditAttribute` / `analyze_symbol MyAuditAttribute`) surface the idiomatic `[MyAudit]` call site consistently with `deps` / `impact`. Substring mode uses an exact-OR for the alias (not a second LIKE) so user queries like `MyAuditAttribute` do not over-match unrelated names such as `FooAuditLog`. The alias is C# only, so Java / Kotlin / Scala / TypeScript annotation sites remain unaffected. `callers` / `callees` intentionally skip the alias because they filter to the shared call-graph kinds (`call`, `instantiate`, `subscribe`) and reject `--kind attribute|annotation` at the CLI / MCP boundary — those commands model the dynamic call graph, not the dependency graph. The alias branch is additionally scoped so it cannot produce false positives: `ComputeCSharpAttributeSuffixAlias` returns `null` when `referenceKind` is a non-`attribute` call-graph kind (so `references FooAttribute --kind call` stays strict and does not match `Foo()`), and the SQL alias disjunct in all three reader methods is clamped to `f.lang = 'csharp' AND r.reference_kind = 'attribute'` so an unscoped `references FooAttribute` (no `--lang` / no `--kind`) still cannot bleed into Java annotations or C# call rows. Suffix detection is also case-insensitive (`OrdinalIgnoreCase`) so `references myauditattribute` and `inspect MyAuditATTRIBUTE` still produce the `MyAudit` alias, consistent with the NOCASE / folded contract of the surrounding exact / substring query paths. Added regression coverage `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies` (runtime + metadata edges both surface), `DbReaderTests.GetFileDependencies_MatchesCSharpAttributeSuffixConvention` (`[MyAudit]` → `MyAuditAttribute` canonicalization through `deps`), `DbReaderTests.SearchReferences_MatchesCSharpAttributeSuffixConvention_Substring` / `_Exact` / `_CSharpAttributeSuffixAliasDoesNotBleedToOtherLanguages` (same canonicalization through `references` / `inspect` / `analyze_symbol`, plus C#-only scope guard), `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_NotAppliedToCallKind` (alias must not fire under `--kind call`), `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_UnscopedLangStillLimitsToCSharpAttributeRows` (unscoped query must not match Java annotations or C# call rows), and `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_CaseInsensitiveQuery` (lowercase / mixed-case queries still produce the alias). Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. - **`callers` / `callees` now reject `--kind attribute|annotation` at the CLI and MCP boundary, and Swift / Gradle `@` metadata is reclassified as `annotation` (#293 follow-up)** — Metadata rows (`attribute` / `annotation`) are attributed to their enclosing body-range symbol rather than the annotated target, so `callers Obsolete --kind attribute` would silently return `[Obsolete] void M()` under the enclosing class (not `M`), and file-level targets such as `[assembly: CLSCompliant]` or Kotlin `@file:JvmName` would drop entirely because `container_name` is `NULL`. `QueryCommandRunner.RunCallers` / `RunCallees` now reject `--kind attribute` and `--kind annotation` with `CommandExitCodes.UsageError`, and the MCP `callers` / `callees` tools return an `isError` tool response; both paths direct users to `references --kind attribute|annotation`, which IS a correct metadata enumeration path. Separately, `AnnotationLanguages` now includes `swift` and `gradle`, so Swift `@available(iOS 13.0, *)` / `@objc` / `@MainActor` and Gradle/Groovy `@CompileStatic` / `@TaskAction` are recorded as `annotation` instead of `call` (previously the `@Name(args)` forms leaked into `callers` / `callees` / `hotspots` / `impact` as phantom call edges, and the no-arg `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` forms dropped from the index entirely because no-arg annotation emission was gated to languages in this set). Added `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError` (theory, 4 cases), `McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError` (theory, 4 cases), `ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`, and `ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation`. Affected: `src/CodeIndex/Cli/QueryCommandRunner.cs`, `src/CodeIndex/Mcp/McpToolHandlers.cs`, `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. @@ -685,6 +686,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **DEVELOPER_GUIDE が SQLite の位置付けとリリース番号の例示を明確化** — cdidx が「設定ゼロ・単一ファイル・本番依存1個」の CLI であり、その唯一の本番依存が `Microsoft.Data.Sqlite` であることを明記した。あわせて、リリース手順のチェックリスト中に出てくる `1.9.0` は固定値ではなく例示であり、実際のリリース番号へ読み替える前提だと明示した。対象: `DEVELOPER_GUIDE.md`。 #### 修正 +- **`deps` の C# attribute サフィックス別名を class-like ターゲットに限定し、`impact` の metadata bypass も class-like ターゲット名が名前空間をまたいで曖昧な場合は抑止するよう修正 (#293 追加対応)** — 直前の #293 追加対応では、`deps` の logical-reference CTE に C# 限定で `symbol_name || 'Attribute'` のサフィックス別名を UNION することで `[Foo]` の attribute consumer が `FooAttribute` クラスファイルへ到達するようにし、`impact`(`GetFileDependencyHintsToResolvedType`)も寄与参照に `attribute` / `annotation` が含まれていれば structured-evidence guard をバイパスするようにしていたが、どちらも広すぎた。`deps` 側では別名が純粋に名前だけで join していたため、`[Foo]` が同名の関数・プロパティ・変数(例: 無関係なヘルパーの `void MyAuditAttribute()`)を定義するファイルにも依存を張ってしまっていた。`impact` 側の metadata bypass も、対象名が一意に class-like 定義を指すかを確認していなかったため、`MyAuditAttribute` のような短い名前が複数の namespace に存在すると rename / remove の blast-radius が過剰報告されていた(`attribute` / `annotation` 参照行は短い名前しか保持できず曖昧性解消ができない)。`DbReader.GetFileDependencies` は合成された別名行に `is_attribute_alias = 1`(本物の行は `0`)をタグ付けし、`source_name_counts` では `MIN` でフラグを伝播し、`edges` CTE の join で別名 edge をターゲット側 kind が `class` / `struct` / `interface` のときだけに制限するようにした。`target_files` CTE も `DISTINCT (path, lang, name, kind)` から `GROUP BY (path, lang, name)` + `MAX(...)` の `has_class_like_kind` フラグに切り替えたため、class と同名関数(例: C# コンストラクタ `Target()` と `class Target`)を同じファイルで定義しているケースでもターゲット行が 2 本に増えず `deps` の参照数を水増ししない。`DbReader.GetFileDependencyHintsToResolvedType` は新ヘルパー `IsMetadataTargetUnambiguous(definition)` を呼び、graph 対応言語横断で同名の class-like 定義数を数え、その数が 1 以下のときに限って metadata evidence-guard bypass を適用する。名前が曖昧なときは metadata 単独 edge も通常の `SourceFileHasStructuredTypeEvidence` に戻り、純 `[MyAudit]` consumer は誤帰属せずに落ちる。既存の C# attribute deps テスト `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`、曖昧ではない単一定義の `impact` テスト、および既存の graph-queries dedupe テストは全てグリーンのまま。回帰テストとして `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets`(同名関数があっても虚偽 edge が出ないこと)、`DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget`(namespace 違いで `MyAuditAttribute` が複数ある場合に metadata heuristic hint を抑止)、`DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous`(単一クラス定義のみのときは従来どおり `[MyAudit]` consumer を浮かせる)を追加。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 - **MCP の `callers` / `callees` の `kind` schema・CLI help・README の `--kind` オプション行が metadata kinds を有効フィルタ値として広告しないよう修正 (#293 追加対応)** — `callers` / `callees` の handler は `kind: "attribute"` / `kind: "annotation"` を usage error(CLI)/ `isError` ツールレスポンス(MCP)で拒否するにもかかわらず、MCP ツール schema の `kind` プロパティ説明は 5 種すべてを列挙した `"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"` のままで、`cdidx --help` の `--kind` 行も `callers` / `callees` を `references` と同じ行にまとめていた。さらに README の `--kind` オプション表(英語 436 行目 / 日本語 1307 行目)も、`callers` / `callees` で `--kind attribute` / `--kind annotation` を渡せるかのように案内していた。schema と handler の契約を一致させ、`callers` / `callees` の `kind` description を `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."` に修正し、CLI の `--kind` help 行も `references`(5 種すべて受理)と `callers` / `callees`(call-graph 種別のみ、metadata kinds は拒否して `references` へ誘導)を分けるようにし、README の `--kind` 行(英語・日本語の両方)も `callers` / `callees` が `--kind attribute|annotation` を usage error で拒否すること、metadata 列挙の正規経路が `references --kind attribute|annotation` であることを明示する文言に直した。MCP の `references` ツール説明は全 reference kind を受け付ける契約のままなので変更なし。修正後の schema 文言を固定する `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` を追加。対象: `src/CodeIndex/Mcp/McpToolDefinitions.cs`、`src/CodeIndex/Cli/ConsoleUi.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`、`README.md`。 - **`deps`・`impact`・`references`・`inspect`・`analyze_symbol` が metadata 参照を compile-time 依存として保持し、C# の attribute サフィックス命名規約を正規化 (#293 追加対応)** — `cdidx deps` の実装である `DbReader.GetFileDependencies` と、`cdidx impact` / MCP `impact_analysis` の heuristic file-hint フォールバックである `DbReader.GetFileDependencyHintsToResolvedType` は、logical reference CTE に metadata 参照も含める。attribute クラスを rename / 削除すれば `[JsonConverterAttribute]` のような注釈サイトも compile-time で壊れるため、ファイル単位の blast-radius 分析はそれらも本物の依存 edge として表示する必要があり、`logical_reference_kind` では `attribute` / `annotation` として正しくラベル付けされる(分類修正前は同じサイトが phantom `call` として漏れていた)。さらに `GetFileDependencies` は C# の attribute 命名規約を正規化する: 慣用的な `[Foo]` は `symbol_name = "Foo"` で保存されるが、定義クラスは `FooAttribute` 命名となるため、logical-reference CTE に C# `attribute` 種別の行をサフィックス `'Attribute'` を付与した別名コピーとして UNION ALL で追加する(元の symbol_name が既に `Attribute` で終わっている場合は重複しないようスキップ)。これにより target ファイル join が attribute クラス側のファイルへ到達する。`GetFileDependencyHintsToResolvedType` はさらに candidate ごとに `has_metadata_ref` フラグを集約し、寄与する参照が metadata だけである edge については `SourceFileHasStructuredTypeEvidence` guard をバイパスするため、`[MyAudit] class Svc` のような純 attribute consumer ファイルも `impact MyAuditAttribute` で出るようになった(以前は filter で落ちていた)。`ResolveImpactFallbackNames` も対称的に、`Attribute` で終わる C# クラス名にはサフィックスを外した別名を追加するため、BFS を使わない file-hint 経路でも source symbol_name が定義クラス名と一致しない `[Foo]` サイトを発見できる。同じ正規化をシンボル単位の参照 reader にも伝播する: `references`・`inspect`・MCP `analyze_symbol` を支える `DbReader.SearchReferences`・`CountSearchReferences`・`CountSearchReferencesTotal` は、新しいヘルパー `ComputeCSharpAttributeSuffixAlias` で C# 限定の `queryAttributeAlias` を計算し、exact(folded と NOCASE)・substring の両方の WHERE 句に OR で追加する。これにより `references MyAuditAttribute`(および `inspect MyAuditAttribute` / `analyze_symbol MyAuditAttribute`)でも慣用的な `[MyAudit]` 参照サイトが `deps` / `impact` と一貫して surface する。substring モードでは alias を二つ目の LIKE にせず exact-OR で束ねているため、ユーザーが `MyAuditAttribute` と打った時に `FooAuditLog` のような無関係な名前に over-match しない。alias は C# 限定なので、Java / Kotlin / Scala / TypeScript の annotation サイトには影響しない。`callers` / `callees` は共有の call-graph 種別 (`call` / `instantiate` / `subscribe`) に絞って `--kind attribute|annotation` を CLI / MCP 境界で拒否する契約なので、alias は意図的に適用しない(dynamic call graph と dependency graph は別契約)。さらに alias 節が誤一致を生まないようスコープする: `ComputeCSharpAttributeSuffixAlias` は `referenceKind` が `attribute` 以外の call-graph 種別のときは `null` を返す(`references FooAttribute --kind call` は厳格一致のままで `Foo()` に一致しない)、3 つの reader メソッドの SQL alias 節は `f.lang = 'csharp' AND r.reference_kind = 'attribute'` に限定するため、`--lang` / `--kind` 無指定の `references FooAttribute` でも Java の annotation や C# の call 行を拾わない。suffix 検出も `OrdinalIgnoreCase` にして case-insensitive 化したため、`references myauditattribute` / `inspect MyAuditATTRIBUTE` のような小文字・混在ケースでも alias `MyAudit` を生成でき、周辺の NOCASE / folded 契約と整合する。回帰テスト `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies`(runtime と metadata の両方の edge が出ることを固定)、`DbReaderTests.GetFileDependencies_MatchesCSharpAttributeSuffixConvention`(`deps` 経由での `[MyAudit]` → `MyAuditAttribute` 正規化)、`DbReaderTests.SearchReferences_MatchesCSharpAttributeSuffixConvention_Substring` / `_Exact` / `_CSharpAttributeSuffixAliasDoesNotBleedToOtherLanguages`(`references` / `inspect` / `analyze_symbol` での同一正規化と C# 限定スコープ)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_NotAppliedToCallKind`(`--kind call` で alias が発火しないこと)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_UnscopedLangStillLimitsToCSharpAttributeRows`(無指定でも Java annotation や C# call 行に一致しないこと)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_CaseInsensitiveQuery`(小文字・混在ケースのクエリでも alias を生成)を追加。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 - **`callers` / `callees` が CLI と MCP 境界で `--kind attribute|annotation` を拒否し、Swift / Gradle の `@` metadata を `annotation` に再分類 (#293 追加対応)** — metadata 行 (`attribute` / `annotation`) は注釈対象そのものではなく body-range の外側シンボルに帰属するため、`callers Obsolete --kind attribute` は `[Obsolete] void M()` を `M` ではなく外側クラスの下に黙って返し、`[assembly: CLSCompliant]` や Kotlin の `@file:JvmName` のような file-level target は `container_name = NULL` で完全脱落していた。`QueryCommandRunner.RunCallers` / `RunCallees` は `--kind attribute` / `--kind annotation` を `CommandExitCodes.UsageError` で弾き、MCP の `callers` / `callees` ツールは `isError` ツールレスポンスを返すようにした。どちらの経路も、metadata 列挙の正しいパスである `references --kind attribute|annotation` へ誘導する。あわせて `AnnotationLanguages` に `swift` と `gradle` を追加し、Swift の `@available(iOS 13.0, *)` / `@objc` / `@MainActor` や Gradle / Groovy の `@CompileStatic` / `@TaskAction` を `call` ではなく `annotation` として記録するようにした(修正前は引数付きの `@Name(args)` 形は `callers` / `callees` / `hotspots` / `impact` に phantom call edge として漏れ、引数なしの `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` 形はこの集合で gate されていた no-arg annotation 出力から外れてインデックス自体に残らなかった)。回帰テストとして `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError`(theory 4 ケース)、`McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError`(theory 4 ケース)、`ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`、`ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation` を追加。対象: `src/CodeIndex/Cli/QueryCommandRunner.cs`、`src/CodeIndex/Mcp/McpToolHandlers.cs`、`src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 diff --git a/src/CodeIndex/Database/DbReader.cs b/src/CodeIndex/Database/DbReader.cs index ef377a9f0d..87d88a8e8a 100644 --- a/src/CodeIndex/Database/DbReader.cs +++ b/src/CodeIndex/Database/DbReader.cs @@ -1865,19 +1865,33 @@ FROM symbol_references r })); } + // Metadata references only carry the short use-site name (`Foo` for `[Foo]`, + // `@Foo`). If multiple class-like definitions share the same unqualified name + // across namespaces / packages (e.g. `A.MyAuditAttribute` and + // `B.MyAuditAttribute`), we cannot uniquely attribute a `[MyAudit]` site to + // either target. Skip the metadata evidence bypass in that ambiguous case so + // `impact` does not over-report the blast radius of a rename / removal. + // metadata 参照は use-site 側の短縮名 (`[Foo]` / `@Foo` の `Foo`) しか持た + // ないため、namespace / package を跨いで同名の class-like 定義が複数存在 + // する場合、`[MyAudit]` 参照をどちらの target にも一意に紐付けられない。 + // そのような曖昧なケースでは metadata の evidence bypass を行わず、 + // `impact` が rename / 削除の影響範囲を過大報告しないようにする。 + var metadataBypassSafe = IsMetadataTargetUnambiguous(definition); var evidenceCache = new Dictionary(); var filtered = new List(); foreach (var candidate in candidates) { // Metadata-only consumers (attribute / annotation sites like `[MyAudit]` or // `@Inject(User.class)`) legitimately lack structured type evidence in the - // source file. Bypass the evidence guard for those edges so deps/impact can - // still surface pure-attribute consumers. + // source file. Bypass the evidence guard for those edges only when the + // class-like target is unambiguous so deps/impact can surface pure-attribute + // consumers without over-attributing same-named targets. // metadata 専用の参照 (`[MyAudit]` や `@Inject(User.class)` のような attribute / // annotation 利用) は、source 側のファイルに structured な型利用が無くても - // 正当な依存となる。そのため、metadata 参照を含むエッジでは evidence guard を - // スキップし、純粋な attribute 消費側ファイルも deps/impact に出せるようにする。 - if (candidate.HasMetadataRef) + // 正当な依存となるが、class-like target が一意に決まるときだけ evidence guard + // をスキップする。曖昧なときは下の evidence 要求へフォールスルーさせ、 + // 同名 target への誤帰属を防ぐ。 + if (candidate.HasMetadataRef && metadataBypassSafe) { filtered.Add(candidate.Edge); continue; @@ -1898,6 +1912,35 @@ FROM symbol_references r return (filtered, truncated); } + // Returns true when the metadata target name resolves to at most one class-like + // symbol across the graph-supported languages. Ambiguous names (same unqualified + // name under different namespaces / packages) must not trigger the metadata + // evidence bypass because attribute / annotation reference rows only keep the + // short name and cannot disambiguate between them. + // graph 対応言語の中で class-like シンボルが高々 1 件しか存在しないときに true。 + // namespace / package を跨いで同名の class-like 定義が複数ある曖昧なケースでは + // attribute / annotation 参照行が短縮名しか持たず区別できないため、metadata の + // evidence bypass を許可しない。 + private bool IsMetadataTargetUnambiguous(SymbolResult definition) + { + if (string.IsNullOrWhiteSpace(definition.Name)) + return false; + using var cmd = _conn.CreateCommand(); + var supportedLangFilter = BuildGraphSupportedLanguagePredicate(cmd, "f", "metadataAmbigLang"); + cmd.CommandText = $@" + SELECT COUNT(*) FROM ( + SELECT DISTINCT f.path + FROM symbols s + JOIN files f ON s.file_id = f.id + WHERE s.name = @metadataAmbigName COLLATE NOCASE + AND s.kind IN ('class','struct','interface') + AND {supportedLangFilter} + )"; + cmd.Parameters.AddWithValue("@metadataAmbigName", definition.Name); + var count = Convert.ToInt32(cmd.ExecuteScalar() ?? 0); + return count <= 1; + } + private bool SourceFileHasStructuredTypeEvidence(long fileId, string typeName) { using var cmd = _conn.CreateCommand(); @@ -2512,18 +2555,24 @@ FROM symbol_references r GROUP BY src.id, src.path, src.lang, r.symbol_name, r.line, r.column_number, logical_reference_kind ), logical_references AS ( - SELECT source_file_id, source_path, source_lang, symbol_name, line, column_number, logical_reference_kind + SELECT source_file_id, source_path, source_lang, symbol_name, line, column_number, logical_reference_kind, + 0 AS is_attribute_alias FROM logical_references_primary UNION ALL -- C# attribute suffix alias: [Foo] in source is stored with symbol_name='Foo', -- but the defining class is named 'FooAttribute'. Emit the canonical 'Foo' + 'Attribute' - -- form so deps can match the class file as a target. + -- form so deps can match the class file as a target. The alias rows are flagged + -- so the edges CTE can restrict them to class-like targets and avoid spurious + -- edges to unrelated functions / properties that happen to be named 'FooAttribute'. -- C# 属性のサフィックス別名: ソース上の [Foo] は symbol_name='Foo' で保存されるが、 -- 定義クラスは 'FooAttribute' 命名になるため、正規形 'Foo' + 'Attribute' を補って - -- deps がクラス側のファイルを target として join できるようにする。 + -- deps がクラス側のファイルを target として join できるようにする。alias 行には + -- フラグを付け、edges CTE 側で class-like target だけに限定する。これにより、 + -- 偶然 'FooAttribute' という名前を持つ関数やプロパティへの誤ったエッジを防ぐ。 SELECT source_file_id, source_path, source_lang, symbol_name || 'Attribute' AS symbol_name, - line, column_number, logical_reference_kind + line, column_number, logical_reference_kind, + 1 AS is_attribute_alias FROM logical_references_primary WHERE source_lang = 'csharp' AND logical_reference_kind = 'attribute' @@ -2534,14 +2583,31 @@ source_name_counts AS ( source_path, source_lang, symbol_name, + -- MIN: if any real (non-alias) ref shares the same symbol_name group, + -- treat the group as non-alias so the class-kind restriction only + -- applies to purely synthetic alias rows. + -- MIN: 同じ symbol_name のグループに real (非 alias) 行が 1 件でも + -- あれば、そのグループは非 alias 扱いとし、class 種別の制限を + -- 純粋に合成された alias 行にのみ適用する。 + MIN(is_attribute_alias) AS is_attribute_alias, COUNT(*) AS ref_count FROM logical_references GROUP BY source_file_id, source_path, source_lang, symbol_name ), target_files AS ( - SELECT DISTINCT dst.path AS target_path, + -- Collapse per-symbol rows to one per (target_path, target_lang, symbol_name) + -- and remember whether any of the same-name symbols is a class-like kind + -- via MAX. Keeping kind in DISTINCT would split identical (path, lang, name) + -- rows when one file defines both a class and a same-name function (e.g. a + -- C# constructor), inflating the deps reference count. + -- (target_path, target_lang, symbol_name) 単位に集約し、同名のシンボルの + -- いずれかが class 系であるかを MAX で覚える。kind を DISTINCT に含めると、 + -- 同じ (path, lang, name) でも class と同名 function (C# のコンストラクタ等) + -- が別行として残り、deps の参照カウントが膨らんでしまう。 + SELECT dst.path AS target_path, dst.lang AS target_lang, - s.name AS symbol_name + s.name AS symbol_name, + MAX(CASE WHEN s.kind IN ('class','struct','interface') THEN 1 ELSE 0 END) AS has_class_like_kind FROM symbols s JOIN files dst ON s.file_id = dst.id WHERE 1 = 1"; @@ -2563,6 +2629,7 @@ FROM symbols s if (reverse && excludeTests) sql += $" AND NOT {TestPathCondition.Replace("f.path", $"{targetFilterAlias}.path")}"; sql += @" + GROUP BY dst.path, dst.lang, s.name ), edges AS ( SELECT snc.source_path, @@ -2574,6 +2641,14 @@ JOIN target_files tf ON tf.symbol_name = snc.symbol_name AND tf.target_lang = snc.source_lang WHERE snc.source_path != tf.target_path + -- Purely synthetic C# attribute alias rows must only match class-like + -- target kinds; otherwise '[Foo]' would spuriously depend on any file + -- that merely defines a function / property / variable named + -- 'FooAttribute'. Real attribute references continue to match any kind. + -- 純粋な合成 alias 行は class 系の target 種別にのみ一致させる。 + -- これを許すと '[Foo]' が単に 'FooAttribute' という名前の関数や + -- プロパティを持つファイルにまで誤って依存してしまう。 + AND (snc.is_attribute_alias = 0 OR tf.has_class_like_kind = 1) ) SELECT source_path, target_path, diff --git a/tests/CodeIndex.Tests/DbReaderTests.cs b/tests/CodeIndex.Tests/DbReaderTests.cs index 046daa8108..dce4119cb2 100644 --- a/tests/CodeIndex.Tests/DbReaderTests.cs +++ b/tests/CodeIndex.Tests/DbReaderTests.cs @@ -2527,6 +2527,143 @@ public class Svc Assert.Contains(mixedCaseExactResults, r => r.Path == "src/Svc.cs" && r.ReferenceKind == "attribute"); } + [Fact] + public void GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets() + { + // issue #293 review: the C# attribute suffix alias UNION synthesizes a + // `FooAttribute` lookup key for `[Foo]` references. Without a kind guard the + // subsequent name-only join would spuriously attribute the consumer to any + // file that merely defines a function / property / variable also named + // `FooAttribute`. Only class-like target symbols should match synthetic alias + // rows. + // issue #293 レビュー指摘: `[Foo]` 用の alias UNION は `FooAttribute` という + // lookup key を合成するが、kind によるガードが無いと、偶然 `FooAttribute` + // という名前を持つ関数 / プロパティ / 変数を含むファイルにまで依存が張られて + // しまう。合成 alias 行は class 系の target にのみ一致すべき。 + InsertIndexedFile("src/MyAuditAttribute.cs", "csharp", + """ + using System; + + [AttributeUsage(AttributeTargets.Class)] + public sealed class MyAuditAttribute : Attribute + { + } + """); + // Unrelated file containing a function named `MyAuditAttribute` — not an + // attribute class, so `[MyAudit]` must not produce a dependency edge to it. + // 無関係なファイルに関数として `MyAuditAttribute` が居るケース。 + // `[MyAudit]` はこのファイルへの依存を作ってはいけない。 + InsertIndexedFile("src/Util.cs", "csharp", + """ + public static class Util + { + public static void MyAuditAttribute() + { + } + } + """); + InsertIndexedFile("src/Svc.cs", "csharp", + """ + [MyAudit] + public class Svc + { + } + """); + + var dependencies = _reader.GetFileDependencies(limit: 20, lang: "csharp"); + + Assert.Contains(dependencies, d => d.SourcePath == "src/Svc.cs" && d.TargetPath == "src/MyAuditAttribute.cs"); + Assert.DoesNotContain(dependencies, d => d.SourcePath == "src/Svc.cs" && d.TargetPath == "src/Util.cs"); + } + + [Fact] + public void GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget() + { + // issue #293 review: when two classes share the `MyAuditAttribute` name across + // namespaces, a `[MyAudit]` reference row only carries the short name and + // cannot be uniquely attributed to either target. In that ambiguous case the + // `impact` metadata evidence bypass must be skipped so rename / removal blast + // radius is not over-reported. + // issue #293 レビュー指摘: namespace を跨いで同名の `MyAuditAttribute` クラスが + // 存在するとき、`[MyAudit]` 参照行は短縮名しか持たず、どちらの target にも + // 一意に紐付けられない。この曖昧なケースでは `impact` の metadata evidence + // bypass を行わず、rename / 削除の影響範囲を過大報告しないようにする。 + InsertIndexedFile("src/A/MyAuditAttribute.cs", "csharp", + """ + namespace A; + + public sealed class MyAuditAttribute : System.Attribute + { + } + """); + InsertIndexedFile("src/B/MyAuditAttribute.cs", "csharp", + """ + namespace B; + + public sealed class MyAuditAttribute : System.Attribute + { + } + """); + // Pure attribute consumer in src/A/ — no structured type evidence exists for + // `MyAuditAttribute` other than the `[MyAudit]` use site itself. + // src/A/ に純粋な attribute consumer — `MyAuditAttribute` に対する構造化された + // 型証拠は `[MyAudit]` use site 以外には無い。 + InsertIndexedFile("src/A/Svc.cs", "csharp", + """ + namespace A; + + [MyAudit] + public class Svc + { + } + """); + + // Narrow `impact` to src/A/ so ResolveImpactDefinitions returns exactly one + // definition. Without the ambiguity guard, the metadata bypass would fabricate + // a heuristic edge even though the `[MyAudit]` target is qualifier-ambiguous. + // src/A/ に絞ることで ResolveImpactDefinitions が 1 件だけ返す状態を作る。 + // ambiguity guard が無ければ、`[MyAudit]` の target が qualifier 曖昧でも + // metadata bypass が heuristic エッジを作ってしまう。 + var result = _reader.AnalyzeImpact( + "MyAuditAttribute", + maxDepth: 3, + limit: 20, + lang: "csharp", + pathPatterns: new[] { "src/A/" }); + + Assert.DoesNotContain(result.FileImpacts, f => f.SourcePath == "src/A/Svc.cs"); + } + + [Fact] + public void GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous() + { + // issue #293 review: the ambiguity guard must only fire when genuinely + // ambiguous. With a single class-like `MyAuditAttribute` definition the + // metadata bypass should still surface the `[MyAudit]` consumer as a + // file-level hint, preserving the legitimate pure-attribute consumer case. + // issue #293 レビュー指摘: ambiguity guard は本当に曖昧なときだけ発動すべき。 + // `MyAuditAttribute` の class 定義が 1 件しかない場合は従来通り metadata + // bypass で `[MyAudit]` consumer を file-level hint として出し、純粋な + // attribute consumer の正当な検出を保つ。 + InsertIndexedFile("src/MyAuditAttribute.cs", "csharp", + """ + public sealed class MyAuditAttribute : System.Attribute + { + } + """); + InsertIndexedFile("src/Svc.cs", "csharp", + """ + [MyAudit] + public class Svc + { + } + """); + + var result = _reader.AnalyzeImpact("MyAuditAttribute", maxDepth: 3, limit: 20, lang: "csharp"); + + Assert.Contains(result.FileImpacts, f => f.SourcePath == "src/Svc.cs" && f.TargetPath == "src/MyAuditAttribute.cs"); + } + [Fact] public void GetGroupedSymbolHotspots_CollapsesDuplicateNamesWithoutBareJoinInflation() { From 13ae9f6c1d355f8c8a783a113dd4bd4a2036bfd8 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 18 Apr 2026 13:57:43 +0900 Subject: [PATCH 21/30] Fix #293 scope impact metadata bypass ambiguity guard to active lang/path/excludeTests Round 11 adversarial review finding: IsMetadataTargetUnambiguous counted class-like definitions across all graph-supported languages in the whole DB, ignoring the caller's --lang / --path / --exclude-path / --exclude-tests filters. That made the bypass brittle: a same-named class in an unrelated language, path subtree, or test tree could suppress a legitimate C#-scoped heuristic file hint and contradict the documented README contract ("active --lang / --path / --exclude-path / --exclude-tests scope"). - DbReader.IsMetadataTargetUnambiguous now takes the same lang / pathPatterns / excludePathPatterns / excludeTests filters as GetFileDependencyHintsToResolvedType and applies them to the class-like counting query. - Existing ambiguity regression test is rewritten to place both MyAuditAttribute definitions inside the same path scope (src/A/Inner1/ and src/A/Inner2/) so it still fails without the guard after the scoping fix. - Adds three regressions for the newly scoped guard: - RespectsLangScope (same-named Java annotation must not break --lang csharp bypass) - RespectsPathScope (out-of-scope src/B/ class must not break src/A/-scoped bypass) - RespectsExcludeTests (test-tree same-named class must not break --exclude-tests bypass) --- CHANGELOG.md | 4 +- src/CodeIndex/Database/DbReader.cs | 41 +++++- tests/CodeIndex.Tests/DbReaderTests.cs | 175 ++++++++++++++++++++++--- 3 files changed, 193 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7687da475..632f1bcfcb 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/), - **Developer Guide now distinguishes SQLite's role from "zero dependencies" and labels release numbers as examples** — Clarified that cdidx remains a zero-configuration, single-file CLI with exactly one production dependency (`Microsoft.Data.Sqlite`), and made the release-workflow checklist explicitly state that `1.9.0` is only an example version to substitute during an actual release. Affected: `DEVELOPER_GUIDE.md`. #### Fixed -- **`deps` C# attribute suffix alias now only matches class-like targets, and `impact` metadata bypass is suppressed when the class-like target name is ambiguous across namespaces (#293 follow-up)** — The earlier `#293` follow-up made `deps` UNION a C#-only `symbol_name || 'Attribute'` suffix alias into the logical-reference CTE so `[Foo]` attribute consumers could reach `FooAttribute` class files, and made `impact` (`GetFileDependencyHintsToResolvedType`) bypass the structured-evidence guard whenever any contributing reference was `attribute` / `annotation`. Both changes were too broad. On `deps` the alias joined purely by name, so `[Foo]` would also depend on any file that merely defined a function / property / variable named `FooAttribute` (e.g. `void MyAuditAttribute()` in an unrelated helper). On `impact` the metadata bypass did not check whether the target name was uniquely a class-like definition, so when the same unqualified name (e.g. `MyAuditAttribute`) existed in multiple namespaces the rename/remove blast radius was over-reported — attribute / annotation reference rows only keep the short name and cannot be disambiguated. `DbReader.GetFileDependencies` now tags synthetic alias rows with `is_attribute_alias = 1` (real rows are `0`), propagates the flag through `source_name_counts` via `MIN`, and restricts alias edges to class-like target kinds (`class` / `struct` / `interface`) in the `edges` CTE join. The `target_files` CTE is also switched from `DISTINCT` over `(path, lang, name, kind)` to `GROUP BY (path, lang, name)` with a `MAX(...)` `has_class_like_kind` flag, so files that define both a class and a same-named function (e.g. the C# constructor `Target()` inside `class Target`) no longer fan out into two target rows and inflate the `deps` reference count. `DbReader.GetFileDependencyHintsToResolvedType` now calls a new `IsMetadataTargetUnambiguous(definition)` helper that counts class-like definitions with the same unqualified name across graph-supported languages; the metadata evidence-guard bypass is only applied when that count is ≤ 1. If the name is ambiguous, metadata-only edges fall through to the normal `SourceFileHasStructuredTypeEvidence` check, so pure `[MyAudit]` consumers are dropped instead of being mis-attributed. The C# attribute deps test `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`, the unambiguous single-definition `impact` test, and the pre-existing graph-queries dedupe test all still pass. Added `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets` (ambiguous same-name function must not produce a spurious edge), `DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget` (two `MyAuditAttribute` classes across namespaces suppress the metadata heuristic hint), and `DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous` (single class definition still surfaces the `[MyAudit]` consumer). Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. +- **`deps` C# attribute suffix alias now only matches class-like targets, and `impact` metadata bypass is suppressed when the class-like target name is ambiguous across namespaces (#293 follow-up)** — The earlier `#293` follow-up made `deps` UNION a C#-only `symbol_name || 'Attribute'` suffix alias into the logical-reference CTE so `[Foo]` attribute consumers could reach `FooAttribute` class files, and made `impact` (`GetFileDependencyHintsToResolvedType`) bypass the structured-evidence guard whenever any contributing reference was `attribute` / `annotation`. Both changes were too broad. On `deps` the alias joined purely by name, so `[Foo]` would also depend on any file that merely defined a function / property / variable named `FooAttribute` (e.g. `void MyAuditAttribute()` in an unrelated helper). On `impact` the metadata bypass did not check whether the target name was uniquely a class-like definition, so when the same unqualified name (e.g. `MyAuditAttribute`) existed in multiple namespaces the rename/remove blast radius was over-reported — attribute / annotation reference rows only keep the short name and cannot be disambiguated. `DbReader.GetFileDependencies` now tags synthetic alias rows with `is_attribute_alias = 1` (real rows are `0`), propagates the flag through `source_name_counts` via `MIN`, and restricts alias edges to class-like target kinds (`class` / `struct` / `interface`) in the `edges` CTE join. The `target_files` CTE is also switched from `DISTINCT` over `(path, lang, name, kind)` to `GROUP BY (path, lang, name)` with a `MAX(...)` `has_class_like_kind` flag, so files that define both a class and a same-named function (e.g. the C# constructor `Target()` inside `class Target`) no longer fan out into two target rows and inflate the `deps` reference count. `DbReader.GetFileDependencyHintsToResolvedType` now calls a new `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` helper that counts class-like definitions with the same unqualified name *within the active impact scope* (graph-supported languages, plus the caller's `--lang` / `--path` / `--exclude-path` / `--exclude-tests` filters); the metadata evidence-guard bypass is only applied when that in-scope count is ≤ 1. If the name is ambiguous within scope, metadata-only edges fall through to the normal `SourceFileHasStructuredTypeEvidence` check, so pure `[MyAudit]` consumers are dropped instead of being mis-attributed. Scoping matters: a same-named class in an unrelated language / path / test tree must not suppress the C# metadata bypass, because attribute reference rows are already language-qualified through the graph-supported reference join, and the README contract for `impact` explicitly promises "active `--lang` / `--path` / `--exclude-path` / `--exclude-tests` scope". The C# attribute deps test `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`, the unambiguous single-definition `impact` test, and the pre-existing graph-queries dedupe test all still pass. Added `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets` (ambiguous same-name function must not produce a spurious edge), `DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget` (two `MyAuditAttribute` classes within the same impact scope suppress the metadata heuristic hint), `DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous` (single class definition still surfaces the `[MyAudit]` consumer), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope` (same-named Java annotation must not suppress a `--lang csharp` query), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope` (out-of-scope `src/B/` same-named class must not suppress a `src/A/`-scoped query), and `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests` (test-tree same-named class must not suppress an `--exclude-tests` query). Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. - **MCP `callers` / `callees` `kind` schema, CLI help, and README `--kind` option row no longer advertise metadata kinds as valid filter values (#293 follow-up)** — The `callers` / `callees` handlers reject `kind: "attribute"` / `kind: "annotation"` with a usage error (CLI) / `isError` tool response (MCP), but the MCP tool schema's `kind` property description still listed all five kinds as if they were valid filter values (`"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"`), `cdidx --help` similarly lumped `callers` / `callees` together with `references` for the `--kind` help row, and the README `--kind` option table (EN line 436 / JP line 1307) still directed readers to pass `--kind attribute` or `--kind annotation` on `callers` / `callees`. Schema and handler now agree: the `callers` / `callees` `kind` description now reads `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."`, the CLI `--kind` help row splits `references` (accepts all five kinds) from `callers` / `callees` (call-graph kinds only, metadata rejected, redirects to `references`), and the README `--kind` row (both English and Japanese) now explicitly states that `callers` / `callees` reject `--kind attribute|annotation` with a usage error and points to `references --kind attribute|annotation` as the correct metadata-enumeration path. The MCP `references` tool description is unchanged because it still accepts every indexed kind. Added `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` to pin the corrected schema wording. Affected: `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `src/CodeIndex/Cli/ConsoleUi.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `README.md`. - **`deps`, `impact`, `references`, `inspect`, and `analyze_symbol` retain metadata references as compile-time dependency edges and canonicalize the C# attribute-suffix convention (#293 follow-up)** — `DbReader.GetFileDependencies` (backing `cdidx deps`) and `DbReader.GetFileDependencyHintsToResolvedType` (backing `cdidx impact` / MCP `impact_analysis` heuristic file-hint fallback) keep metadata references in their logical reference CTE. Renaming or removing an attribute class still breaks annotated sites such as `[JsonConverterAttribute]` at compile time, so file-level blast-radius analysis must surface those edges as real dependencies — now labeled `attribute` / `annotation` in `logical_reference_kind` (previously these same sites leaked in as phantom `call` rows before classification was corrected). `GetFileDependencies` additionally canonicalizes the C# attribute naming convention: idiomatic `[Foo]` usage is stored with `symbol_name = "Foo"` but the defining class is `FooAttribute`, so the logical-reference CTE now UNION-ALLs a suffix-aliased copy of every C# `attribute`-kind row with `'Attribute'` appended (only when the original symbol_name does not already end in `Attribute`) so the target-file join reaches the attribute class file. `GetFileDependencyHintsToResolvedType` also aggregates a per-candidate `has_metadata_ref` flag and bypasses the `SourceFileHasStructuredTypeEvidence` guard for edges whose contributing references are purely metadata, so pure-attribute consumer files such as `[MyAudit] class Svc` now surface through `impact MyAuditAttribute` instead of being filtered out. `ResolveImpactFallbackNames` symmetrically adds suffix-stripped aliases for C# class names ending in `Attribute`, so the BFS-less file-hint path can find `[Foo]` sites whose source symbol_name does not match the defining class name. The same canonicalization is propagated to the symbol-level reference reader: `DbReader.SearchReferences`, `CountSearchReferences`, and `CountSearchReferencesTotal` (backing `references`, `inspect`, MCP `analyze_symbol`) now compute a C#-only `queryAttributeAlias` (defined in the new `ComputeCSharpAttributeSuffixAlias` helper) and OR it into both exact (folded and NOCASE) and substring WHERE clauses, so `references MyAuditAttribute` (and `inspect MyAuditAttribute` / `analyze_symbol MyAuditAttribute`) surface the idiomatic `[MyAudit]` call site consistently with `deps` / `impact`. Substring mode uses an exact-OR for the alias (not a second LIKE) so user queries like `MyAuditAttribute` do not over-match unrelated names such as `FooAuditLog`. The alias is C# only, so Java / Kotlin / Scala / TypeScript annotation sites remain unaffected. `callers` / `callees` intentionally skip the alias because they filter to the shared call-graph kinds (`call`, `instantiate`, `subscribe`) and reject `--kind attribute|annotation` at the CLI / MCP boundary — those commands model the dynamic call graph, not the dependency graph. The alias branch is additionally scoped so it cannot produce false positives: `ComputeCSharpAttributeSuffixAlias` returns `null` when `referenceKind` is a non-`attribute` call-graph kind (so `references FooAttribute --kind call` stays strict and does not match `Foo()`), and the SQL alias disjunct in all three reader methods is clamped to `f.lang = 'csharp' AND r.reference_kind = 'attribute'` so an unscoped `references FooAttribute` (no `--lang` / no `--kind`) still cannot bleed into Java annotations or C# call rows. Suffix detection is also case-insensitive (`OrdinalIgnoreCase`) so `references myauditattribute` and `inspect MyAuditATTRIBUTE` still produce the `MyAudit` alias, consistent with the NOCASE / folded contract of the surrounding exact / substring query paths. Added regression coverage `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies` (runtime + metadata edges both surface), `DbReaderTests.GetFileDependencies_MatchesCSharpAttributeSuffixConvention` (`[MyAudit]` → `MyAuditAttribute` canonicalization through `deps`), `DbReaderTests.SearchReferences_MatchesCSharpAttributeSuffixConvention_Substring` / `_Exact` / `_CSharpAttributeSuffixAliasDoesNotBleedToOtherLanguages` (same canonicalization through `references` / `inspect` / `analyze_symbol`, plus C#-only scope guard), `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_NotAppliedToCallKind` (alias must not fire under `--kind call`), `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_UnscopedLangStillLimitsToCSharpAttributeRows` (unscoped query must not match Java annotations or C# call rows), and `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_CaseInsensitiveQuery` (lowercase / mixed-case queries still produce the alias). Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. - **`callers` / `callees` now reject `--kind attribute|annotation` at the CLI and MCP boundary, and Swift / Gradle `@` metadata is reclassified as `annotation` (#293 follow-up)** — Metadata rows (`attribute` / `annotation`) are attributed to their enclosing body-range symbol rather than the annotated target, so `callers Obsolete --kind attribute` would silently return `[Obsolete] void M()` under the enclosing class (not `M`), and file-level targets such as `[assembly: CLSCompliant]` or Kotlin `@file:JvmName` would drop entirely because `container_name` is `NULL`. `QueryCommandRunner.RunCallers` / `RunCallees` now reject `--kind attribute` and `--kind annotation` with `CommandExitCodes.UsageError`, and the MCP `callers` / `callees` tools return an `isError` tool response; both paths direct users to `references --kind attribute|annotation`, which IS a correct metadata enumeration path. Separately, `AnnotationLanguages` now includes `swift` and `gradle`, so Swift `@available(iOS 13.0, *)` / `@objc` / `@MainActor` and Gradle/Groovy `@CompileStatic` / `@TaskAction` are recorded as `annotation` instead of `call` (previously the `@Name(args)` forms leaked into `callers` / `callees` / `hotspots` / `impact` as phantom call edges, and the no-arg `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` forms dropped from the index entirely because no-arg annotation emission was gated to languages in this set). Added `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError` (theory, 4 cases), `McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError` (theory, 4 cases), `ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`, and `ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation`. Affected: `src/CodeIndex/Cli/QueryCommandRunner.cs`, `src/CodeIndex/Mcp/McpToolHandlers.cs`, `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. @@ -686,7 +686,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **DEVELOPER_GUIDE が SQLite の位置付けとリリース番号の例示を明確化** — cdidx が「設定ゼロ・単一ファイル・本番依存1個」の CLI であり、その唯一の本番依存が `Microsoft.Data.Sqlite` であることを明記した。あわせて、リリース手順のチェックリスト中に出てくる `1.9.0` は固定値ではなく例示であり、実際のリリース番号へ読み替える前提だと明示した。対象: `DEVELOPER_GUIDE.md`。 #### 修正 -- **`deps` の C# attribute サフィックス別名を class-like ターゲットに限定し、`impact` の metadata bypass も class-like ターゲット名が名前空間をまたいで曖昧な場合は抑止するよう修正 (#293 追加対応)** — 直前の #293 追加対応では、`deps` の logical-reference CTE に C# 限定で `symbol_name || 'Attribute'` のサフィックス別名を UNION することで `[Foo]` の attribute consumer が `FooAttribute` クラスファイルへ到達するようにし、`impact`(`GetFileDependencyHintsToResolvedType`)も寄与参照に `attribute` / `annotation` が含まれていれば structured-evidence guard をバイパスするようにしていたが、どちらも広すぎた。`deps` 側では別名が純粋に名前だけで join していたため、`[Foo]` が同名の関数・プロパティ・変数(例: 無関係なヘルパーの `void MyAuditAttribute()`)を定義するファイルにも依存を張ってしまっていた。`impact` 側の metadata bypass も、対象名が一意に class-like 定義を指すかを確認していなかったため、`MyAuditAttribute` のような短い名前が複数の namespace に存在すると rename / remove の blast-radius が過剰報告されていた(`attribute` / `annotation` 参照行は短い名前しか保持できず曖昧性解消ができない)。`DbReader.GetFileDependencies` は合成された別名行に `is_attribute_alias = 1`(本物の行は `0`)をタグ付けし、`source_name_counts` では `MIN` でフラグを伝播し、`edges` CTE の join で別名 edge をターゲット側 kind が `class` / `struct` / `interface` のときだけに制限するようにした。`target_files` CTE も `DISTINCT (path, lang, name, kind)` から `GROUP BY (path, lang, name)` + `MAX(...)` の `has_class_like_kind` フラグに切り替えたため、class と同名関数(例: C# コンストラクタ `Target()` と `class Target`)を同じファイルで定義しているケースでもターゲット行が 2 本に増えず `deps` の参照数を水増ししない。`DbReader.GetFileDependencyHintsToResolvedType` は新ヘルパー `IsMetadataTargetUnambiguous(definition)` を呼び、graph 対応言語横断で同名の class-like 定義数を数え、その数が 1 以下のときに限って metadata evidence-guard bypass を適用する。名前が曖昧なときは metadata 単独 edge も通常の `SourceFileHasStructuredTypeEvidence` に戻り、純 `[MyAudit]` consumer は誤帰属せずに落ちる。既存の C# attribute deps テスト `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`、曖昧ではない単一定義の `impact` テスト、および既存の graph-queries dedupe テストは全てグリーンのまま。回帰テストとして `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets`(同名関数があっても虚偽 edge が出ないこと)、`DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget`(namespace 違いで `MyAuditAttribute` が複数ある場合に metadata heuristic hint を抑止)、`DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous`(単一クラス定義のみのときは従来どおり `[MyAudit]` consumer を浮かせる)を追加。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 +- **`deps` の C# attribute サフィックス別名を class-like ターゲットに限定し、`impact` の metadata bypass も class-like ターゲット名が名前空間をまたいで曖昧な場合は抑止するよう修正 (#293 追加対応)** — 直前の #293 追加対応では、`deps` の logical-reference CTE に C# 限定で `symbol_name || 'Attribute'` のサフィックス別名を UNION することで `[Foo]` の attribute consumer が `FooAttribute` クラスファイルへ到達するようにし、`impact`(`GetFileDependencyHintsToResolvedType`)も寄与参照に `attribute` / `annotation` が含まれていれば structured-evidence guard をバイパスするようにしていたが、どちらも広すぎた。`deps` 側では別名が純粋に名前だけで join していたため、`[Foo]` が同名の関数・プロパティ・変数(例: 無関係なヘルパーの `void MyAuditAttribute()`)を定義するファイルにも依存を張ってしまっていた。`impact` 側の metadata bypass も、対象名が一意に class-like 定義を指すかを確認していなかったため、`MyAuditAttribute` のような短い名前が複数の namespace に存在すると rename / remove の blast-radius が過剰報告されていた(`attribute` / `annotation` 参照行は短い名前しか保持できず曖昧性解消ができない)。`DbReader.GetFileDependencies` は合成された別名行に `is_attribute_alias = 1`(本物の行は `0`)をタグ付けし、`source_name_counts` では `MIN` でフラグを伝播し、`edges` CTE の join で別名 edge をターゲット側 kind が `class` / `struct` / `interface` のときだけに制限するようにした。`target_files` CTE も `DISTINCT (path, lang, name, kind)` から `GROUP BY (path, lang, name)` + `MAX(...)` の `has_class_like_kind` フラグに切り替えたため、class と同名関数(例: C# コンストラクタ `Target()` と `class Target`)を同じファイルで定義しているケースでもターゲット行が 2 本に増えず `deps` の参照数を水増ししない。`DbReader.GetFileDependencyHintsToResolvedType` は新ヘルパー `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` を呼び、*active な impact スコープ内* で同名の class-like 定義数を数える(graph 対応言語に加えて、呼び出し側の `--lang` / `--path` / `--exclude-path` / `--exclude-tests` を同じフィルタで適用)。in-scope でその数が 1 以下のときに限り metadata evidence-guard bypass を適用する。スコープ内で曖昧なときは metadata 単独 edge も通常の `SourceFileHasStructuredTypeEvidence` に戻り、純 `[MyAudit]` consumer は誤帰属せずに落ちる。スコープ尊重が重要: 別言語 / 別 path / test ツリーにある同名クラスが C# の metadata bypass を潰してはならない(参照側が既に graph 対応 join で言語修飾されているため、曖昧性は scope 内で判定すべき)。README の `impact` 契約も「active な `--lang` / `--path` / `--exclude-path` / `--exclude-tests` スコープ」と明記している。既存の C# attribute deps テスト `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`、曖昧ではない単一定義の `impact` テスト、および既存の graph-queries dedupe テストは全てグリーンのまま。回帰テストとして `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets`(同名関数があっても虚偽 edge が出ないこと)、`DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget`(impact スコープ内で `MyAuditAttribute` が複数ある場合に metadata heuristic hint を抑止)、`DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous`(単一クラス定義のみのときは従来どおり `[MyAudit]` consumer を浮かせる)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope`(別言語の同名 annotation は `--lang csharp` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope`(スコープ外 `src/B/` の同名クラスは `src/A/` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests`(test ツリーの同名クラスは `--exclude-tests` クエリを潰さない)を追加。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 - **MCP の `callers` / `callees` の `kind` schema・CLI help・README の `--kind` オプション行が metadata kinds を有効フィルタ値として広告しないよう修正 (#293 追加対応)** — `callers` / `callees` の handler は `kind: "attribute"` / `kind: "annotation"` を usage error(CLI)/ `isError` ツールレスポンス(MCP)で拒否するにもかかわらず、MCP ツール schema の `kind` プロパティ説明は 5 種すべてを列挙した `"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"` のままで、`cdidx --help` の `--kind` 行も `callers` / `callees` を `references` と同じ行にまとめていた。さらに README の `--kind` オプション表(英語 436 行目 / 日本語 1307 行目)も、`callers` / `callees` で `--kind attribute` / `--kind annotation` を渡せるかのように案内していた。schema と handler の契約を一致させ、`callers` / `callees` の `kind` description を `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."` に修正し、CLI の `--kind` help 行も `references`(5 種すべて受理)と `callers` / `callees`(call-graph 種別のみ、metadata kinds は拒否して `references` へ誘導)を分けるようにし、README の `--kind` 行(英語・日本語の両方)も `callers` / `callees` が `--kind attribute|annotation` を usage error で拒否すること、metadata 列挙の正規経路が `references --kind attribute|annotation` であることを明示する文言に直した。MCP の `references` ツール説明は全 reference kind を受け付ける契約のままなので変更なし。修正後の schema 文言を固定する `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` を追加。対象: `src/CodeIndex/Mcp/McpToolDefinitions.cs`、`src/CodeIndex/Cli/ConsoleUi.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`、`README.md`。 - **`deps`・`impact`・`references`・`inspect`・`analyze_symbol` が metadata 参照を compile-time 依存として保持し、C# の attribute サフィックス命名規約を正規化 (#293 追加対応)** — `cdidx deps` の実装である `DbReader.GetFileDependencies` と、`cdidx impact` / MCP `impact_analysis` の heuristic file-hint フォールバックである `DbReader.GetFileDependencyHintsToResolvedType` は、logical reference CTE に metadata 参照も含める。attribute クラスを rename / 削除すれば `[JsonConverterAttribute]` のような注釈サイトも compile-time で壊れるため、ファイル単位の blast-radius 分析はそれらも本物の依存 edge として表示する必要があり、`logical_reference_kind` では `attribute` / `annotation` として正しくラベル付けされる(分類修正前は同じサイトが phantom `call` として漏れていた)。さらに `GetFileDependencies` は C# の attribute 命名規約を正規化する: 慣用的な `[Foo]` は `symbol_name = "Foo"` で保存されるが、定義クラスは `FooAttribute` 命名となるため、logical-reference CTE に C# `attribute` 種別の行をサフィックス `'Attribute'` を付与した別名コピーとして UNION ALL で追加する(元の symbol_name が既に `Attribute` で終わっている場合は重複しないようスキップ)。これにより target ファイル join が attribute クラス側のファイルへ到達する。`GetFileDependencyHintsToResolvedType` はさらに candidate ごとに `has_metadata_ref` フラグを集約し、寄与する参照が metadata だけである edge については `SourceFileHasStructuredTypeEvidence` guard をバイパスするため、`[MyAudit] class Svc` のような純 attribute consumer ファイルも `impact MyAuditAttribute` で出るようになった(以前は filter で落ちていた)。`ResolveImpactFallbackNames` も対称的に、`Attribute` で終わる C# クラス名にはサフィックスを外した別名を追加するため、BFS を使わない file-hint 経路でも source symbol_name が定義クラス名と一致しない `[Foo]` サイトを発見できる。同じ正規化をシンボル単位の参照 reader にも伝播する: `references`・`inspect`・MCP `analyze_symbol` を支える `DbReader.SearchReferences`・`CountSearchReferences`・`CountSearchReferencesTotal` は、新しいヘルパー `ComputeCSharpAttributeSuffixAlias` で C# 限定の `queryAttributeAlias` を計算し、exact(folded と NOCASE)・substring の両方の WHERE 句に OR で追加する。これにより `references MyAuditAttribute`(および `inspect MyAuditAttribute` / `analyze_symbol MyAuditAttribute`)でも慣用的な `[MyAudit]` 参照サイトが `deps` / `impact` と一貫して surface する。substring モードでは alias を二つ目の LIKE にせず exact-OR で束ねているため、ユーザーが `MyAuditAttribute` と打った時に `FooAuditLog` のような無関係な名前に over-match しない。alias は C# 限定なので、Java / Kotlin / Scala / TypeScript の annotation サイトには影響しない。`callers` / `callees` は共有の call-graph 種別 (`call` / `instantiate` / `subscribe`) に絞って `--kind attribute|annotation` を CLI / MCP 境界で拒否する契約なので、alias は意図的に適用しない(dynamic call graph と dependency graph は別契約)。さらに alias 節が誤一致を生まないようスコープする: `ComputeCSharpAttributeSuffixAlias` は `referenceKind` が `attribute` 以外の call-graph 種別のときは `null` を返す(`references FooAttribute --kind call` は厳格一致のままで `Foo()` に一致しない)、3 つの reader メソッドの SQL alias 節は `f.lang = 'csharp' AND r.reference_kind = 'attribute'` に限定するため、`--lang` / `--kind` 無指定の `references FooAttribute` でも Java の annotation や C# の call 行を拾わない。suffix 検出も `OrdinalIgnoreCase` にして case-insensitive 化したため、`references myauditattribute` / `inspect MyAuditATTRIBUTE` のような小文字・混在ケースでも alias `MyAudit` を生成でき、周辺の NOCASE / folded 契約と整合する。回帰テスト `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies`(runtime と metadata の両方の edge が出ることを固定)、`DbReaderTests.GetFileDependencies_MatchesCSharpAttributeSuffixConvention`(`deps` 経由での `[MyAudit]` → `MyAuditAttribute` 正規化)、`DbReaderTests.SearchReferences_MatchesCSharpAttributeSuffixConvention_Substring` / `_Exact` / `_CSharpAttributeSuffixAliasDoesNotBleedToOtherLanguages`(`references` / `inspect` / `analyze_symbol` での同一正規化と C# 限定スコープ)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_NotAppliedToCallKind`(`--kind call` で alias が発火しないこと)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_UnscopedLangStillLimitsToCSharpAttributeRows`(無指定でも Java annotation や C# call 行に一致しないこと)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_CaseInsensitiveQuery`(小文字・混在ケースのクエリでも alias を生成)を追加。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 - **`callers` / `callees` が CLI と MCP 境界で `--kind attribute|annotation` を拒否し、Swift / Gradle の `@` metadata を `annotation` に再分類 (#293 追加対応)** — metadata 行 (`attribute` / `annotation`) は注釈対象そのものではなく body-range の外側シンボルに帰属するため、`callers Obsolete --kind attribute` は `[Obsolete] void M()` を `M` ではなく外側クラスの下に黙って返し、`[assembly: CLSCompliant]` や Kotlin の `@file:JvmName` のような file-level target は `container_name = NULL` で完全脱落していた。`QueryCommandRunner.RunCallers` / `RunCallees` は `--kind attribute` / `--kind annotation` を `CommandExitCodes.UsageError` で弾き、MCP の `callers` / `callees` ツールは `isError` ツールレスポンスを返すようにした。どちらの経路も、metadata 列挙の正しいパスである `references --kind attribute|annotation` へ誘導する。あわせて `AnnotationLanguages` に `swift` と `gradle` を追加し、Swift の `@available(iOS 13.0, *)` / `@objc` / `@MainActor` や Gradle / Groovy の `@CompileStatic` / `@TaskAction` を `call` ではなく `annotation` として記録するようにした(修正前は引数付きの `@Name(args)` 形は `callers` / `callees` / `hotspots` / `impact` に phantom call edge として漏れ、引数なしの `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` 形はこの集合で gate されていた no-arg annotation 出力から外れてインデックス自体に残らなかった)。回帰テストとして `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError`(theory 4 ケース)、`McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError`(theory 4 ケース)、`ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`、`ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation` を追加。対象: `src/CodeIndex/Cli/QueryCommandRunner.cs`、`src/CodeIndex/Mcp/McpToolHandlers.cs`、`src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 diff --git a/src/CodeIndex/Database/DbReader.cs b/src/CodeIndex/Database/DbReader.cs index 87d88a8e8a..b0a878615d 100644 --- a/src/CodeIndex/Database/DbReader.cs +++ b/src/CodeIndex/Database/DbReader.cs @@ -1876,7 +1876,7 @@ FROM symbol_references r // する場合、`[MyAudit]` 参照をどちらの target にも一意に紐付けられない。 // そのような曖昧なケースでは metadata の evidence bypass を行わず、 // `impact` が rename / 削除の影響範囲を過大報告しないようにする。 - var metadataBypassSafe = IsMetadataTargetUnambiguous(definition); + var metadataBypassSafe = IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests); var evidenceCache = new Dictionary(); var filtered = new List(); foreach (var candidate in candidates) @@ -1921,21 +1921,52 @@ FROM symbol_references r // namespace / package を跨いで同名の class-like 定義が複数ある曖昧なケースでは // attribute / annotation 参照行が短縮名しか持たず区別できないため、metadata の // evidence bypass を許可しない。 - private bool IsMetadataTargetUnambiguous(SymbolResult definition) + private bool IsMetadataTargetUnambiguous( + SymbolResult definition, + string? lang, + IReadOnlyList? pathPatterns, + IReadOnlyList? excludePathPatterns, + bool excludeTests) { if (string.IsNullOrWhiteSpace(definition.Name)) return false; using var cmd = _conn.CreateCommand(); var supportedLangFilter = BuildGraphSupportedLanguagePredicate(cmd, "f", "metadataAmbigLang"); - cmd.CommandText = $@" + var sql = $@" SELECT COUNT(*) FROM ( SELECT DISTINCT f.path FROM symbols s JOIN files f ON s.file_id = f.id WHERE s.name = @metadataAmbigName COLLATE NOCASE AND s.kind IN ('class','struct','interface') - AND {supportedLangFilter} - )"; + AND {supportedLangFilter}"; + if (lang != null) + { + sql += " AND f.lang = @metadataAmbigLangFilter"; + cmd.Parameters.AddWithValue("@metadataAmbigLangFilter", lang); + } + if (pathPatterns is { Count: > 0 }) + { + var ors = new List(pathPatterns.Count); + for (int i = 0; i < pathPatterns.Count; i++) + { + ors.Add($"f.path LIKE @metadataAmbigPath{i} ESCAPE '\\'"); + cmd.Parameters.AddWithValue($"@metadataAmbigPath{i}", pathPatterns[i]); + } + sql += " AND (" + string.Join(" OR ", ors) + ")"; + } + if (excludePathPatterns is { Count: > 0 }) + { + for (int i = 0; i < excludePathPatterns.Count; i++) + { + sql += $" AND f.path NOT LIKE @metadataAmbigExcludePath{i} ESCAPE '\\'"; + cmd.Parameters.AddWithValue($"@metadataAmbigExcludePath{i}", excludePathPatterns[i]); + } + } + if (excludeTests) + sql += $" AND NOT {TestPathCondition}"; + sql += ")"; + cmd.CommandText = sql; cmd.Parameters.AddWithValue("@metadataAmbigName", definition.Name); var count = Convert.ToInt32(cmd.ExecuteScalar() ?? 0); return count <= 1; diff --git a/tests/CodeIndex.Tests/DbReaderTests.cs b/tests/CodeIndex.Tests/DbReaderTests.cs index dce4119cb2..c84847a794 100644 --- a/tests/CodeIndex.Tests/DbReaderTests.cs +++ b/tests/CodeIndex.Tests/DbReaderTests.cs @@ -2579,26 +2579,27 @@ public class Svc [Fact] public void GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget() { - // issue #293 review: when two classes share the `MyAuditAttribute` name across - // namespaces, a `[MyAudit]` reference row only carries the short name and - // cannot be uniquely attributed to either target. In that ambiguous case the - // `impact` metadata evidence bypass must be skipped so rename / removal blast - // radius is not over-reported. - // issue #293 レビュー指摘: namespace を跨いで同名の `MyAuditAttribute` クラスが - // 存在するとき、`[MyAudit]` 参照行は短縮名しか持たず、どちらの target にも - // 一意に紐付けられない。この曖昧なケースでは `impact` の metadata evidence - // bypass を行わず、rename / 削除の影響範囲を過大報告しないようにする。 - InsertIndexedFile("src/A/MyAuditAttribute.cs", "csharp", - """ - namespace A; + // issue #293 review: when two classes share the `MyAuditAttribute` name + // *within the active impact scope*, a `[MyAudit]` reference row only + // carries the short name and cannot be uniquely attributed to either + // target. In that ambiguous case the `impact` metadata evidence bypass + // must be skipped so rename / removal blast radius is not over-reported. + // issue #293 レビュー指摘: impact スコープ内で同名の `MyAuditAttribute` + // クラスが複数存在するとき、`[MyAudit]` 参照行は短縮名しか持たず、 + // どちらの target にも一意に紐付けられない。この曖昧なケースでは + // `impact` の metadata evidence bypass を行わず、rename / 削除の影響 + //範囲を過大報告しないようにする。 + InsertIndexedFile("src/A/Inner1/MyAuditAttribute.cs", "csharp", + """ + namespace A.Inner1; public sealed class MyAuditAttribute : System.Attribute { } """); - InsertIndexedFile("src/B/MyAuditAttribute.cs", "csharp", + InsertIndexedFile("src/A/Inner2/MyAuditAttribute.cs", "csharp", """ - namespace B; + namespace A.Inner2; public sealed class MyAuditAttribute : System.Attribute { @@ -2618,12 +2619,12 @@ public class Svc } """); - // Narrow `impact` to src/A/ so ResolveImpactDefinitions returns exactly one - // definition. Without the ambiguity guard, the metadata bypass would fabricate - // a heuristic edge even though the `[MyAudit]` target is qualifier-ambiguous. - // src/A/ に絞ることで ResolveImpactDefinitions が 1 件だけ返す状態を作る。 - // ambiguity guard が無ければ、`[MyAudit]` の target が qualifier 曖昧でも - // metadata bypass が heuristic エッジを作ってしまう。 + // Both ambiguous definitions are within the `src/A/` scope; without the + // ambiguity guard, the metadata bypass would fabricate a heuristic edge + // even though the `[MyAudit]` target is qualifier-ambiguous. + // src/A/ スコープ内に曖昧な定義が 2 件ある。ambiguity guard が無ければ、 + // `[MyAudit]` の target が qualifier 曖昧でも metadata bypass が heuristic + // エッジを作ってしまう。 var result = _reader.AnalyzeImpact( "MyAuditAttribute", maxDepth: 3, @@ -2664,6 +2665,140 @@ public class Svc Assert.Contains(result.FileImpacts, f => f.SourcePath == "src/Svc.cs" && f.TargetPath == "src/MyAuditAttribute.cs"); } + [Fact] + public void GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope() + { + // issue #293 round-11 follow-up: the ambiguity guard must honor the active + // `--lang` scope. A same-named class in an unrelated language must not + // suppress the C# metadata bypass because attribute reference rows are + // already language-qualified through the graph-supported `f.lang = 'csharp'` + // join on the reference side. + // issue #293 round-11 追加: ambiguity guard は active な `--lang` スコープを + // 尊重すべき。別言語に同名クラスが存在しても C# の metadata bypass を + // 潰してはならない — 参照側の join で既に言語修飾されているため、曖昧性は + // 言語スコープ内でのみ判定する。 + InsertIndexedFile("src/MyAuditAttribute.cs", "csharp", + """ + public sealed class MyAuditAttribute : System.Attribute + { + } + """); + InsertIndexedFile("src/Svc.cs", "csharp", + """ + [MyAudit] + public class Svc + { + } + """); + // Unrelated Java class / annotation sharing the unqualified name — must not + // affect the C#-only impact query. + // 無関係な Java 側の同名クラス / アノテーション — C# 限定の impact クエリに + // 影響してはならない。 + InsertIndexedFile("src/java/MyAuditAttribute.java", "java", + """ + package pkg; + + public @interface MyAuditAttribute { + } + """); + + var result = _reader.AnalyzeImpact("MyAuditAttribute", maxDepth: 3, limit: 20, lang: "csharp"); + + Assert.Contains(result.FileImpacts, f => f.SourcePath == "src/Svc.cs" && f.TargetPath == "src/MyAuditAttribute.cs"); + } + + [Fact] + public void GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope() + { + // issue #293 round-11 follow-up: ambiguity guard must honor `--path` + // scoping. A same-named class outside the requested path subtree should + // not suppress the bypass inside that subtree. + // issue #293 round-11 追加: ambiguity guard は `--path` スコープを尊重すべき。 + // 要求した path サブツリー外にある同名クラスが、サブツリー内の bypass を + // 潰してはならない。 + InsertIndexedFile("src/A/MyAuditAttribute.cs", "csharp", + """ + namespace A; + + public sealed class MyAuditAttribute : System.Attribute + { + } + """); + InsertIndexedFile("src/A/Svc.cs", "csharp", + """ + namespace A; + + [MyAudit] + public class Svc + { + } + """); + // Out-of-scope same-named definition in src/B/ — must not affect the + // src/A/-scoped impact query. + // スコープ外 src/B/ の同名定義 — src/A/ 限定の impact クエリに影響してはならない。 + InsertIndexedFile("src/B/MyAuditAttribute.cs", "csharp", + """ + namespace B; + + public sealed class MyAuditAttribute : System.Attribute + { + } + """); + + var result = _reader.AnalyzeImpact( + "MyAuditAttribute", + maxDepth: 3, + limit: 20, + lang: "csharp", + pathPatterns: new[] { "src/A/" }); + + Assert.Contains(result.FileImpacts, f => f.SourcePath == "src/A/Svc.cs" && f.TargetPath == "src/A/MyAuditAttribute.cs"); + } + + [Fact] + public void GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests() + { + // issue #293 round-11 follow-up: ambiguity guard must honor + // `--exclude-tests`. A same-named class only present in tests should not + // suppress the bypass when the caller has already excluded tests from the + // impact scope. + // issue #293 round-11 追加: ambiguity guard は `--exclude-tests` を尊重すべき。 + // test 配下にしか存在しない同名定義が、test を除外した impact クエリの + // bypass を潰してはならない。 + InsertIndexedFile("src/MyAuditAttribute.cs", "csharp", + """ + public sealed class MyAuditAttribute : System.Attribute + { + } + """); + InsertIndexedFile("src/Svc.cs", "csharp", + """ + [MyAudit] + public class Svc + { + } + """); + // Test-only same-named definition — must be filtered out when the caller + // passes excludeTests=true so the bypass stays active in the source scope. + // test 配下にしかない同名定義 — excludeTests=true のときはスコープ外になり、 + // source 側の bypass を維持すべき。 + InsertIndexedFile("tests/CodeIndex.Tests/MyAuditAttributeTests.cs", "csharp", + """ + public sealed class MyAuditAttribute : System.Attribute + { + } + """); + + var result = _reader.AnalyzeImpact( + "MyAuditAttribute", + maxDepth: 3, + limit: 20, + lang: "csharp", + excludeTests: true); + + Assert.Contains(result.FileImpacts, f => f.SourcePath == "src/Svc.cs" && f.TargetPath == "src/MyAuditAttribute.cs"); + } + [Fact] public void GetGroupedSymbolHotspots_CollapsesDuplicateNamesWithoutBareJoinInflation() { From 2913c6152e0930409bbf265e73564cf3f886ce3c Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 18 Apr 2026 14:07:12 +0900 Subject: [PATCH 22/30] Fix #293 limit C# attribute suffix alias in ResolveImpactFallbackNames to the resolved definition Round 12 adversarial review finding: ResolveImpactFallbackNames was applying the `Attribute`-suffix strip to every same-file fallback name collected for the resolved definition, not to the definition's own name. A file that defines `FooAttribute` with a nested `BarAttribute` therefore caused `impact FooAttribute` to alias `Bar` and claim `[Bar]` use sites as part of `FooAttribute`'s blast radius, even though `references FooAttribute --kind attribute` returned zero rows. - DbReader.ResolveImpactFallbackNames now restricts the C# `Attribute` suffix alias to `definition.Name` itself. Same-file siblings such as nested `*Attribute` classes no longer contribute spurious aliases. - Adds DbReaderTests. GetFileDependencyHints_CSharpAttributeSuffixAlias_DoesNotLeakToSameFileSiblings to pin the behavior. --- CHANGELOG.md | 4 +-- src/CodeIndex/Database/DbReader.cs | 30 ++++++++++--------- tests/CodeIndex.Tests/DbReaderTests.cs | 41 ++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 632f1bcfcb..a57979815c 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/), - **Developer Guide now distinguishes SQLite's role from "zero dependencies" and labels release numbers as examples** — Clarified that cdidx remains a zero-configuration, single-file CLI with exactly one production dependency (`Microsoft.Data.Sqlite`), and made the release-workflow checklist explicitly state that `1.9.0` is only an example version to substitute during an actual release. Affected: `DEVELOPER_GUIDE.md`. #### Fixed -- **`deps` C# attribute suffix alias now only matches class-like targets, and `impact` metadata bypass is suppressed when the class-like target name is ambiguous across namespaces (#293 follow-up)** — The earlier `#293` follow-up made `deps` UNION a C#-only `symbol_name || 'Attribute'` suffix alias into the logical-reference CTE so `[Foo]` attribute consumers could reach `FooAttribute` class files, and made `impact` (`GetFileDependencyHintsToResolvedType`) bypass the structured-evidence guard whenever any contributing reference was `attribute` / `annotation`. Both changes were too broad. On `deps` the alias joined purely by name, so `[Foo]` would also depend on any file that merely defined a function / property / variable named `FooAttribute` (e.g. `void MyAuditAttribute()` in an unrelated helper). On `impact` the metadata bypass did not check whether the target name was uniquely a class-like definition, so when the same unqualified name (e.g. `MyAuditAttribute`) existed in multiple namespaces the rename/remove blast radius was over-reported — attribute / annotation reference rows only keep the short name and cannot be disambiguated. `DbReader.GetFileDependencies` now tags synthetic alias rows with `is_attribute_alias = 1` (real rows are `0`), propagates the flag through `source_name_counts` via `MIN`, and restricts alias edges to class-like target kinds (`class` / `struct` / `interface`) in the `edges` CTE join. The `target_files` CTE is also switched from `DISTINCT` over `(path, lang, name, kind)` to `GROUP BY (path, lang, name)` with a `MAX(...)` `has_class_like_kind` flag, so files that define both a class and a same-named function (e.g. the C# constructor `Target()` inside `class Target`) no longer fan out into two target rows and inflate the `deps` reference count. `DbReader.GetFileDependencyHintsToResolvedType` now calls a new `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` helper that counts class-like definitions with the same unqualified name *within the active impact scope* (graph-supported languages, plus the caller's `--lang` / `--path` / `--exclude-path` / `--exclude-tests` filters); the metadata evidence-guard bypass is only applied when that in-scope count is ≤ 1. If the name is ambiguous within scope, metadata-only edges fall through to the normal `SourceFileHasStructuredTypeEvidence` check, so pure `[MyAudit]` consumers are dropped instead of being mis-attributed. Scoping matters: a same-named class in an unrelated language / path / test tree must not suppress the C# metadata bypass, because attribute reference rows are already language-qualified through the graph-supported reference join, and the README contract for `impact` explicitly promises "active `--lang` / `--path` / `--exclude-path` / `--exclude-tests` scope". The C# attribute deps test `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`, the unambiguous single-definition `impact` test, and the pre-existing graph-queries dedupe test all still pass. Added `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets` (ambiguous same-name function must not produce a spurious edge), `DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget` (two `MyAuditAttribute` classes within the same impact scope suppress the metadata heuristic hint), `DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous` (single class definition still surfaces the `[MyAudit]` consumer), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope` (same-named Java annotation must not suppress a `--lang csharp` query), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope` (out-of-scope `src/B/` same-named class must not suppress a `src/A/`-scoped query), and `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests` (test-tree same-named class must not suppress an `--exclude-tests` query). Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. +- **`deps` C# attribute suffix alias now only matches class-like targets, and `impact` metadata bypass is suppressed when the class-like target name is ambiguous across namespaces (#293 follow-up)** — The earlier `#293` follow-up made `deps` UNION a C#-only `symbol_name || 'Attribute'` suffix alias into the logical-reference CTE so `[Foo]` attribute consumers could reach `FooAttribute` class files, and made `impact` (`GetFileDependencyHintsToResolvedType`) bypass the structured-evidence guard whenever any contributing reference was `attribute` / `annotation`. Both changes were too broad. On `deps` the alias joined purely by name, so `[Foo]` would also depend on any file that merely defined a function / property / variable named `FooAttribute` (e.g. `void MyAuditAttribute()` in an unrelated helper). On `impact` the metadata bypass did not check whether the target name was uniquely a class-like definition, so when the same unqualified name (e.g. `MyAuditAttribute`) existed in multiple namespaces the rename/remove blast radius was over-reported — attribute / annotation reference rows only keep the short name and cannot be disambiguated. `DbReader.GetFileDependencies` now tags synthetic alias rows with `is_attribute_alias = 1` (real rows are `0`), propagates the flag through `source_name_counts` via `MIN`, and restricts alias edges to class-like target kinds (`class` / `struct` / `interface`) in the `edges` CTE join. The `target_files` CTE is also switched from `DISTINCT` over `(path, lang, name, kind)` to `GROUP BY (path, lang, name)` with a `MAX(...)` `has_class_like_kind` flag, so files that define both a class and a same-named function (e.g. the C# constructor `Target()` inside `class Target`) no longer fan out into two target rows and inflate the `deps` reference count. `DbReader.GetFileDependencyHintsToResolvedType` now calls a new `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` helper that counts class-like definitions with the same unqualified name *within the active impact scope* (graph-supported languages, plus the caller's `--lang` / `--path` / `--exclude-path` / `--exclude-tests` filters); the metadata evidence-guard bypass is only applied when that in-scope count is ≤ 1. If the name is ambiguous within scope, metadata-only edges fall through to the normal `SourceFileHasStructuredTypeEvidence` check, so pure `[MyAudit]` consumers are dropped instead of being mis-attributed. Scoping matters: a same-named class in an unrelated language / path / test tree must not suppress the C# metadata bypass, because attribute reference rows are already language-qualified through the graph-supported reference join, and the README contract for `impact` explicitly promises "active `--lang` / `--path` / `--exclude-path` / `--exclude-tests` scope". The C# attribute deps test `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`, the unambiguous single-definition `impact` test, and the pre-existing graph-queries dedupe test all still pass. Added `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets` (ambiguous same-name function must not produce a spurious edge), `DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget` (two `MyAuditAttribute` classes within the same impact scope suppress the metadata heuristic hint), `DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous` (single class definition still surfaces the `[MyAudit]` consumer), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope` (same-named Java annotation must not suppress a `--lang csharp` query), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope` (out-of-scope `src/B/` same-named class must not suppress a `src/A/`-scoped query), and `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests` (test-tree same-named class must not suppress an `--exclude-tests` query). `ResolveImpactFallbackNames` is additionally tightened so the C# `Attribute`-suffix alias is only applied to the resolved definition's own name: previously the alias was applied to every same-file fallback name, so a nested `BarAttribute` inside the file that defines `FooAttribute` caused `impact FooAttribute` to falsely attribute `[Bar]` use sites to `FooAttribute`'s blast radius. Added `DbReaderTests.GetFileDependencyHints_CSharpAttributeSuffixAlias_DoesNotLeakToSameFileSiblings` to pin this. Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. - **MCP `callers` / `callees` `kind` schema, CLI help, and README `--kind` option row no longer advertise metadata kinds as valid filter values (#293 follow-up)** — The `callers` / `callees` handlers reject `kind: "attribute"` / `kind: "annotation"` with a usage error (CLI) / `isError` tool response (MCP), but the MCP tool schema's `kind` property description still listed all five kinds as if they were valid filter values (`"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"`), `cdidx --help` similarly lumped `callers` / `callees` together with `references` for the `--kind` help row, and the README `--kind` option table (EN line 436 / JP line 1307) still directed readers to pass `--kind attribute` or `--kind annotation` on `callers` / `callees`. Schema and handler now agree: the `callers` / `callees` `kind` description now reads `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."`, the CLI `--kind` help row splits `references` (accepts all five kinds) from `callers` / `callees` (call-graph kinds only, metadata rejected, redirects to `references`), and the README `--kind` row (both English and Japanese) now explicitly states that `callers` / `callees` reject `--kind attribute|annotation` with a usage error and points to `references --kind attribute|annotation` as the correct metadata-enumeration path. The MCP `references` tool description is unchanged because it still accepts every indexed kind. Added `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` to pin the corrected schema wording. Affected: `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `src/CodeIndex/Cli/ConsoleUi.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `README.md`. - **`deps`, `impact`, `references`, `inspect`, and `analyze_symbol` retain metadata references as compile-time dependency edges and canonicalize the C# attribute-suffix convention (#293 follow-up)** — `DbReader.GetFileDependencies` (backing `cdidx deps`) and `DbReader.GetFileDependencyHintsToResolvedType` (backing `cdidx impact` / MCP `impact_analysis` heuristic file-hint fallback) keep metadata references in their logical reference CTE. Renaming or removing an attribute class still breaks annotated sites such as `[JsonConverterAttribute]` at compile time, so file-level blast-radius analysis must surface those edges as real dependencies — now labeled `attribute` / `annotation` in `logical_reference_kind` (previously these same sites leaked in as phantom `call` rows before classification was corrected). `GetFileDependencies` additionally canonicalizes the C# attribute naming convention: idiomatic `[Foo]` usage is stored with `symbol_name = "Foo"` but the defining class is `FooAttribute`, so the logical-reference CTE now UNION-ALLs a suffix-aliased copy of every C# `attribute`-kind row with `'Attribute'` appended (only when the original symbol_name does not already end in `Attribute`) so the target-file join reaches the attribute class file. `GetFileDependencyHintsToResolvedType` also aggregates a per-candidate `has_metadata_ref` flag and bypasses the `SourceFileHasStructuredTypeEvidence` guard for edges whose contributing references are purely metadata, so pure-attribute consumer files such as `[MyAudit] class Svc` now surface through `impact MyAuditAttribute` instead of being filtered out. `ResolveImpactFallbackNames` symmetrically adds suffix-stripped aliases for C# class names ending in `Attribute`, so the BFS-less file-hint path can find `[Foo]` sites whose source symbol_name does not match the defining class name. The same canonicalization is propagated to the symbol-level reference reader: `DbReader.SearchReferences`, `CountSearchReferences`, and `CountSearchReferencesTotal` (backing `references`, `inspect`, MCP `analyze_symbol`) now compute a C#-only `queryAttributeAlias` (defined in the new `ComputeCSharpAttributeSuffixAlias` helper) and OR it into both exact (folded and NOCASE) and substring WHERE clauses, so `references MyAuditAttribute` (and `inspect MyAuditAttribute` / `analyze_symbol MyAuditAttribute`) surface the idiomatic `[MyAudit]` call site consistently with `deps` / `impact`. Substring mode uses an exact-OR for the alias (not a second LIKE) so user queries like `MyAuditAttribute` do not over-match unrelated names such as `FooAuditLog`. The alias is C# only, so Java / Kotlin / Scala / TypeScript annotation sites remain unaffected. `callers` / `callees` intentionally skip the alias because they filter to the shared call-graph kinds (`call`, `instantiate`, `subscribe`) and reject `--kind attribute|annotation` at the CLI / MCP boundary — those commands model the dynamic call graph, not the dependency graph. The alias branch is additionally scoped so it cannot produce false positives: `ComputeCSharpAttributeSuffixAlias` returns `null` when `referenceKind` is a non-`attribute` call-graph kind (so `references FooAttribute --kind call` stays strict and does not match `Foo()`), and the SQL alias disjunct in all three reader methods is clamped to `f.lang = 'csharp' AND r.reference_kind = 'attribute'` so an unscoped `references FooAttribute` (no `--lang` / no `--kind`) still cannot bleed into Java annotations or C# call rows. Suffix detection is also case-insensitive (`OrdinalIgnoreCase`) so `references myauditattribute` and `inspect MyAuditATTRIBUTE` still produce the `MyAudit` alias, consistent with the NOCASE / folded contract of the surrounding exact / substring query paths. Added regression coverage `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies` (runtime + metadata edges both surface), `DbReaderTests.GetFileDependencies_MatchesCSharpAttributeSuffixConvention` (`[MyAudit]` → `MyAuditAttribute` canonicalization through `deps`), `DbReaderTests.SearchReferences_MatchesCSharpAttributeSuffixConvention_Substring` / `_Exact` / `_CSharpAttributeSuffixAliasDoesNotBleedToOtherLanguages` (same canonicalization through `references` / `inspect` / `analyze_symbol`, plus C#-only scope guard), `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_NotAppliedToCallKind` (alias must not fire under `--kind call`), `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_UnscopedLangStillLimitsToCSharpAttributeRows` (unscoped query must not match Java annotations or C# call rows), and `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_CaseInsensitiveQuery` (lowercase / mixed-case queries still produce the alias). Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. - **`callers` / `callees` now reject `--kind attribute|annotation` at the CLI and MCP boundary, and Swift / Gradle `@` metadata is reclassified as `annotation` (#293 follow-up)** — Metadata rows (`attribute` / `annotation`) are attributed to their enclosing body-range symbol rather than the annotated target, so `callers Obsolete --kind attribute` would silently return `[Obsolete] void M()` under the enclosing class (not `M`), and file-level targets such as `[assembly: CLSCompliant]` or Kotlin `@file:JvmName` would drop entirely because `container_name` is `NULL`. `QueryCommandRunner.RunCallers` / `RunCallees` now reject `--kind attribute` and `--kind annotation` with `CommandExitCodes.UsageError`, and the MCP `callers` / `callees` tools return an `isError` tool response; both paths direct users to `references --kind attribute|annotation`, which IS a correct metadata enumeration path. Separately, `AnnotationLanguages` now includes `swift` and `gradle`, so Swift `@available(iOS 13.0, *)` / `@objc` / `@MainActor` and Gradle/Groovy `@CompileStatic` / `@TaskAction` are recorded as `annotation` instead of `call` (previously the `@Name(args)` forms leaked into `callers` / `callees` / `hotspots` / `impact` as phantom call edges, and the no-arg `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` forms dropped from the index entirely because no-arg annotation emission was gated to languages in this set). Added `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError` (theory, 4 cases), `McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError` (theory, 4 cases), `ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`, and `ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation`. Affected: `src/CodeIndex/Cli/QueryCommandRunner.cs`, `src/CodeIndex/Mcp/McpToolHandlers.cs`, `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. @@ -686,7 +686,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **DEVELOPER_GUIDE が SQLite の位置付けとリリース番号の例示を明確化** — cdidx が「設定ゼロ・単一ファイル・本番依存1個」の CLI であり、その唯一の本番依存が `Microsoft.Data.Sqlite` であることを明記した。あわせて、リリース手順のチェックリスト中に出てくる `1.9.0` は固定値ではなく例示であり、実際のリリース番号へ読み替える前提だと明示した。対象: `DEVELOPER_GUIDE.md`。 #### 修正 -- **`deps` の C# attribute サフィックス別名を class-like ターゲットに限定し、`impact` の metadata bypass も class-like ターゲット名が名前空間をまたいで曖昧な場合は抑止するよう修正 (#293 追加対応)** — 直前の #293 追加対応では、`deps` の logical-reference CTE に C# 限定で `symbol_name || 'Attribute'` のサフィックス別名を UNION することで `[Foo]` の attribute consumer が `FooAttribute` クラスファイルへ到達するようにし、`impact`(`GetFileDependencyHintsToResolvedType`)も寄与参照に `attribute` / `annotation` が含まれていれば structured-evidence guard をバイパスするようにしていたが、どちらも広すぎた。`deps` 側では別名が純粋に名前だけで join していたため、`[Foo]` が同名の関数・プロパティ・変数(例: 無関係なヘルパーの `void MyAuditAttribute()`)を定義するファイルにも依存を張ってしまっていた。`impact` 側の metadata bypass も、対象名が一意に class-like 定義を指すかを確認していなかったため、`MyAuditAttribute` のような短い名前が複数の namespace に存在すると rename / remove の blast-radius が過剰報告されていた(`attribute` / `annotation` 参照行は短い名前しか保持できず曖昧性解消ができない)。`DbReader.GetFileDependencies` は合成された別名行に `is_attribute_alias = 1`(本物の行は `0`)をタグ付けし、`source_name_counts` では `MIN` でフラグを伝播し、`edges` CTE の join で別名 edge をターゲット側 kind が `class` / `struct` / `interface` のときだけに制限するようにした。`target_files` CTE も `DISTINCT (path, lang, name, kind)` から `GROUP BY (path, lang, name)` + `MAX(...)` の `has_class_like_kind` フラグに切り替えたため、class と同名関数(例: C# コンストラクタ `Target()` と `class Target`)を同じファイルで定義しているケースでもターゲット行が 2 本に増えず `deps` の参照数を水増ししない。`DbReader.GetFileDependencyHintsToResolvedType` は新ヘルパー `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` を呼び、*active な impact スコープ内* で同名の class-like 定義数を数える(graph 対応言語に加えて、呼び出し側の `--lang` / `--path` / `--exclude-path` / `--exclude-tests` を同じフィルタで適用)。in-scope でその数が 1 以下のときに限り metadata evidence-guard bypass を適用する。スコープ内で曖昧なときは metadata 単独 edge も通常の `SourceFileHasStructuredTypeEvidence` に戻り、純 `[MyAudit]` consumer は誤帰属せずに落ちる。スコープ尊重が重要: 別言語 / 別 path / test ツリーにある同名クラスが C# の metadata bypass を潰してはならない(参照側が既に graph 対応 join で言語修飾されているため、曖昧性は scope 内で判定すべき)。README の `impact` 契約も「active な `--lang` / `--path` / `--exclude-path` / `--exclude-tests` スコープ」と明記している。既存の C# attribute deps テスト `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`、曖昧ではない単一定義の `impact` テスト、および既存の graph-queries dedupe テストは全てグリーンのまま。回帰テストとして `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets`(同名関数があっても虚偽 edge が出ないこと)、`DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget`(impact スコープ内で `MyAuditAttribute` が複数ある場合に metadata heuristic hint を抑止)、`DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous`(単一クラス定義のみのときは従来どおり `[MyAudit]` consumer を浮かせる)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope`(別言語の同名 annotation は `--lang csharp` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope`(スコープ外 `src/B/` の同名クラスは `src/A/` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests`(test ツリーの同名クラスは `--exclude-tests` クエリを潰さない)を追加。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 +- **`deps` の C# attribute サフィックス別名を class-like ターゲットに限定し、`impact` の metadata bypass も class-like ターゲット名が名前空間をまたいで曖昧な場合は抑止するよう修正 (#293 追加対応)** — 直前の #293 追加対応では、`deps` の logical-reference CTE に C# 限定で `symbol_name || 'Attribute'` のサフィックス別名を UNION することで `[Foo]` の attribute consumer が `FooAttribute` クラスファイルへ到達するようにし、`impact`(`GetFileDependencyHintsToResolvedType`)も寄与参照に `attribute` / `annotation` が含まれていれば structured-evidence guard をバイパスするようにしていたが、どちらも広すぎた。`deps` 側では別名が純粋に名前だけで join していたため、`[Foo]` が同名の関数・プロパティ・変数(例: 無関係なヘルパーの `void MyAuditAttribute()`)を定義するファイルにも依存を張ってしまっていた。`impact` 側の metadata bypass も、対象名が一意に class-like 定義を指すかを確認していなかったため、`MyAuditAttribute` のような短い名前が複数の namespace に存在すると rename / remove の blast-radius が過剰報告されていた(`attribute` / `annotation` 参照行は短い名前しか保持できず曖昧性解消ができない)。`DbReader.GetFileDependencies` は合成された別名行に `is_attribute_alias = 1`(本物の行は `0`)をタグ付けし、`source_name_counts` では `MIN` でフラグを伝播し、`edges` CTE の join で別名 edge をターゲット側 kind が `class` / `struct` / `interface` のときだけに制限するようにした。`target_files` CTE も `DISTINCT (path, lang, name, kind)` から `GROUP BY (path, lang, name)` + `MAX(...)` の `has_class_like_kind` フラグに切り替えたため、class と同名関数(例: C# コンストラクタ `Target()` と `class Target`)を同じファイルで定義しているケースでもターゲット行が 2 本に増えず `deps` の参照数を水増ししない。`DbReader.GetFileDependencyHintsToResolvedType` は新ヘルパー `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` を呼び、*active な impact スコープ内* で同名の class-like 定義数を数える(graph 対応言語に加えて、呼び出し側の `--lang` / `--path` / `--exclude-path` / `--exclude-tests` を同じフィルタで適用)。in-scope でその数が 1 以下のときに限り metadata evidence-guard bypass を適用する。スコープ内で曖昧なときは metadata 単独 edge も通常の `SourceFileHasStructuredTypeEvidence` に戻り、純 `[MyAudit]` consumer は誤帰属せずに落ちる。スコープ尊重が重要: 別言語 / 別 path / test ツリーにある同名クラスが C# の metadata bypass を潰してはならない(参照側が既に graph 対応 join で言語修飾されているため、曖昧性は scope 内で判定すべき)。README の `impact` 契約も「active な `--lang` / `--path` / `--exclude-path` / `--exclude-tests` スコープ」と明記している。既存の C# attribute deps テスト `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`、曖昧ではない単一定義の `impact` テスト、および既存の graph-queries dedupe テストは全てグリーンのまま。回帰テストとして `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets`(同名関数があっても虚偽 edge が出ないこと)、`DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget`(impact スコープ内で `MyAuditAttribute` が複数ある場合に metadata heuristic hint を抑止)、`DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous`(単一クラス定義のみのときは従来どおり `[MyAudit]` consumer を浮かせる)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope`(別言語の同名 annotation は `--lang csharp` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope`(スコープ外 `src/B/` の同名クラスは `src/A/` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests`(test ツリーの同名クラスは `--exclude-tests` クエリを潰さない)を追加。`ResolveImpactFallbackNames` も併せて引き締め、C# の `Attribute` サフィックス別名は *解決済み定義自身* の名前にのみ適用するようにした: 以前は同一ファイル内の fallback 名全てに別名を付けていたため、`FooAttribute` を定義するファイルに nested で `BarAttribute` が存在するだけで `impact FooAttribute` が `[Bar]` 利用サイトを `FooAttribute` の影響範囲として誤報告していた。`DbReaderTests.GetFileDependencyHints_CSharpAttributeSuffixAlias_DoesNotLeakToSameFileSiblings` を追加して固定。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 - **MCP の `callers` / `callees` の `kind` schema・CLI help・README の `--kind` オプション行が metadata kinds を有効フィルタ値として広告しないよう修正 (#293 追加対応)** — `callers` / `callees` の handler は `kind: "attribute"` / `kind: "annotation"` を usage error(CLI)/ `isError` ツールレスポンス(MCP)で拒否するにもかかわらず、MCP ツール schema の `kind` プロパティ説明は 5 種すべてを列挙した `"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"` のままで、`cdidx --help` の `--kind` 行も `callers` / `callees` を `references` と同じ行にまとめていた。さらに README の `--kind` オプション表(英語 436 行目 / 日本語 1307 行目)も、`callers` / `callees` で `--kind attribute` / `--kind annotation` を渡せるかのように案内していた。schema と handler の契約を一致させ、`callers` / `callees` の `kind` description を `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."` に修正し、CLI の `--kind` help 行も `references`(5 種すべて受理)と `callers` / `callees`(call-graph 種別のみ、metadata kinds は拒否して `references` へ誘導)を分けるようにし、README の `--kind` 行(英語・日本語の両方)も `callers` / `callees` が `--kind attribute|annotation` を usage error で拒否すること、metadata 列挙の正規経路が `references --kind attribute|annotation` であることを明示する文言に直した。MCP の `references` ツール説明は全 reference kind を受け付ける契約のままなので変更なし。修正後の schema 文言を固定する `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` を追加。対象: `src/CodeIndex/Mcp/McpToolDefinitions.cs`、`src/CodeIndex/Cli/ConsoleUi.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`、`README.md`。 - **`deps`・`impact`・`references`・`inspect`・`analyze_symbol` が metadata 参照を compile-time 依存として保持し、C# の attribute サフィックス命名規約を正規化 (#293 追加対応)** — `cdidx deps` の実装である `DbReader.GetFileDependencies` と、`cdidx impact` / MCP `impact_analysis` の heuristic file-hint フォールバックである `DbReader.GetFileDependencyHintsToResolvedType` は、logical reference CTE に metadata 参照も含める。attribute クラスを rename / 削除すれば `[JsonConverterAttribute]` のような注釈サイトも compile-time で壊れるため、ファイル単位の blast-radius 分析はそれらも本物の依存 edge として表示する必要があり、`logical_reference_kind` では `attribute` / `annotation` として正しくラベル付けされる(分類修正前は同じサイトが phantom `call` として漏れていた)。さらに `GetFileDependencies` は C# の attribute 命名規約を正規化する: 慣用的な `[Foo]` は `symbol_name = "Foo"` で保存されるが、定義クラスは `FooAttribute` 命名となるため、logical-reference CTE に C# `attribute` 種別の行をサフィックス `'Attribute'` を付与した別名コピーとして UNION ALL で追加する(元の symbol_name が既に `Attribute` で終わっている場合は重複しないようスキップ)。これにより target ファイル join が attribute クラス側のファイルへ到達する。`GetFileDependencyHintsToResolvedType` はさらに candidate ごとに `has_metadata_ref` フラグを集約し、寄与する参照が metadata だけである edge については `SourceFileHasStructuredTypeEvidence` guard をバイパスするため、`[MyAudit] class Svc` のような純 attribute consumer ファイルも `impact MyAuditAttribute` で出るようになった(以前は filter で落ちていた)。`ResolveImpactFallbackNames` も対称的に、`Attribute` で終わる C# クラス名にはサフィックスを外した別名を追加するため、BFS を使わない file-hint 経路でも source symbol_name が定義クラス名と一致しない `[Foo]` サイトを発見できる。同じ正規化をシンボル単位の参照 reader にも伝播する: `references`・`inspect`・MCP `analyze_symbol` を支える `DbReader.SearchReferences`・`CountSearchReferences`・`CountSearchReferencesTotal` は、新しいヘルパー `ComputeCSharpAttributeSuffixAlias` で C# 限定の `queryAttributeAlias` を計算し、exact(folded と NOCASE)・substring の両方の WHERE 句に OR で追加する。これにより `references MyAuditAttribute`(および `inspect MyAuditAttribute` / `analyze_symbol MyAuditAttribute`)でも慣用的な `[MyAudit]` 参照サイトが `deps` / `impact` と一貫して surface する。substring モードでは alias を二つ目の LIKE にせず exact-OR で束ねているため、ユーザーが `MyAuditAttribute` と打った時に `FooAuditLog` のような無関係な名前に over-match しない。alias は C# 限定なので、Java / Kotlin / Scala / TypeScript の annotation サイトには影響しない。`callers` / `callees` は共有の call-graph 種別 (`call` / `instantiate` / `subscribe`) に絞って `--kind attribute|annotation` を CLI / MCP 境界で拒否する契約なので、alias は意図的に適用しない(dynamic call graph と dependency graph は別契約)。さらに alias 節が誤一致を生まないようスコープする: `ComputeCSharpAttributeSuffixAlias` は `referenceKind` が `attribute` 以外の call-graph 種別のときは `null` を返す(`references FooAttribute --kind call` は厳格一致のままで `Foo()` に一致しない)、3 つの reader メソッドの SQL alias 節は `f.lang = 'csharp' AND r.reference_kind = 'attribute'` に限定するため、`--lang` / `--kind` 無指定の `references FooAttribute` でも Java の annotation や C# の call 行を拾わない。suffix 検出も `OrdinalIgnoreCase` にして case-insensitive 化したため、`references myauditattribute` / `inspect MyAuditATTRIBUTE` のような小文字・混在ケースでも alias `MyAudit` を生成でき、周辺の NOCASE / folded 契約と整合する。回帰テスト `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies`(runtime と metadata の両方の edge が出ることを固定)、`DbReaderTests.GetFileDependencies_MatchesCSharpAttributeSuffixConvention`(`deps` 経由での `[MyAudit]` → `MyAuditAttribute` 正規化)、`DbReaderTests.SearchReferences_MatchesCSharpAttributeSuffixConvention_Substring` / `_Exact` / `_CSharpAttributeSuffixAliasDoesNotBleedToOtherLanguages`(`references` / `inspect` / `analyze_symbol` での同一正規化と C# 限定スコープ)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_NotAppliedToCallKind`(`--kind call` で alias が発火しないこと)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_UnscopedLangStillLimitsToCSharpAttributeRows`(無指定でも Java annotation や C# call 行に一致しないこと)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_CaseInsensitiveQuery`(小文字・混在ケースのクエリでも alias を生成)を追加。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 - **`callers` / `callees` が CLI と MCP 境界で `--kind attribute|annotation` を拒否し、Swift / Gradle の `@` metadata を `annotation` に再分類 (#293 追加対応)** — metadata 行 (`attribute` / `annotation`) は注釈対象そのものではなく body-range の外側シンボルに帰属するため、`callers Obsolete --kind attribute` は `[Obsolete] void M()` を `M` ではなく外側クラスの下に黙って返し、`[assembly: CLSCompliant]` や Kotlin の `@file:JvmName` のような file-level target は `container_name = NULL` で完全脱落していた。`QueryCommandRunner.RunCallers` / `RunCallees` は `--kind attribute` / `--kind annotation` を `CommandExitCodes.UsageError` で弾き、MCP の `callers` / `callees` ツールは `isError` ツールレスポンスを返すようにした。どちらの経路も、metadata 列挙の正しいパスである `references --kind attribute|annotation` へ誘導する。あわせて `AnnotationLanguages` に `swift` と `gradle` を追加し、Swift の `@available(iOS 13.0, *)` / `@objc` / `@MainActor` や Gradle / Groovy の `@CompileStatic` / `@TaskAction` を `call` ではなく `annotation` として記録するようにした(修正前は引数付きの `@Name(args)` 形は `callers` / `callees` / `hotspots` / `impact` に phantom call edge として漏れ、引数なしの `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` 形はこの集合で gate されていた no-arg annotation 出力から外れてインデックス自体に残らなかった)。回帰テストとして `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError`(theory 4 ケース)、`McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError`(theory 4 ケース)、`ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`、`ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation` を追加。対象: `src/CodeIndex/Cli/QueryCommandRunner.cs`、`src/CodeIndex/Mcp/McpToolHandlers.cs`、`src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 diff --git a/src/CodeIndex/Database/DbReader.cs b/src/CodeIndex/Database/DbReader.cs index b0a878615d..feb1a84430 100644 --- a/src/CodeIndex/Database/DbReader.cs +++ b/src/CodeIndex/Database/DbReader.cs @@ -1760,23 +1760,25 @@ FROM symbols s // C# attribute naming convention: a class `FooAttribute` is used as `[Foo]` in source, // so reference sites are stored with symbol_name `Foo`. Add the suffix-stripped alias - // so impact on `FooAttribute` can find metadata-only usage sites. + // for the resolved definition itself so impact on `FooAttribute` can find metadata-only + // usage sites. Only the resolved definition's own name gets the alias — applying the + // strip to every same-file fallback name (e.g. a nested `BarAttribute` inside the file + // that defines `FooAttribute`) would let `impact FooAttribute` falsely report `[Bar]` + // usages as part of `FooAttribute`'s blast radius. // C# の属性命名規約: クラス `FooAttribute` はソースで `[Foo]` として使われ、参照サイトは // symbol_name `Foo` で保存される。`FooAttribute` への impact でも metadata 参照サイトを - // 見つけられるよう、サフィックスを外した別名を追加する。 - if (string.Equals(definition.Lang, "csharp", StringComparison.OrdinalIgnoreCase)) + // 見つけられるよう、*解決済み定義自身* にのみサフィックスを外した別名を追加する。 + // same-file fallback 名全体(例: `FooAttribute` と同一ファイルに nested で存在する + // `BarAttribute`)にまで strip を適用すると、`impact FooAttribute` が `[Bar]` 利用を + // 誤って `FooAttribute` の影響範囲として報告してしまうため、定義自身だけに限定する。 + if (string.Equals(definition.Lang, "csharp", StringComparison.OrdinalIgnoreCase) && + !string.IsNullOrEmpty(definition.Name) && + definition.Name.Length > "Attribute".Length && + definition.Name.EndsWith("Attribute", StringComparison.Ordinal)) { - var aliases = new List(); - foreach (var name in results) - { - if (name.Length > "Attribute".Length && name.EndsWith("Attribute", StringComparison.Ordinal)) - { - var stripped = name.Substring(0, name.Length - "Attribute".Length); - if (stripped.Length > 0 && !results.Contains(stripped) && !aliases.Contains(stripped)) - aliases.Add(stripped); - } - } - results.AddRange(aliases); + var stripped = definition.Name.Substring(0, definition.Name.Length - "Attribute".Length); + if (stripped.Length > 0 && !results.Contains(stripped)) + results.Add(stripped); } return results; diff --git a/tests/CodeIndex.Tests/DbReaderTests.cs b/tests/CodeIndex.Tests/DbReaderTests.cs index c84847a794..1e9d4ef7fe 100644 --- a/tests/CodeIndex.Tests/DbReaderTests.cs +++ b/tests/CodeIndex.Tests/DbReaderTests.cs @@ -2665,6 +2665,47 @@ public class Svc Assert.Contains(result.FileImpacts, f => f.SourcePath == "src/Svc.cs" && f.TargetPath == "src/MyAuditAttribute.cs"); } + [Fact] + public void GetFileDependencyHints_CSharpAttributeSuffixAlias_DoesNotLeakToSameFileSiblings() + { + // issue #293 round-12 follow-up: the C# `Attribute` suffix alias used by + // ResolveImpactFallbackNames must only be applied to the resolved + // definition's own name. If it were applied to every same-file fallback + // name (e.g. a nested `BarAttribute` inside the file that defines + // `FooAttribute`), `impact FooAttribute` would falsely claim `[Bar]` use + // sites as its own blast radius. + // issue #293 round-12 追加: ResolveImpactFallbackNames の C# `Attribute` + // suffix 別名は、解決済み定義自身の名前にだけ適用すべき。same-file + // fallback 名全体(例: `FooAttribute` と同一ファイルに nested で存在する + // `BarAttribute`)にまで strip を適用すると、`impact FooAttribute` が + // `[Bar]` 利用を自身の影響範囲として誤報告してしまう。 + InsertIndexedFile("src/FooAttribute.cs", "csharp", + """ + public sealed class FooAttribute : System.Attribute + { + public sealed class BarAttribute : System.Attribute + { + } + } + """); + // A separate file uses `[Bar]` — that must NOT show up in + // `impact FooAttribute` because it references `BarAttribute`, not + // `FooAttribute`. + // 別ファイルで `[Bar]` を使う — これは `BarAttribute` の参照であり、 + // `FooAttribute` の `impact` には出てはならない。 + InsertIndexedFile("src/UseBar.cs", "csharp", + """ + [Bar] + public class UseBar + { + } + """); + + var result = _reader.AnalyzeImpact("FooAttribute", maxDepth: 3, limit: 20, lang: "csharp"); + + Assert.DoesNotContain(result.FileImpacts, f => f.SourcePath == "src/UseBar.cs"); + } + [Fact] public void GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope() { From 9e3a43b7afa65cbcbaf23f4f54eb335165bf30cc Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 18 Apr 2026 14:28:46 +0900 Subject: [PATCH 23/30] Fix #293 restrict deps metadata edges to class-like targets and suppress raw form when suffix alias resolves The earlier #293 follow-up only restricted synthetic C# attribute-suffix alias rows to class-like targets, which left three over-reports intact on `GetFileDependencies` (`deps`): 1. `[MyAuditAttribute]` (fully-qualified) still joined to any same-named method / property in an unrelated file. 2. `[MyAudit]` (bare form) still joined to an unrelated plain `class MyAudit` when `class MyAuditAttribute` also existed, because the raw row and the synthetic alias both produced edges. 3. `[MyAudit]` still fanned out to every same-named attribute class when multiple namespaces defined `MyAuditAttribute`. Restructure the CTE so metadata rows stay separated from non-metadata call-graph rows with the same symbol_name, and add two CTEs that resolve the remaining false-positive shapes: - `logical_references` now carries an `is_metadata` flag. - `source_name_counts` groups by `is_metadata` and `is_attribute_alias` so metadata vs call-graph do not collapse. - `edges` restricts ALL metadata edges (real rows and synthetic aliases) to `tf.has_class_like_kind = 1`. - `metadata_raw_suppression` drops the raw C# `[Foo]` row when the synthetic `FooAttribute` alias has already resolved to a class-like target in the same source file. - `target_ambiguity` drops metadata edges when the target resolves to multiple class-like definitions in scope. The pre-existing `JsonConverter` test (target class named without the `Attribute` suffix, single class-like target in scope) still passes. Add regression coverage for all three shapes. Co-Authored-By: Claude Opus 4 --- CHANGELOG.md | 4 +- src/CodeIndex/Database/DbReader.cs | 101 ++++++++++++++--- tests/CodeIndex.Tests/DbReaderTests.cs | 145 +++++++++++++++++++++++++ 3 files changed, 230 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a57979815c..6f9ffbf51a 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/), - **Developer Guide now distinguishes SQLite's role from "zero dependencies" and labels release numbers as examples** — Clarified that cdidx remains a zero-configuration, single-file CLI with exactly one production dependency (`Microsoft.Data.Sqlite`), and made the release-workflow checklist explicitly state that `1.9.0` is only an example version to substitute during an actual release. Affected: `DEVELOPER_GUIDE.md`. #### Fixed -- **`deps` C# attribute suffix alias now only matches class-like targets, and `impact` metadata bypass is suppressed when the class-like target name is ambiguous across namespaces (#293 follow-up)** — The earlier `#293` follow-up made `deps` UNION a C#-only `symbol_name || 'Attribute'` suffix alias into the logical-reference CTE so `[Foo]` attribute consumers could reach `FooAttribute` class files, and made `impact` (`GetFileDependencyHintsToResolvedType`) bypass the structured-evidence guard whenever any contributing reference was `attribute` / `annotation`. Both changes were too broad. On `deps` the alias joined purely by name, so `[Foo]` would also depend on any file that merely defined a function / property / variable named `FooAttribute` (e.g. `void MyAuditAttribute()` in an unrelated helper). On `impact` the metadata bypass did not check whether the target name was uniquely a class-like definition, so when the same unqualified name (e.g. `MyAuditAttribute`) existed in multiple namespaces the rename/remove blast radius was over-reported — attribute / annotation reference rows only keep the short name and cannot be disambiguated. `DbReader.GetFileDependencies` now tags synthetic alias rows with `is_attribute_alias = 1` (real rows are `0`), propagates the flag through `source_name_counts` via `MIN`, and restricts alias edges to class-like target kinds (`class` / `struct` / `interface`) in the `edges` CTE join. The `target_files` CTE is also switched from `DISTINCT` over `(path, lang, name, kind)` to `GROUP BY (path, lang, name)` with a `MAX(...)` `has_class_like_kind` flag, so files that define both a class and a same-named function (e.g. the C# constructor `Target()` inside `class Target`) no longer fan out into two target rows and inflate the `deps` reference count. `DbReader.GetFileDependencyHintsToResolvedType` now calls a new `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` helper that counts class-like definitions with the same unqualified name *within the active impact scope* (graph-supported languages, plus the caller's `--lang` / `--path` / `--exclude-path` / `--exclude-tests` filters); the metadata evidence-guard bypass is only applied when that in-scope count is ≤ 1. If the name is ambiguous within scope, metadata-only edges fall through to the normal `SourceFileHasStructuredTypeEvidence` check, so pure `[MyAudit]` consumers are dropped instead of being mis-attributed. Scoping matters: a same-named class in an unrelated language / path / test tree must not suppress the C# metadata bypass, because attribute reference rows are already language-qualified through the graph-supported reference join, and the README contract for `impact` explicitly promises "active `--lang` / `--path` / `--exclude-path` / `--exclude-tests` scope". The C# attribute deps test `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`, the unambiguous single-definition `impact` test, and the pre-existing graph-queries dedupe test all still pass. Added `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets` (ambiguous same-name function must not produce a spurious edge), `DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget` (two `MyAuditAttribute` classes within the same impact scope suppress the metadata heuristic hint), `DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous` (single class definition still surfaces the `[MyAudit]` consumer), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope` (same-named Java annotation must not suppress a `--lang csharp` query), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope` (out-of-scope `src/B/` same-named class must not suppress a `src/A/`-scoped query), and `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests` (test-tree same-named class must not suppress an `--exclude-tests` query). `ResolveImpactFallbackNames` is additionally tightened so the C# `Attribute`-suffix alias is only applied to the resolved definition's own name: previously the alias was applied to every same-file fallback name, so a nested `BarAttribute` inside the file that defines `FooAttribute` caused `impact FooAttribute` to falsely attribute `[Bar]` use sites to `FooAttribute`'s blast radius. Added `DbReaderTests.GetFileDependencyHints_CSharpAttributeSuffixAlias_DoesNotLeakToSameFileSiblings` to pin this. Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. +- **`deps` C# attribute suffix alias now only matches class-like targets, and `impact` metadata bypass is suppressed when the class-like target name is ambiguous across namespaces (#293 follow-up)** — The earlier `#293` follow-up made `deps` UNION a C#-only `symbol_name || 'Attribute'` suffix alias into the logical-reference CTE so `[Foo]` attribute consumers could reach `FooAttribute` class files, and made `impact` (`GetFileDependencyHintsToResolvedType`) bypass the structured-evidence guard whenever any contributing reference was `attribute` / `annotation`. Both changes were too broad. On `deps` the alias joined purely by name, so `[Foo]` would also depend on any file that merely defined a function / property / variable named `FooAttribute` (e.g. `void MyAuditAttribute()` in an unrelated helper). On `impact` the metadata bypass did not check whether the target name was uniquely a class-like definition, so when the same unqualified name (e.g. `MyAuditAttribute`) existed in multiple namespaces the rename/remove blast radius was over-reported — attribute / annotation reference rows only keep the short name and cannot be disambiguated. `DbReader.GetFileDependencies` now tags synthetic alias rows with `is_attribute_alias = 1` (real rows are `0`), propagates the flag through `source_name_counts` via `MIN`, and restricts alias edges to class-like target kinds (`class` / `struct` / `interface`) in the `edges` CTE join. The `target_files` CTE is also switched from `DISTINCT` over `(path, lang, name, kind)` to `GROUP BY (path, lang, name)` with a `MAX(...)` `has_class_like_kind` flag, so files that define both a class and a same-named function (e.g. the C# constructor `Target()` inside `class Target`) no longer fan out into two target rows and inflate the `deps` reference count. `DbReader.GetFileDependencyHintsToResolvedType` now calls a new `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` helper that counts class-like definitions with the same unqualified name *within the active impact scope* (graph-supported languages, plus the caller's `--lang` / `--path` / `--exclude-path` / `--exclude-tests` filters); the metadata evidence-guard bypass is only applied when that in-scope count is ≤ 1. If the name is ambiguous within scope, metadata-only edges fall through to the normal `SourceFileHasStructuredTypeEvidence` check, so pure `[MyAudit]` consumers are dropped instead of being mis-attributed. Scoping matters: a same-named class in an unrelated language / path / test tree must not suppress the C# metadata bypass, because attribute reference rows are already language-qualified through the graph-supported reference join, and the README contract for `impact` explicitly promises "active `--lang` / `--path` / `--exclude-path` / `--exclude-tests` scope". The C# attribute deps test `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`, the unambiguous single-definition `impact` test, and the pre-existing graph-queries dedupe test all still pass. Added `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets` (ambiguous same-name function must not produce a spurious edge), `DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget` (two `MyAuditAttribute` classes within the same impact scope suppress the metadata heuristic hint), `DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous` (single class definition still surfaces the `[MyAudit]` consumer), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope` (same-named Java annotation must not suppress a `--lang csharp` query), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope` (out-of-scope `src/B/` same-named class must not suppress a `src/A/`-scoped query), and `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests` (test-tree same-named class must not suppress an `--exclude-tests` query). `ResolveImpactFallbackNames` is additionally tightened so the C# `Attribute`-suffix alias is only applied to the resolved definition's own name: previously the alias was applied to every same-file fallback name, so a nested `BarAttribute` inside the file that defines `FooAttribute` caused `impact FooAttribute` to falsely attribute `[Bar]` use sites to `FooAttribute`'s blast radius. Added `DbReaderTests.GetFileDependencyHints_CSharpAttributeSuffixAlias_DoesNotLeakToSameFileSiblings` to pin this. `GetFileDependencies` is additionally restructured so metadata rows (`attribute` / `annotation`) cannot leak onto bare-name call-graph targets: the `logical_references` CTE now carries an `is_metadata` flag, `source_name_counts` groups by `(source_file_id, source_path, source_lang, symbol_name, is_attribute_alias, is_metadata)` so metadata and non-metadata rows with the same symbol_name stay separate, the `edges` CTE restricts ALL metadata edges (not just synthetic aliases) to `tf.has_class_like_kind = 1`, and two new CTEs resolve the remaining false-positive shapes: `metadata_raw_suppression` drops the raw C# `[Foo]` row when the synthetic `FooAttribute` alias already resolves to a class-like target in the same source file (so `[Foo]` does not fan out to both `class FooAttribute` and a plain `class Foo` that shares the bare name), and `target_ambiguity` drops metadata edges when the same-named symbol resolves to multiple class-like definitions in scope (so ambiguous `[MyAudit]` usage is left to `impact` / `references` to disambiguate rather than being fanned out to every same-named attribute class). The existing `GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies` test (which pins `[JsonConverter(...)]` → `class JsonConverter : Attribute` for a target class whose name does NOT end in `Attribute`) still passes because only one class-like target exists in scope, so `target_ambiguity` keeps it, the `is_metadata` / class-like restriction accepts it, and `metadata_raw_suppression` finds no synthetic `JsonConverterAttribute` alias target and therefore does not suppress the raw row. Added `DbReaderTests.GetFileDependencies_CSharpAttributeRawDoesNotLeakToBareNameClass_WhenSuffixTargetExists` (raw `[MyAudit]` row is suppressed when the alias resolves to `class MyAuditAttribute`, so `[MyAudit]` does NOT depend on an unrelated plain `class MyAudit` file), `DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotLeakToSameNameMethodOrProperty` (fully-qualified `[MyAuditAttribute]` metadata does NOT depend on a file that only defines a method / property named `MyAuditAttribute`), and `DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotFanOutWhenMultipleSameNameAttributeClasses` (two same-named attribute classes in different namespaces cause the metadata edge to be dropped as ambiguous). Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. - **MCP `callers` / `callees` `kind` schema, CLI help, and README `--kind` option row no longer advertise metadata kinds as valid filter values (#293 follow-up)** — The `callers` / `callees` handlers reject `kind: "attribute"` / `kind: "annotation"` with a usage error (CLI) / `isError` tool response (MCP), but the MCP tool schema's `kind` property description still listed all five kinds as if they were valid filter values (`"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"`), `cdidx --help` similarly lumped `callers` / `callees` together with `references` for the `--kind` help row, and the README `--kind` option table (EN line 436 / JP line 1307) still directed readers to pass `--kind attribute` or `--kind annotation` on `callers` / `callees`. Schema and handler now agree: the `callers` / `callees` `kind` description now reads `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."`, the CLI `--kind` help row splits `references` (accepts all five kinds) from `callers` / `callees` (call-graph kinds only, metadata rejected, redirects to `references`), and the README `--kind` row (both English and Japanese) now explicitly states that `callers` / `callees` reject `--kind attribute|annotation` with a usage error and points to `references --kind attribute|annotation` as the correct metadata-enumeration path. The MCP `references` tool description is unchanged because it still accepts every indexed kind. Added `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` to pin the corrected schema wording. Affected: `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `src/CodeIndex/Cli/ConsoleUi.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `README.md`. - **`deps`, `impact`, `references`, `inspect`, and `analyze_symbol` retain metadata references as compile-time dependency edges and canonicalize the C# attribute-suffix convention (#293 follow-up)** — `DbReader.GetFileDependencies` (backing `cdidx deps`) and `DbReader.GetFileDependencyHintsToResolvedType` (backing `cdidx impact` / MCP `impact_analysis` heuristic file-hint fallback) keep metadata references in their logical reference CTE. Renaming or removing an attribute class still breaks annotated sites such as `[JsonConverterAttribute]` at compile time, so file-level blast-radius analysis must surface those edges as real dependencies — now labeled `attribute` / `annotation` in `logical_reference_kind` (previously these same sites leaked in as phantom `call` rows before classification was corrected). `GetFileDependencies` additionally canonicalizes the C# attribute naming convention: idiomatic `[Foo]` usage is stored with `symbol_name = "Foo"` but the defining class is `FooAttribute`, so the logical-reference CTE now UNION-ALLs a suffix-aliased copy of every C# `attribute`-kind row with `'Attribute'` appended (only when the original symbol_name does not already end in `Attribute`) so the target-file join reaches the attribute class file. `GetFileDependencyHintsToResolvedType` also aggregates a per-candidate `has_metadata_ref` flag and bypasses the `SourceFileHasStructuredTypeEvidence` guard for edges whose contributing references are purely metadata, so pure-attribute consumer files such as `[MyAudit] class Svc` now surface through `impact MyAuditAttribute` instead of being filtered out. `ResolveImpactFallbackNames` symmetrically adds suffix-stripped aliases for C# class names ending in `Attribute`, so the BFS-less file-hint path can find `[Foo]` sites whose source symbol_name does not match the defining class name. The same canonicalization is propagated to the symbol-level reference reader: `DbReader.SearchReferences`, `CountSearchReferences`, and `CountSearchReferencesTotal` (backing `references`, `inspect`, MCP `analyze_symbol`) now compute a C#-only `queryAttributeAlias` (defined in the new `ComputeCSharpAttributeSuffixAlias` helper) and OR it into both exact (folded and NOCASE) and substring WHERE clauses, so `references MyAuditAttribute` (and `inspect MyAuditAttribute` / `analyze_symbol MyAuditAttribute`) surface the idiomatic `[MyAudit]` call site consistently with `deps` / `impact`. Substring mode uses an exact-OR for the alias (not a second LIKE) so user queries like `MyAuditAttribute` do not over-match unrelated names such as `FooAuditLog`. The alias is C# only, so Java / Kotlin / Scala / TypeScript annotation sites remain unaffected. `callers` / `callees` intentionally skip the alias because they filter to the shared call-graph kinds (`call`, `instantiate`, `subscribe`) and reject `--kind attribute|annotation` at the CLI / MCP boundary — those commands model the dynamic call graph, not the dependency graph. The alias branch is additionally scoped so it cannot produce false positives: `ComputeCSharpAttributeSuffixAlias` returns `null` when `referenceKind` is a non-`attribute` call-graph kind (so `references FooAttribute --kind call` stays strict and does not match `Foo()`), and the SQL alias disjunct in all three reader methods is clamped to `f.lang = 'csharp' AND r.reference_kind = 'attribute'` so an unscoped `references FooAttribute` (no `--lang` / no `--kind`) still cannot bleed into Java annotations or C# call rows. Suffix detection is also case-insensitive (`OrdinalIgnoreCase`) so `references myauditattribute` and `inspect MyAuditATTRIBUTE` still produce the `MyAudit` alias, consistent with the NOCASE / folded contract of the surrounding exact / substring query paths. Added regression coverage `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies` (runtime + metadata edges both surface), `DbReaderTests.GetFileDependencies_MatchesCSharpAttributeSuffixConvention` (`[MyAudit]` → `MyAuditAttribute` canonicalization through `deps`), `DbReaderTests.SearchReferences_MatchesCSharpAttributeSuffixConvention_Substring` / `_Exact` / `_CSharpAttributeSuffixAliasDoesNotBleedToOtherLanguages` (same canonicalization through `references` / `inspect` / `analyze_symbol`, plus C#-only scope guard), `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_NotAppliedToCallKind` (alias must not fire under `--kind call`), `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_UnscopedLangStillLimitsToCSharpAttributeRows` (unscoped query must not match Java annotations or C# call rows), and `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_CaseInsensitiveQuery` (lowercase / mixed-case queries still produce the alias). Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. - **`callers` / `callees` now reject `--kind attribute|annotation` at the CLI and MCP boundary, and Swift / Gradle `@` metadata is reclassified as `annotation` (#293 follow-up)** — Metadata rows (`attribute` / `annotation`) are attributed to their enclosing body-range symbol rather than the annotated target, so `callers Obsolete --kind attribute` would silently return `[Obsolete] void M()` under the enclosing class (not `M`), and file-level targets such as `[assembly: CLSCompliant]` or Kotlin `@file:JvmName` would drop entirely because `container_name` is `NULL`. `QueryCommandRunner.RunCallers` / `RunCallees` now reject `--kind attribute` and `--kind annotation` with `CommandExitCodes.UsageError`, and the MCP `callers` / `callees` tools return an `isError` tool response; both paths direct users to `references --kind attribute|annotation`, which IS a correct metadata enumeration path. Separately, `AnnotationLanguages` now includes `swift` and `gradle`, so Swift `@available(iOS 13.0, *)` / `@objc` / `@MainActor` and Gradle/Groovy `@CompileStatic` / `@TaskAction` are recorded as `annotation` instead of `call` (previously the `@Name(args)` forms leaked into `callers` / `callees` / `hotspots` / `impact` as phantom call edges, and the no-arg `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` forms dropped from the index entirely because no-arg annotation emission was gated to languages in this set). Added `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError` (theory, 4 cases), `McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError` (theory, 4 cases), `ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`, and `ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation`. Affected: `src/CodeIndex/Cli/QueryCommandRunner.cs`, `src/CodeIndex/Mcp/McpToolHandlers.cs`, `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. @@ -686,7 +686,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **DEVELOPER_GUIDE が SQLite の位置付けとリリース番号の例示を明確化** — cdidx が「設定ゼロ・単一ファイル・本番依存1個」の CLI であり、その唯一の本番依存が `Microsoft.Data.Sqlite` であることを明記した。あわせて、リリース手順のチェックリスト中に出てくる `1.9.0` は固定値ではなく例示であり、実際のリリース番号へ読み替える前提だと明示した。対象: `DEVELOPER_GUIDE.md`。 #### 修正 -- **`deps` の C# attribute サフィックス別名を class-like ターゲットに限定し、`impact` の metadata bypass も class-like ターゲット名が名前空間をまたいで曖昧な場合は抑止するよう修正 (#293 追加対応)** — 直前の #293 追加対応では、`deps` の logical-reference CTE に C# 限定で `symbol_name || 'Attribute'` のサフィックス別名を UNION することで `[Foo]` の attribute consumer が `FooAttribute` クラスファイルへ到達するようにし、`impact`(`GetFileDependencyHintsToResolvedType`)も寄与参照に `attribute` / `annotation` が含まれていれば structured-evidence guard をバイパスするようにしていたが、どちらも広すぎた。`deps` 側では別名が純粋に名前だけで join していたため、`[Foo]` が同名の関数・プロパティ・変数(例: 無関係なヘルパーの `void MyAuditAttribute()`)を定義するファイルにも依存を張ってしまっていた。`impact` 側の metadata bypass も、対象名が一意に class-like 定義を指すかを確認していなかったため、`MyAuditAttribute` のような短い名前が複数の namespace に存在すると rename / remove の blast-radius が過剰報告されていた(`attribute` / `annotation` 参照行は短い名前しか保持できず曖昧性解消ができない)。`DbReader.GetFileDependencies` は合成された別名行に `is_attribute_alias = 1`(本物の行は `0`)をタグ付けし、`source_name_counts` では `MIN` でフラグを伝播し、`edges` CTE の join で別名 edge をターゲット側 kind が `class` / `struct` / `interface` のときだけに制限するようにした。`target_files` CTE も `DISTINCT (path, lang, name, kind)` から `GROUP BY (path, lang, name)` + `MAX(...)` の `has_class_like_kind` フラグに切り替えたため、class と同名関数(例: C# コンストラクタ `Target()` と `class Target`)を同じファイルで定義しているケースでもターゲット行が 2 本に増えず `deps` の参照数を水増ししない。`DbReader.GetFileDependencyHintsToResolvedType` は新ヘルパー `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` を呼び、*active な impact スコープ内* で同名の class-like 定義数を数える(graph 対応言語に加えて、呼び出し側の `--lang` / `--path` / `--exclude-path` / `--exclude-tests` を同じフィルタで適用)。in-scope でその数が 1 以下のときに限り metadata evidence-guard bypass を適用する。スコープ内で曖昧なときは metadata 単独 edge も通常の `SourceFileHasStructuredTypeEvidence` に戻り、純 `[MyAudit]` consumer は誤帰属せずに落ちる。スコープ尊重が重要: 別言語 / 別 path / test ツリーにある同名クラスが C# の metadata bypass を潰してはならない(参照側が既に graph 対応 join で言語修飾されているため、曖昧性は scope 内で判定すべき)。README の `impact` 契約も「active な `--lang` / `--path` / `--exclude-path` / `--exclude-tests` スコープ」と明記している。既存の C# attribute deps テスト `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`、曖昧ではない単一定義の `impact` テスト、および既存の graph-queries dedupe テストは全てグリーンのまま。回帰テストとして `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets`(同名関数があっても虚偽 edge が出ないこと)、`DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget`(impact スコープ内で `MyAuditAttribute` が複数ある場合に metadata heuristic hint を抑止)、`DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous`(単一クラス定義のみのときは従来どおり `[MyAudit]` consumer を浮かせる)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope`(別言語の同名 annotation は `--lang csharp` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope`(スコープ外 `src/B/` の同名クラスは `src/A/` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests`(test ツリーの同名クラスは `--exclude-tests` クエリを潰さない)を追加。`ResolveImpactFallbackNames` も併せて引き締め、C# の `Attribute` サフィックス別名は *解決済み定義自身* の名前にのみ適用するようにした: 以前は同一ファイル内の fallback 名全てに別名を付けていたため、`FooAttribute` を定義するファイルに nested で `BarAttribute` が存在するだけで `impact FooAttribute` が `[Bar]` 利用サイトを `FooAttribute` の影響範囲として誤報告していた。`DbReaderTests.GetFileDependencyHints_CSharpAttributeSuffixAlias_DoesNotLeakToSameFileSiblings` を追加して固定。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 +- **`deps` の C# attribute サフィックス別名を class-like ターゲットに限定し、`impact` の metadata bypass も class-like ターゲット名が名前空間をまたいで曖昧な場合は抑止するよう修正 (#293 追加対応)** — 直前の #293 追加対応では、`deps` の logical-reference CTE に C# 限定で `symbol_name || 'Attribute'` のサフィックス別名を UNION することで `[Foo]` の attribute consumer が `FooAttribute` クラスファイルへ到達するようにし、`impact`(`GetFileDependencyHintsToResolvedType`)も寄与参照に `attribute` / `annotation` が含まれていれば structured-evidence guard をバイパスするようにしていたが、どちらも広すぎた。`deps` 側では別名が純粋に名前だけで join していたため、`[Foo]` が同名の関数・プロパティ・変数(例: 無関係なヘルパーの `void MyAuditAttribute()`)を定義するファイルにも依存を張ってしまっていた。`impact` 側の metadata bypass も、対象名が一意に class-like 定義を指すかを確認していなかったため、`MyAuditAttribute` のような短い名前が複数の namespace に存在すると rename / remove の blast-radius が過剰報告されていた(`attribute` / `annotation` 参照行は短い名前しか保持できず曖昧性解消ができない)。`DbReader.GetFileDependencies` は合成された別名行に `is_attribute_alias = 1`(本物の行は `0`)をタグ付けし、`source_name_counts` では `MIN` でフラグを伝播し、`edges` CTE の join で別名 edge をターゲット側 kind が `class` / `struct` / `interface` のときだけに制限するようにした。`target_files` CTE も `DISTINCT (path, lang, name, kind)` から `GROUP BY (path, lang, name)` + `MAX(...)` の `has_class_like_kind` フラグに切り替えたため、class と同名関数(例: C# コンストラクタ `Target()` と `class Target`)を同じファイルで定義しているケースでもターゲット行が 2 本に増えず `deps` の参照数を水増ししない。`DbReader.GetFileDependencyHintsToResolvedType` は新ヘルパー `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` を呼び、*active な impact スコープ内* で同名の class-like 定義数を数える(graph 対応言語に加えて、呼び出し側の `--lang` / `--path` / `--exclude-path` / `--exclude-tests` を同じフィルタで適用)。in-scope でその数が 1 以下のときに限り metadata evidence-guard bypass を適用する。スコープ内で曖昧なときは metadata 単独 edge も通常の `SourceFileHasStructuredTypeEvidence` に戻り、純 `[MyAudit]` consumer は誤帰属せずに落ちる。スコープ尊重が重要: 別言語 / 別 path / test ツリーにある同名クラスが C# の metadata bypass を潰してはならない(参照側が既に graph 対応 join で言語修飾されているため、曖昧性は scope 内で判定すべき)。README の `impact` 契約も「active な `--lang` / `--path` / `--exclude-path` / `--exclude-tests` スコープ」と明記している。既存の C# attribute deps テスト `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`、曖昧ではない単一定義の `impact` テスト、および既存の graph-queries dedupe テストは全てグリーンのまま。回帰テストとして `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets`(同名関数があっても虚偽 edge が出ないこと)、`DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget`(impact スコープ内で `MyAuditAttribute` が複数ある場合に metadata heuristic hint を抑止)、`DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous`(単一クラス定義のみのときは従来どおり `[MyAudit]` consumer を浮かせる)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope`(別言語の同名 annotation は `--lang csharp` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope`(スコープ外 `src/B/` の同名クラスは `src/A/` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests`(test ツリーの同名クラスは `--exclude-tests` クエリを潰さない)を追加。`ResolveImpactFallbackNames` も併せて引き締め、C# の `Attribute` サフィックス別名は *解決済み定義自身* の名前にのみ適用するようにした: 以前は同一ファイル内の fallback 名全てに別名を付けていたため、`FooAttribute` を定義するファイルに nested で `BarAttribute` が存在するだけで `impact FooAttribute` が `[Bar]` 利用サイトを `FooAttribute` の影響範囲として誤報告していた。`DbReaderTests.GetFileDependencyHints_CSharpAttributeSuffixAlias_DoesNotLeakToSameFileSiblings` を追加して固定。さらに `GetFileDependencies` を再構成し、metadata 行(`attribute` / `annotation`)が bare-name の call-graph ターゲットに漏れないようにした: `logical_references` CTE に `is_metadata` フラグを付与し、`source_name_counts` の GROUP BY を `(source_file_id, source_path, source_lang, symbol_name, is_attribute_alias, is_metadata)` に拡張して metadata と非 metadata で同じ symbol_name の行を別グループ化する。`edges` CTE は全 metadata edge(合成別名だけでなく実体行も)に `tf.has_class_like_kind = 1` を要求し、さらに 2 つの CTE で残る誤検知を潰す: `metadata_raw_suppression` は同じ source file で合成別名 `FooAttribute` が class-like ターゲットに解決できているとき、生の C# `[Foo]` 行を落とす(`[Foo]` が `class FooAttribute` と無関係な同名 `class Foo` に fan-out しない)。`target_ambiguity` はスコープ内で同名 class-like ターゲットが複数あるとき metadata edge を落とす(曖昧な `[MyAudit]` は deps で fan-out させず、`impact` / `references` に判断を委ねる)。既存の `GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies`(`[JsonConverter(...)]` → `class JsonConverter : Attribute` をターゲット名に `Attribute` サフィックスがなくても固定)は、class-like ターゲットが 1 つしかないので `target_ambiguity` で残り、`is_metadata` / class-like 制限も受理し、合成 `JsonConverterAttribute` 別名は class-like に解決できないので `metadata_raw_suppression` も発火せず、そのまま通る。回帰テストとして `DbReaderTests.GetFileDependencies_CSharpAttributeRawDoesNotLeakToBareNameClass_WhenSuffixTargetExists`(別名が `class MyAuditAttribute` に解決できたとき生 `[MyAudit]` 行は抑止され、無関係な plain `class MyAudit` ファイルに依存しない)、`DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotLeakToSameNameMethodOrProperty`(完全形 `[MyAuditAttribute]` は同名メソッド / プロパティしか持たないファイルに依存しない)、`DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotFanOutWhenMultipleSameNameAttributeClasses`(別名前空間に同名 attribute クラスが 2 つあると metadata edge はあいまいとして落とす)を追加。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 - **MCP の `callers` / `callees` の `kind` schema・CLI help・README の `--kind` オプション行が metadata kinds を有効フィルタ値として広告しないよう修正 (#293 追加対応)** — `callers` / `callees` の handler は `kind: "attribute"` / `kind: "annotation"` を usage error(CLI)/ `isError` ツールレスポンス(MCP)で拒否するにもかかわらず、MCP ツール schema の `kind` プロパティ説明は 5 種すべてを列挙した `"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"` のままで、`cdidx --help` の `--kind` 行も `callers` / `callees` を `references` と同じ行にまとめていた。さらに README の `--kind` オプション表(英語 436 行目 / 日本語 1307 行目)も、`callers` / `callees` で `--kind attribute` / `--kind annotation` を渡せるかのように案内していた。schema と handler の契約を一致させ、`callers` / `callees` の `kind` description を `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."` に修正し、CLI の `--kind` help 行も `references`(5 種すべて受理)と `callers` / `callees`(call-graph 種別のみ、metadata kinds は拒否して `references` へ誘導)を分けるようにし、README の `--kind` 行(英語・日本語の両方)も `callers` / `callees` が `--kind attribute|annotation` を usage error で拒否すること、metadata 列挙の正規経路が `references --kind attribute|annotation` であることを明示する文言に直した。MCP の `references` ツール説明は全 reference kind を受け付ける契約のままなので変更なし。修正後の schema 文言を固定する `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` を追加。対象: `src/CodeIndex/Mcp/McpToolDefinitions.cs`、`src/CodeIndex/Cli/ConsoleUi.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`、`README.md`。 - **`deps`・`impact`・`references`・`inspect`・`analyze_symbol` が metadata 参照を compile-time 依存として保持し、C# の attribute サフィックス命名規約を正規化 (#293 追加対応)** — `cdidx deps` の実装である `DbReader.GetFileDependencies` と、`cdidx impact` / MCP `impact_analysis` の heuristic file-hint フォールバックである `DbReader.GetFileDependencyHintsToResolvedType` は、logical reference CTE に metadata 参照も含める。attribute クラスを rename / 削除すれば `[JsonConverterAttribute]` のような注釈サイトも compile-time で壊れるため、ファイル単位の blast-radius 分析はそれらも本物の依存 edge として表示する必要があり、`logical_reference_kind` では `attribute` / `annotation` として正しくラベル付けされる(分類修正前は同じサイトが phantom `call` として漏れていた)。さらに `GetFileDependencies` は C# の attribute 命名規約を正規化する: 慣用的な `[Foo]` は `symbol_name = "Foo"` で保存されるが、定義クラスは `FooAttribute` 命名となるため、logical-reference CTE に C# `attribute` 種別の行をサフィックス `'Attribute'` を付与した別名コピーとして UNION ALL で追加する(元の symbol_name が既に `Attribute` で終わっている場合は重複しないようスキップ)。これにより target ファイル join が attribute クラス側のファイルへ到達する。`GetFileDependencyHintsToResolvedType` はさらに candidate ごとに `has_metadata_ref` フラグを集約し、寄与する参照が metadata だけである edge については `SourceFileHasStructuredTypeEvidence` guard をバイパスするため、`[MyAudit] class Svc` のような純 attribute consumer ファイルも `impact MyAuditAttribute` で出るようになった(以前は filter で落ちていた)。`ResolveImpactFallbackNames` も対称的に、`Attribute` で終わる C# クラス名にはサフィックスを外した別名を追加するため、BFS を使わない file-hint 経路でも source symbol_name が定義クラス名と一致しない `[Foo]` サイトを発見できる。同じ正規化をシンボル単位の参照 reader にも伝播する: `references`・`inspect`・MCP `analyze_symbol` を支える `DbReader.SearchReferences`・`CountSearchReferences`・`CountSearchReferencesTotal` は、新しいヘルパー `ComputeCSharpAttributeSuffixAlias` で C# 限定の `queryAttributeAlias` を計算し、exact(folded と NOCASE)・substring の両方の WHERE 句に OR で追加する。これにより `references MyAuditAttribute`(および `inspect MyAuditAttribute` / `analyze_symbol MyAuditAttribute`)でも慣用的な `[MyAudit]` 参照サイトが `deps` / `impact` と一貫して surface する。substring モードでは alias を二つ目の LIKE にせず exact-OR で束ねているため、ユーザーが `MyAuditAttribute` と打った時に `FooAuditLog` のような無関係な名前に over-match しない。alias は C# 限定なので、Java / Kotlin / Scala / TypeScript の annotation サイトには影響しない。`callers` / `callees` は共有の call-graph 種別 (`call` / `instantiate` / `subscribe`) に絞って `--kind attribute|annotation` を CLI / MCP 境界で拒否する契約なので、alias は意図的に適用しない(dynamic call graph と dependency graph は別契約)。さらに alias 節が誤一致を生まないようスコープする: `ComputeCSharpAttributeSuffixAlias` は `referenceKind` が `attribute` 以外の call-graph 種別のときは `null` を返す(`references FooAttribute --kind call` は厳格一致のままで `Foo()` に一致しない)、3 つの reader メソッドの SQL alias 節は `f.lang = 'csharp' AND r.reference_kind = 'attribute'` に限定するため、`--lang` / `--kind` 無指定の `references FooAttribute` でも Java の annotation や C# の call 行を拾わない。suffix 検出も `OrdinalIgnoreCase` にして case-insensitive 化したため、`references myauditattribute` / `inspect MyAuditATTRIBUTE` のような小文字・混在ケースでも alias `MyAudit` を生成でき、周辺の NOCASE / folded 契約と整合する。回帰テスト `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies`(runtime と metadata の両方の edge が出ることを固定)、`DbReaderTests.GetFileDependencies_MatchesCSharpAttributeSuffixConvention`(`deps` 経由での `[MyAudit]` → `MyAuditAttribute` 正規化)、`DbReaderTests.SearchReferences_MatchesCSharpAttributeSuffixConvention_Substring` / `_Exact` / `_CSharpAttributeSuffixAliasDoesNotBleedToOtherLanguages`(`references` / `inspect` / `analyze_symbol` での同一正規化と C# 限定スコープ)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_NotAppliedToCallKind`(`--kind call` で alias が発火しないこと)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_UnscopedLangStillLimitsToCSharpAttributeRows`(無指定でも Java annotation や C# call 行に一致しないこと)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_CaseInsensitiveQuery`(小文字・混在ケースのクエリでも alias を生成)を追加。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 - **`callers` / `callees` が CLI と MCP 境界で `--kind attribute|annotation` を拒否し、Swift / Gradle の `@` metadata を `annotation` に再分類 (#293 追加対応)** — metadata 行 (`attribute` / `annotation`) は注釈対象そのものではなく body-range の外側シンボルに帰属するため、`callers Obsolete --kind attribute` は `[Obsolete] void M()` を `M` ではなく外側クラスの下に黙って返し、`[assembly: CLSCompliant]` や Kotlin の `@file:JvmName` のような file-level target は `container_name = NULL` で完全脱落していた。`QueryCommandRunner.RunCallers` / `RunCallees` は `--kind attribute` / `--kind annotation` を `CommandExitCodes.UsageError` で弾き、MCP の `callers` / `callees` ツールは `isError` ツールレスポンスを返すようにした。どちらの経路も、metadata 列挙の正しいパスである `references --kind attribute|annotation` へ誘導する。あわせて `AnnotationLanguages` に `swift` と `gradle` を追加し、Swift の `@available(iOS 13.0, *)` / `@objc` / `@MainActor` や Gradle / Groovy の `@CompileStatic` / `@TaskAction` を `call` ではなく `annotation` として記録するようにした(修正前は引数付きの `@Name(args)` 形は `callers` / `callees` / `hotspots` / `impact` に phantom call edge として漏れ、引数なしの `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` 形はこの集合で gate されていた no-arg annotation 出力から外れてインデックス自体に残らなかった)。回帰テストとして `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError`(theory 4 ケース)、`McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError`(theory 4 ケース)、`ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`、`ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation` を追加。対象: `src/CodeIndex/Cli/QueryCommandRunner.cs`、`src/CodeIndex/Mcp/McpToolHandlers.cs`、`src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 diff --git a/src/CodeIndex/Database/DbReader.cs b/src/CodeIndex/Database/DbReader.cs index feb1a84430..ab393d49d9 100644 --- a/src/CodeIndex/Database/DbReader.cs +++ b/src/CodeIndex/Database/DbReader.cs @@ -2589,7 +2589,8 @@ FROM symbol_references r ), logical_references AS ( SELECT source_file_id, source_path, source_lang, symbol_name, line, column_number, logical_reference_kind, - 0 AS is_attribute_alias + 0 AS is_attribute_alias, + CASE WHEN logical_reference_kind IN ('attribute', 'annotation') THEN 1 ELSE 0 END AS is_metadata FROM logical_references_primary UNION ALL -- C# attribute suffix alias: [Foo] in source is stored with symbol_name='Foo', @@ -2605,27 +2606,31 @@ UNION ALL SELECT source_file_id, source_path, source_lang, symbol_name || 'Attribute' AS symbol_name, line, column_number, logical_reference_kind, - 1 AS is_attribute_alias + 1 AS is_attribute_alias, + 1 AS is_metadata FROM logical_references_primary WHERE source_lang = 'csharp' AND logical_reference_kind = 'attribute' AND symbol_name NOT LIKE '%Attribute' ), source_name_counts AS ( + -- Grouping includes is_metadata so metadata-only groups ([Foo] / @Foo) + -- can be restricted to class-like targets independently from non-metadata + -- call-graph groups that share the same symbol_name in the same file + -- (e.g. `Foo()` call + `[Foo]` attribute both present in the same source). + -- is_metadata を GROUP BY に含めることで、同じ source file / symbol_name を + -- 共有する metadata 行と call-graph 行 (例: 同じファイル内の `Foo()` 呼び出し + -- と `[Foo]` 属性) を別グループとして扱い、metadata 側だけに class-like + -- target 制限を掛けられるようにする。 SELECT source_file_id, source_path, source_lang, symbol_name, - -- MIN: if any real (non-alias) ref shares the same symbol_name group, - -- treat the group as non-alias so the class-kind restriction only - -- applies to purely synthetic alias rows. - -- MIN: 同じ symbol_name のグループに real (非 alias) 行が 1 件でも - -- あれば、そのグループは非 alias 扱いとし、class 種別の制限を - -- 純粋に合成された alias 行にのみ適用する。 - MIN(is_attribute_alias) AS is_attribute_alias, + is_attribute_alias, + is_metadata, COUNT(*) AS ref_count FROM logical_references - GROUP BY source_file_id, source_path, source_lang, symbol_name + GROUP BY source_file_id, source_path, source_lang, symbol_name, is_attribute_alias, is_metadata ), target_files AS ( -- Collapse per-symbol rows to one per (target_path, target_lang, symbol_name) @@ -2664,6 +2669,41 @@ FROM symbols s sql += @" GROUP BY dst.path, dst.lang, s.name ), + metadata_raw_suppression AS ( + -- When a raw C# attribute reference '[Foo]' (stored as symbol_name='Foo', + -- logical_reference_kind='attribute') also has a synthetic suffix alias + -- row that resolves to a class-like 'FooAttribute' target, drop the raw + -- row to avoid creating a duplicate edge to any unrelated 'Foo' symbol + -- (method, property, local class) that merely shares the bare name. + -- 生の C# 属性参照 '[Foo]' (symbol_name='Foo', kind='attribute') に対して + -- 同じ source_file 内で 'FooAttribute' の synthetic alias 行が + -- class 系 target に解決できる場合、この行自体は落として + -- 同名の関数/プロパティ/ローカルクラス 'Foo' への誤依存を防ぐ。 + SELECT DISTINCT lrp.source_file_id, lrp.symbol_name + FROM logical_references_primary lrp + JOIN target_files tf_alias + ON tf_alias.target_lang = lrp.source_lang + AND tf_alias.symbol_name = lrp.symbol_name || 'Attribute' + AND tf_alias.has_class_like_kind = 1 + WHERE lrp.source_lang = 'csharp' + AND lrp.logical_reference_kind = 'attribute' + AND lrp.symbol_name NOT LIKE '%Attribute' + ), + target_ambiguity AS ( + -- Count distinct class-like target files per (target_lang, symbol_name). + -- Metadata references ([Foo] / @Foo) must not fan out to multiple + -- same-name attribute / annotation class definitions; when ambiguous, + -- drop the metadata edge and let `impact` / `references` guide the user. + -- (target_lang, symbol_name) 単位で class 系 target ファイル数を数える。 + -- metadata 参照 ([Foo] / @Foo) は同名 attribute / annotation クラスが + -- 複数あるとき fan-out させず、その metadata エッジを落とす。 + SELECT target_lang, + symbol_name, + COUNT(DISTINCT target_path) AS class_like_target_count + FROM target_files + WHERE has_class_like_kind = 1 + GROUP BY target_lang, symbol_name + ), edges AS ( SELECT snc.source_path, tf.target_path, @@ -2673,15 +2713,40 @@ FROM source_name_counts snc JOIN target_files tf ON tf.symbol_name = snc.symbol_name AND tf.target_lang = snc.source_lang + LEFT JOIN metadata_raw_suppression mrs + ON mrs.source_file_id = snc.source_file_id + AND mrs.symbol_name = snc.symbol_name + LEFT JOIN target_ambiguity ta + ON ta.target_lang = snc.source_lang + AND ta.symbol_name = snc.symbol_name WHERE snc.source_path != tf.target_path - -- Purely synthetic C# attribute alias rows must only match class-like - -- target kinds; otherwise '[Foo]' would spuriously depend on any file - -- that merely defines a function / property / variable named - -- 'FooAttribute'. Real attribute references continue to match any kind. - -- 純粋な合成 alias 行は class 系の target 種別にのみ一致させる。 - -- これを許すと '[Foo]' が単に 'FooAttribute' という名前の関数や - -- プロパティを持つファイルにまで誤って依存してしまう。 - AND (snc.is_attribute_alias = 0 OR tf.has_class_like_kind = 1) + -- All metadata references ([Foo] / @Foo) and their synthetic C# + -- suffix aliases must only match class-like target kinds; otherwise + -- a metadata reference would spuriously depend on any file that + -- merely defines a function / property / variable sharing the name. + -- Non-metadata call-graph refs keep matching any kind so e.g. a + -- constructor call can still tie back to a class definition. + -- metadata 参照 ([Foo] / @Foo) と C# の合成 alias 行はいずれも + -- class 系の target 種別にのみ一致させる。これを許すと同名の + -- 関数/プロパティ/変数を持つだけのファイルまで誤って依存してしまう。 + -- 非 metadata の call-graph 参照は任意の kind に一致させて構わない + -- (コンストラクタ呼び出しがクラス定義に結び付くケースなど)。 + AND (snc.is_metadata = 0 OR tf.has_class_like_kind = 1) + -- Drop raw C# '[Foo]' rows when the suffix alias already resolves + -- to a class-like 'FooAttribute' target in the same source file. + -- 同じ source file で suffix alias が class 系 'FooAttribute' に + -- 解決できている C# の raw '[Foo]' 行は落とす。 + AND NOT ( + snc.is_metadata = 1 + AND snc.is_attribute_alias = 0 + AND snc.source_lang = 'csharp' + AND mrs.source_file_id IS NOT NULL + ) + -- Metadata edges only survive when the target symbol resolves to + -- a single class-like definition within scope; ambiguous cases + -- (multiple same-name attribute / annotation classes) are dropped. + -- metadata エッジは同名 class 系 target が 1 つだけのときのみ残す。 + AND (snc.is_metadata = 0 OR COALESCE(ta.class_like_target_count, 0) <= 1) ) SELECT source_path, target_path, diff --git a/tests/CodeIndex.Tests/DbReaderTests.cs b/tests/CodeIndex.Tests/DbReaderTests.cs index 1e9d4ef7fe..4d18e0c91d 100644 --- a/tests/CodeIndex.Tests/DbReaderTests.cs +++ b/tests/CodeIndex.Tests/DbReaderTests.cs @@ -2348,6 +2348,151 @@ public class Svc Assert.Contains(dependencies, d => d.SourcePath == "src/Svc.cs" && d.TargetPath == "src/MyAuditAttribute.cs"); } + [Fact] + public void GetFileDependencies_CSharpAttributeRawDoesNotLeakToBareNameClass_WhenSuffixTargetExists() + { + // issue #293 follow-up: `[MyAudit]` in C# is stored as symbol_name='MyAudit'. + // When both `class MyAudit` (plain class) and `class MyAuditAttribute` + // (the real attribute target) exist, the metadata edge must resolve only + // to `MyAuditAttribute` via the synthetic suffix alias. Keeping the raw + // bare-name edge would over-report: `[MyAudit]` would falsely depend on + // the unrelated plain `class MyAudit` file. + // issue #293 補足: C# の `[MyAudit]` は symbol_name='MyAudit' で保存される。 + // `class MyAudit` (plain) と `class MyAuditAttribute` (本物の attribute) + // が両方あるとき、metadata エッジは synthetic suffix alias 経由で + // `MyAuditAttribute` だけに解決されるべき。raw の bare-name エッジを + // 残すと、`[MyAudit]` が無関係な plain `class MyAudit` のファイルにも + // 誤って依存してしまう。 + InsertIndexedFile("src/MyAuditAttribute.cs", "csharp", + """ + using System; + + [AttributeUsage(AttributeTargets.Class)] + public sealed class MyAuditAttribute : Attribute + { + } + """); + InsertIndexedFile("src/PlainMyAudit.cs", "csharp", + """ + public class MyAudit + { + } + """); + InsertIndexedFile("src/Svc.cs", "csharp", + """ + [MyAudit] + public class Svc + { + } + """); + + var dependencies = _reader.GetFileDependencies(limit: 10, lang: "csharp"); + + // Only MyAuditAttribute.cs should be a dependency target for Svc.cs; + // PlainMyAudit.cs must not appear. + // Svc.cs の依存先は MyAuditAttribute.cs のみで、PlainMyAudit.cs は + // 出現してはならない。 + Assert.Contains(dependencies, d => d.SourcePath == "src/Svc.cs" && d.TargetPath == "src/MyAuditAttribute.cs"); + Assert.DoesNotContain(dependencies, d => d.SourcePath == "src/Svc.cs" && d.TargetPath == "src/PlainMyAudit.cs"); + } + + [Fact] + public void GetFileDependencies_CSharpAttributeDoesNotLeakToSameNameMethodOrProperty() + { + // issue #293 follow-up: `[MyAuditAttribute]` (fully qualified) must only + // match a class-like attribute target. A method / property named + // `MyAuditAttribute` in an unrelated file must never show up as a deps + // edge from the metadata reference. Non-metadata call-graph edges keep + // their previous behavior (they can still resolve to any symbol kind). + // issue #293 補足: `[MyAuditAttribute]` (完全形) は class 系の attribute + // target にしか一致してはならない。別ファイルの同名メソッド/プロパティ + // `MyAuditAttribute` が metadata 参照の deps エッジに現れてはいけない。 + // 非 metadata の call-graph エッジは従来どおり任意の kind に解決できる。 + InsertIndexedFile("src/MyAuditAttribute.cs", "csharp", + """ + using System; + + [AttributeUsage(AttributeTargets.Class)] + public sealed class MyAuditAttribute : Attribute + { + } + """); + InsertIndexedFile("src/Helpers.cs", "csharp", + """ + public class Helpers + { + public void MyAuditAttribute() + { + } + } + """); + InsertIndexedFile("src/Svc.cs", "csharp", + """ + [MyAuditAttribute] + public class Svc + { + } + """); + + var dependencies = _reader.GetFileDependencies(limit: 10, lang: "csharp"); + + Assert.DoesNotContain(dependencies, d => d.SourcePath == "src/Svc.cs" && d.TargetPath == "src/Helpers.cs"); + } + + [Fact] + public void GetFileDependencies_CSharpAttributeDoesNotFanOutWhenMultipleSameNameAttributeClasses() + { + // issue #293 follow-up: if multiple same-named attribute classes exist + // (e.g. two `MyAuditAttribute` classes in separate namespaces/files), + // a metadata reference `[MyAudit]` must not fan out to BOTH files. We + // cannot statically resolve which one the C# compiler picks without + // namespace / using analysis, so we drop the ambiguous metadata edge + // and let `impact` / `references` surface both candidates to the user. + // issue #293 補足: 同名 attribute クラスが複数ある場合 (例: 別名前空間/別 + // ファイルに 2 つの `MyAuditAttribute` がある場合)、metadata 参照 + // `[MyAudit]` を両方に fan-out させない。cdidx は namespace / using を + // 解析しないため正しい解決ができず、あいまいな metadata エッジは落として + // 両候補は `impact` / `references` 経由でユーザーに示す。 + InsertIndexedFile("src/A/MyAuditAttribute.cs", "csharp", + """ + using System; + + namespace A + { + [AttributeUsage(AttributeTargets.Class)] + public sealed class MyAuditAttribute : Attribute + { + } + } + """); + InsertIndexedFile("src/B/MyAuditAttribute.cs", "csharp", + """ + using System; + + namespace B + { + [AttributeUsage(AttributeTargets.Class)] + public sealed class MyAuditAttribute : Attribute + { + } + } + """); + InsertIndexedFile("src/Svc.cs", "csharp", + """ + [MyAudit] + public class Svc + { + } + """); + + var dependencies = _reader.GetFileDependencies(limit: 10, lang: "csharp"); + + // Neither fan-out edge should exist; the metadata reference is ambiguous. + // あいまいな metadata 参照はどちらの fan-out エッジも出してはならない。 + Assert.DoesNotContain(dependencies, d => d.SourcePath == "src/Svc.cs" && d.TargetPath == "src/A/MyAuditAttribute.cs"); + Assert.DoesNotContain(dependencies, d => d.SourcePath == "src/Svc.cs" && d.TargetPath == "src/B/MyAuditAttribute.cs"); + } + [Fact] public void SearchReferences_MatchesCSharpAttributeSuffixConvention_Substring() { From 992397692f42006632487335eadf9fb4becf22ee Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 18 Apr 2026 14:48:57 +0900 Subject: [PATCH 24/30] Fix #293 count metadata target ambiguity at symbol identity level, not path level MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round 14 adversarial review found that both ambiguity counters for metadata references used path-level counting: - `IsMetadataTargetUnambiguous` (impact metadata bypass guard) used `SELECT DISTINCT f.path` on the symbols table. - `target_ambiguity` CTE (deps metadata edge drop) used `COUNT(DISTINCT target_path)` over `target_files`, which is itself grouped by `dst.path`. Both collapsed to count=1 when a single source file contains two same-named class-like definitions under different namespaces, e.g. idiomatic C# with multiple `namespace { ... }` blocks in one .cs file. This contradicts the design intent that ambiguity includes duplicates in one file — metadata reference rows only keep the short name and cannot disambiguate between `A.FooAttribute` and `B.FooAttribute`. Count at symbol-identity level instead: - `IsMetadataTargetUnambiguous` now selects `DISTINCT f.path, s.line, s.name`. - `target_ambiguity` joins `target_files` back through `files` + `symbols` and uses `COUNT(*)` over class-like symbol rows, which restores per-definition count while still inheriting target_files' lang / path / graph-supported scope. Add regression tests for both `deps` and `impact`. Co-Authored-By: Claude Opus 4 --- CHANGELOG.md | 4 +- src/CodeIndex/Database/DbReader.cs | 57 +++++++++--- tests/CodeIndex.Tests/DbReaderTests.cs | 116 +++++++++++++++++++++++++ 3 files changed, 161 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f9ffbf51a..13b0f212a3 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/), - **Developer Guide now distinguishes SQLite's role from "zero dependencies" and labels release numbers as examples** — Clarified that cdidx remains a zero-configuration, single-file CLI with exactly one production dependency (`Microsoft.Data.Sqlite`), and made the release-workflow checklist explicitly state that `1.9.0` is only an example version to substitute during an actual release. Affected: `DEVELOPER_GUIDE.md`. #### Fixed -- **`deps` C# attribute suffix alias now only matches class-like targets, and `impact` metadata bypass is suppressed when the class-like target name is ambiguous across namespaces (#293 follow-up)** — The earlier `#293` follow-up made `deps` UNION a C#-only `symbol_name || 'Attribute'` suffix alias into the logical-reference CTE so `[Foo]` attribute consumers could reach `FooAttribute` class files, and made `impact` (`GetFileDependencyHintsToResolvedType`) bypass the structured-evidence guard whenever any contributing reference was `attribute` / `annotation`. Both changes were too broad. On `deps` the alias joined purely by name, so `[Foo]` would also depend on any file that merely defined a function / property / variable named `FooAttribute` (e.g. `void MyAuditAttribute()` in an unrelated helper). On `impact` the metadata bypass did not check whether the target name was uniquely a class-like definition, so when the same unqualified name (e.g. `MyAuditAttribute`) existed in multiple namespaces the rename/remove blast radius was over-reported — attribute / annotation reference rows only keep the short name and cannot be disambiguated. `DbReader.GetFileDependencies` now tags synthetic alias rows with `is_attribute_alias = 1` (real rows are `0`), propagates the flag through `source_name_counts` via `MIN`, and restricts alias edges to class-like target kinds (`class` / `struct` / `interface`) in the `edges` CTE join. The `target_files` CTE is also switched from `DISTINCT` over `(path, lang, name, kind)` to `GROUP BY (path, lang, name)` with a `MAX(...)` `has_class_like_kind` flag, so files that define both a class and a same-named function (e.g. the C# constructor `Target()` inside `class Target`) no longer fan out into two target rows and inflate the `deps` reference count. `DbReader.GetFileDependencyHintsToResolvedType` now calls a new `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` helper that counts class-like definitions with the same unqualified name *within the active impact scope* (graph-supported languages, plus the caller's `--lang` / `--path` / `--exclude-path` / `--exclude-tests` filters); the metadata evidence-guard bypass is only applied when that in-scope count is ≤ 1. If the name is ambiguous within scope, metadata-only edges fall through to the normal `SourceFileHasStructuredTypeEvidence` check, so pure `[MyAudit]` consumers are dropped instead of being mis-attributed. Scoping matters: a same-named class in an unrelated language / path / test tree must not suppress the C# metadata bypass, because attribute reference rows are already language-qualified through the graph-supported reference join, and the README contract for `impact` explicitly promises "active `--lang` / `--path` / `--exclude-path` / `--exclude-tests` scope". The C# attribute deps test `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`, the unambiguous single-definition `impact` test, and the pre-existing graph-queries dedupe test all still pass. Added `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets` (ambiguous same-name function must not produce a spurious edge), `DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget` (two `MyAuditAttribute` classes within the same impact scope suppress the metadata heuristic hint), `DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous` (single class definition still surfaces the `[MyAudit]` consumer), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope` (same-named Java annotation must not suppress a `--lang csharp` query), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope` (out-of-scope `src/B/` same-named class must not suppress a `src/A/`-scoped query), and `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests` (test-tree same-named class must not suppress an `--exclude-tests` query). `ResolveImpactFallbackNames` is additionally tightened so the C# `Attribute`-suffix alias is only applied to the resolved definition's own name: previously the alias was applied to every same-file fallback name, so a nested `BarAttribute` inside the file that defines `FooAttribute` caused `impact FooAttribute` to falsely attribute `[Bar]` use sites to `FooAttribute`'s blast radius. Added `DbReaderTests.GetFileDependencyHints_CSharpAttributeSuffixAlias_DoesNotLeakToSameFileSiblings` to pin this. `GetFileDependencies` is additionally restructured so metadata rows (`attribute` / `annotation`) cannot leak onto bare-name call-graph targets: the `logical_references` CTE now carries an `is_metadata` flag, `source_name_counts` groups by `(source_file_id, source_path, source_lang, symbol_name, is_attribute_alias, is_metadata)` so metadata and non-metadata rows with the same symbol_name stay separate, the `edges` CTE restricts ALL metadata edges (not just synthetic aliases) to `tf.has_class_like_kind = 1`, and two new CTEs resolve the remaining false-positive shapes: `metadata_raw_suppression` drops the raw C# `[Foo]` row when the synthetic `FooAttribute` alias already resolves to a class-like target in the same source file (so `[Foo]` does not fan out to both `class FooAttribute` and a plain `class Foo` that shares the bare name), and `target_ambiguity` drops metadata edges when the same-named symbol resolves to multiple class-like definitions in scope (so ambiguous `[MyAudit]` usage is left to `impact` / `references` to disambiguate rather than being fanned out to every same-named attribute class). The existing `GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies` test (which pins `[JsonConverter(...)]` → `class JsonConverter : Attribute` for a target class whose name does NOT end in `Attribute`) still passes because only one class-like target exists in scope, so `target_ambiguity` keeps it, the `is_metadata` / class-like restriction accepts it, and `metadata_raw_suppression` finds no synthetic `JsonConverterAttribute` alias target and therefore does not suppress the raw row. Added `DbReaderTests.GetFileDependencies_CSharpAttributeRawDoesNotLeakToBareNameClass_WhenSuffixTargetExists` (raw `[MyAudit]` row is suppressed when the alias resolves to `class MyAuditAttribute`, so `[MyAudit]` does NOT depend on an unrelated plain `class MyAudit` file), `DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotLeakToSameNameMethodOrProperty` (fully-qualified `[MyAuditAttribute]` metadata does NOT depend on a file that only defines a method / property named `MyAuditAttribute`), and `DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotFanOutWhenMultipleSameNameAttributeClasses` (two same-named attribute classes in different namespaces cause the metadata edge to be dropped as ambiguous). Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. +- **`deps` C# attribute suffix alias now only matches class-like targets, and `impact` metadata bypass is suppressed when the class-like target name is ambiguous across namespaces (#293 follow-up)** — The earlier `#293` follow-up made `deps` UNION a C#-only `symbol_name || 'Attribute'` suffix alias into the logical-reference CTE so `[Foo]` attribute consumers could reach `FooAttribute` class files, and made `impact` (`GetFileDependencyHintsToResolvedType`) bypass the structured-evidence guard whenever any contributing reference was `attribute` / `annotation`. Both changes were too broad. On `deps` the alias joined purely by name, so `[Foo]` would also depend on any file that merely defined a function / property / variable named `FooAttribute` (e.g. `void MyAuditAttribute()` in an unrelated helper). On `impact` the metadata bypass did not check whether the target name was uniquely a class-like definition, so when the same unqualified name (e.g. `MyAuditAttribute`) existed in multiple namespaces the rename/remove blast radius was over-reported — attribute / annotation reference rows only keep the short name and cannot be disambiguated. `DbReader.GetFileDependencies` now tags synthetic alias rows with `is_attribute_alias = 1` (real rows are `0`), propagates the flag through `source_name_counts` via `MIN`, and restricts alias edges to class-like target kinds (`class` / `struct` / `interface`) in the `edges` CTE join. The `target_files` CTE is also switched from `DISTINCT` over `(path, lang, name, kind)` to `GROUP BY (path, lang, name)` with a `MAX(...)` `has_class_like_kind` flag, so files that define both a class and a same-named function (e.g. the C# constructor `Target()` inside `class Target`) no longer fan out into two target rows and inflate the `deps` reference count. `DbReader.GetFileDependencyHintsToResolvedType` now calls a new `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` helper that counts class-like definitions with the same unqualified name *within the active impact scope* (graph-supported languages, plus the caller's `--lang` / `--path` / `--exclude-path` / `--exclude-tests` filters); the metadata evidence-guard bypass is only applied when that in-scope count is ≤ 1. If the name is ambiguous within scope, metadata-only edges fall through to the normal `SourceFileHasStructuredTypeEvidence` check, so pure `[MyAudit]` consumers are dropped instead of being mis-attributed. Scoping matters: a same-named class in an unrelated language / path / test tree must not suppress the C# metadata bypass, because attribute reference rows are already language-qualified through the graph-supported reference join, and the README contract for `impact` explicitly promises "active `--lang` / `--path` / `--exclude-path` / `--exclude-tests` scope". The C# attribute deps test `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`, the unambiguous single-definition `impact` test, and the pre-existing graph-queries dedupe test all still pass. Added `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets` (ambiguous same-name function must not produce a spurious edge), `DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget` (two `MyAuditAttribute` classes within the same impact scope suppress the metadata heuristic hint), `DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous` (single class definition still surfaces the `[MyAudit]` consumer), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope` (same-named Java annotation must not suppress a `--lang csharp` query), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope` (out-of-scope `src/B/` same-named class must not suppress a `src/A/`-scoped query), and `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests` (test-tree same-named class must not suppress an `--exclude-tests` query). `ResolveImpactFallbackNames` is additionally tightened so the C# `Attribute`-suffix alias is only applied to the resolved definition's own name: previously the alias was applied to every same-file fallback name, so a nested `BarAttribute` inside the file that defines `FooAttribute` caused `impact FooAttribute` to falsely attribute `[Bar]` use sites to `FooAttribute`'s blast radius. Added `DbReaderTests.GetFileDependencyHints_CSharpAttributeSuffixAlias_DoesNotLeakToSameFileSiblings` to pin this. `GetFileDependencies` is additionally restructured so metadata rows (`attribute` / `annotation`) cannot leak onto bare-name call-graph targets: the `logical_references` CTE now carries an `is_metadata` flag, `source_name_counts` groups by `(source_file_id, source_path, source_lang, symbol_name, is_attribute_alias, is_metadata)` so metadata and non-metadata rows with the same symbol_name stay separate, the `edges` CTE restricts ALL metadata edges (not just synthetic aliases) to `tf.has_class_like_kind = 1`, and two new CTEs resolve the remaining false-positive shapes: `metadata_raw_suppression` drops the raw C# `[Foo]` row when the synthetic `FooAttribute` alias already resolves to a class-like target in the same source file (so `[Foo]` does not fan out to both `class FooAttribute` and a plain `class Foo` that shares the bare name), and `target_ambiguity` drops metadata edges when the same-named symbol resolves to multiple class-like definitions in scope (so ambiguous `[MyAudit]` usage is left to `impact` / `references` to disambiguate rather than being fanned out to every same-named attribute class). The existing `GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies` test (which pins `[JsonConverter(...)]` → `class JsonConverter : Attribute` for a target class whose name does NOT end in `Attribute`) still passes because only one class-like target exists in scope, so `target_ambiguity` keeps it, the `is_metadata` / class-like restriction accepts it, and `metadata_raw_suppression` finds no synthetic `JsonConverterAttribute` alias target and therefore does not suppress the raw row. Added `DbReaderTests.GetFileDependencies_CSharpAttributeRawDoesNotLeakToBareNameClass_WhenSuffixTargetExists` (raw `[MyAudit]` row is suppressed when the alias resolves to `class MyAuditAttribute`, so `[MyAudit]` does NOT depend on an unrelated plain `class MyAudit` file), `DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotLeakToSameNameMethodOrProperty` (fully-qualified `[MyAuditAttribute]` metadata does NOT depend on a file that only defines a method / property named `MyAuditAttribute`), and `DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotFanOutWhenMultipleSameNameAttributeClasses` (two same-named attribute classes in different namespaces cause the metadata edge to be dropped as ambiguous). Both the `impact` ambiguity guard (`IsMetadataTargetUnambiguous`) and the `deps` ambiguity CTE (`target_ambiguity`) are additionally tightened to count class-like definitions at symbol-identity level instead of path level: `IsMetadataTargetUnambiguous` now uses `SELECT DISTINCT f.path, s.line, s.name` so two same-named class-like definitions in one source file (e.g. idiomatic C# `namespace A { class FooAttribute { } } namespace B { class FooAttribute { } }` in a single .cs file) register as ambiguous; `target_ambiguity` joins `target_files` back through `files` + `symbols` with `COUNT(*)` over class-like symbol rows so the same same-file duplicate case correctly drops the `[Foo]` metadata edge. Path-level counting previously treated these same-file duplicates as count=1 because `SELECT DISTINCT f.path` and `target_files` (grouped by `dst.path`) both collapse the two definitions into a single row, so `cdidx deps` and `cdidx impact` were confidently reporting an ambiguous short-name metadata reference as a definite dependency / blast-radius edge — contradicting the design intent that "ambiguity" includes duplicates in one file. Added `DbReaderTests.GetFileDependencies_CSharpAttributeAmbiguityCountsSameFileDuplicateClassDefinitions` and `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CountsSameFileDuplicateDefinitions` to pin the symbol-identity-level counting for both `deps` and `impact`. Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. - **MCP `callers` / `callees` `kind` schema, CLI help, and README `--kind` option row no longer advertise metadata kinds as valid filter values (#293 follow-up)** — The `callers` / `callees` handlers reject `kind: "attribute"` / `kind: "annotation"` with a usage error (CLI) / `isError` tool response (MCP), but the MCP tool schema's `kind` property description still listed all five kinds as if they were valid filter values (`"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"`), `cdidx --help` similarly lumped `callers` / `callees` together with `references` for the `--kind` help row, and the README `--kind` option table (EN line 436 / JP line 1307) still directed readers to pass `--kind attribute` or `--kind annotation` on `callers` / `callees`. Schema and handler now agree: the `callers` / `callees` `kind` description now reads `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."`, the CLI `--kind` help row splits `references` (accepts all five kinds) from `callers` / `callees` (call-graph kinds only, metadata rejected, redirects to `references`), and the README `--kind` row (both English and Japanese) now explicitly states that `callers` / `callees` reject `--kind attribute|annotation` with a usage error and points to `references --kind attribute|annotation` as the correct metadata-enumeration path. The MCP `references` tool description is unchanged because it still accepts every indexed kind. Added `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` to pin the corrected schema wording. Affected: `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `src/CodeIndex/Cli/ConsoleUi.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `README.md`. - **`deps`, `impact`, `references`, `inspect`, and `analyze_symbol` retain metadata references as compile-time dependency edges and canonicalize the C# attribute-suffix convention (#293 follow-up)** — `DbReader.GetFileDependencies` (backing `cdidx deps`) and `DbReader.GetFileDependencyHintsToResolvedType` (backing `cdidx impact` / MCP `impact_analysis` heuristic file-hint fallback) keep metadata references in their logical reference CTE. Renaming or removing an attribute class still breaks annotated sites such as `[JsonConverterAttribute]` at compile time, so file-level blast-radius analysis must surface those edges as real dependencies — now labeled `attribute` / `annotation` in `logical_reference_kind` (previously these same sites leaked in as phantom `call` rows before classification was corrected). `GetFileDependencies` additionally canonicalizes the C# attribute naming convention: idiomatic `[Foo]` usage is stored with `symbol_name = "Foo"` but the defining class is `FooAttribute`, so the logical-reference CTE now UNION-ALLs a suffix-aliased copy of every C# `attribute`-kind row with `'Attribute'` appended (only when the original symbol_name does not already end in `Attribute`) so the target-file join reaches the attribute class file. `GetFileDependencyHintsToResolvedType` also aggregates a per-candidate `has_metadata_ref` flag and bypasses the `SourceFileHasStructuredTypeEvidence` guard for edges whose contributing references are purely metadata, so pure-attribute consumer files such as `[MyAudit] class Svc` now surface through `impact MyAuditAttribute` instead of being filtered out. `ResolveImpactFallbackNames` symmetrically adds suffix-stripped aliases for C# class names ending in `Attribute`, so the BFS-less file-hint path can find `[Foo]` sites whose source symbol_name does not match the defining class name. The same canonicalization is propagated to the symbol-level reference reader: `DbReader.SearchReferences`, `CountSearchReferences`, and `CountSearchReferencesTotal` (backing `references`, `inspect`, MCP `analyze_symbol`) now compute a C#-only `queryAttributeAlias` (defined in the new `ComputeCSharpAttributeSuffixAlias` helper) and OR it into both exact (folded and NOCASE) and substring WHERE clauses, so `references MyAuditAttribute` (and `inspect MyAuditAttribute` / `analyze_symbol MyAuditAttribute`) surface the idiomatic `[MyAudit]` call site consistently with `deps` / `impact`. Substring mode uses an exact-OR for the alias (not a second LIKE) so user queries like `MyAuditAttribute` do not over-match unrelated names such as `FooAuditLog`. The alias is C# only, so Java / Kotlin / Scala / TypeScript annotation sites remain unaffected. `callers` / `callees` intentionally skip the alias because they filter to the shared call-graph kinds (`call`, `instantiate`, `subscribe`) and reject `--kind attribute|annotation` at the CLI / MCP boundary — those commands model the dynamic call graph, not the dependency graph. The alias branch is additionally scoped so it cannot produce false positives: `ComputeCSharpAttributeSuffixAlias` returns `null` when `referenceKind` is a non-`attribute` call-graph kind (so `references FooAttribute --kind call` stays strict and does not match `Foo()`), and the SQL alias disjunct in all three reader methods is clamped to `f.lang = 'csharp' AND r.reference_kind = 'attribute'` so an unscoped `references FooAttribute` (no `--lang` / no `--kind`) still cannot bleed into Java annotations or C# call rows. Suffix detection is also case-insensitive (`OrdinalIgnoreCase`) so `references myauditattribute` and `inspect MyAuditATTRIBUTE` still produce the `MyAudit` alias, consistent with the NOCASE / folded contract of the surrounding exact / substring query paths. Added regression coverage `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies` (runtime + metadata edges both surface), `DbReaderTests.GetFileDependencies_MatchesCSharpAttributeSuffixConvention` (`[MyAudit]` → `MyAuditAttribute` canonicalization through `deps`), `DbReaderTests.SearchReferences_MatchesCSharpAttributeSuffixConvention_Substring` / `_Exact` / `_CSharpAttributeSuffixAliasDoesNotBleedToOtherLanguages` (same canonicalization through `references` / `inspect` / `analyze_symbol`, plus C#-only scope guard), `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_NotAppliedToCallKind` (alias must not fire under `--kind call`), `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_UnscopedLangStillLimitsToCSharpAttributeRows` (unscoped query must not match Java annotations or C# call rows), and `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_CaseInsensitiveQuery` (lowercase / mixed-case queries still produce the alias). Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. - **`callers` / `callees` now reject `--kind attribute|annotation` at the CLI and MCP boundary, and Swift / Gradle `@` metadata is reclassified as `annotation` (#293 follow-up)** — Metadata rows (`attribute` / `annotation`) are attributed to their enclosing body-range symbol rather than the annotated target, so `callers Obsolete --kind attribute` would silently return `[Obsolete] void M()` under the enclosing class (not `M`), and file-level targets such as `[assembly: CLSCompliant]` or Kotlin `@file:JvmName` would drop entirely because `container_name` is `NULL`. `QueryCommandRunner.RunCallers` / `RunCallees` now reject `--kind attribute` and `--kind annotation` with `CommandExitCodes.UsageError`, and the MCP `callers` / `callees` tools return an `isError` tool response; both paths direct users to `references --kind attribute|annotation`, which IS a correct metadata enumeration path. Separately, `AnnotationLanguages` now includes `swift` and `gradle`, so Swift `@available(iOS 13.0, *)` / `@objc` / `@MainActor` and Gradle/Groovy `@CompileStatic` / `@TaskAction` are recorded as `annotation` instead of `call` (previously the `@Name(args)` forms leaked into `callers` / `callees` / `hotspots` / `impact` as phantom call edges, and the no-arg `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` forms dropped from the index entirely because no-arg annotation emission was gated to languages in this set). Added `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError` (theory, 4 cases), `McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError` (theory, 4 cases), `ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`, and `ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation`. Affected: `src/CodeIndex/Cli/QueryCommandRunner.cs`, `src/CodeIndex/Mcp/McpToolHandlers.cs`, `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. @@ -686,7 +686,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **DEVELOPER_GUIDE が SQLite の位置付けとリリース番号の例示を明確化** — cdidx が「設定ゼロ・単一ファイル・本番依存1個」の CLI であり、その唯一の本番依存が `Microsoft.Data.Sqlite` であることを明記した。あわせて、リリース手順のチェックリスト中に出てくる `1.9.0` は固定値ではなく例示であり、実際のリリース番号へ読み替える前提だと明示した。対象: `DEVELOPER_GUIDE.md`。 #### 修正 -- **`deps` の C# attribute サフィックス別名を class-like ターゲットに限定し、`impact` の metadata bypass も class-like ターゲット名が名前空間をまたいで曖昧な場合は抑止するよう修正 (#293 追加対応)** — 直前の #293 追加対応では、`deps` の logical-reference CTE に C# 限定で `symbol_name || 'Attribute'` のサフィックス別名を UNION することで `[Foo]` の attribute consumer が `FooAttribute` クラスファイルへ到達するようにし、`impact`(`GetFileDependencyHintsToResolvedType`)も寄与参照に `attribute` / `annotation` が含まれていれば structured-evidence guard をバイパスするようにしていたが、どちらも広すぎた。`deps` 側では別名が純粋に名前だけで join していたため、`[Foo]` が同名の関数・プロパティ・変数(例: 無関係なヘルパーの `void MyAuditAttribute()`)を定義するファイルにも依存を張ってしまっていた。`impact` 側の metadata bypass も、対象名が一意に class-like 定義を指すかを確認していなかったため、`MyAuditAttribute` のような短い名前が複数の namespace に存在すると rename / remove の blast-radius が過剰報告されていた(`attribute` / `annotation` 参照行は短い名前しか保持できず曖昧性解消ができない)。`DbReader.GetFileDependencies` は合成された別名行に `is_attribute_alias = 1`(本物の行は `0`)をタグ付けし、`source_name_counts` では `MIN` でフラグを伝播し、`edges` CTE の join で別名 edge をターゲット側 kind が `class` / `struct` / `interface` のときだけに制限するようにした。`target_files` CTE も `DISTINCT (path, lang, name, kind)` から `GROUP BY (path, lang, name)` + `MAX(...)` の `has_class_like_kind` フラグに切り替えたため、class と同名関数(例: C# コンストラクタ `Target()` と `class Target`)を同じファイルで定義しているケースでもターゲット行が 2 本に増えず `deps` の参照数を水増ししない。`DbReader.GetFileDependencyHintsToResolvedType` は新ヘルパー `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` を呼び、*active な impact スコープ内* で同名の class-like 定義数を数える(graph 対応言語に加えて、呼び出し側の `--lang` / `--path` / `--exclude-path` / `--exclude-tests` を同じフィルタで適用)。in-scope でその数が 1 以下のときに限り metadata evidence-guard bypass を適用する。スコープ内で曖昧なときは metadata 単独 edge も通常の `SourceFileHasStructuredTypeEvidence` に戻り、純 `[MyAudit]` consumer は誤帰属せずに落ちる。スコープ尊重が重要: 別言語 / 別 path / test ツリーにある同名クラスが C# の metadata bypass を潰してはならない(参照側が既に graph 対応 join で言語修飾されているため、曖昧性は scope 内で判定すべき)。README の `impact` 契約も「active な `--lang` / `--path` / `--exclude-path` / `--exclude-tests` スコープ」と明記している。既存の C# attribute deps テスト `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`、曖昧ではない単一定義の `impact` テスト、および既存の graph-queries dedupe テストは全てグリーンのまま。回帰テストとして `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets`(同名関数があっても虚偽 edge が出ないこと)、`DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget`(impact スコープ内で `MyAuditAttribute` が複数ある場合に metadata heuristic hint を抑止)、`DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous`(単一クラス定義のみのときは従来どおり `[MyAudit]` consumer を浮かせる)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope`(別言語の同名 annotation は `--lang csharp` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope`(スコープ外 `src/B/` の同名クラスは `src/A/` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests`(test ツリーの同名クラスは `--exclude-tests` クエリを潰さない)を追加。`ResolveImpactFallbackNames` も併せて引き締め、C# の `Attribute` サフィックス別名は *解決済み定義自身* の名前にのみ適用するようにした: 以前は同一ファイル内の fallback 名全てに別名を付けていたため、`FooAttribute` を定義するファイルに nested で `BarAttribute` が存在するだけで `impact FooAttribute` が `[Bar]` 利用サイトを `FooAttribute` の影響範囲として誤報告していた。`DbReaderTests.GetFileDependencyHints_CSharpAttributeSuffixAlias_DoesNotLeakToSameFileSiblings` を追加して固定。さらに `GetFileDependencies` を再構成し、metadata 行(`attribute` / `annotation`)が bare-name の call-graph ターゲットに漏れないようにした: `logical_references` CTE に `is_metadata` フラグを付与し、`source_name_counts` の GROUP BY を `(source_file_id, source_path, source_lang, symbol_name, is_attribute_alias, is_metadata)` に拡張して metadata と非 metadata で同じ symbol_name の行を別グループ化する。`edges` CTE は全 metadata edge(合成別名だけでなく実体行も)に `tf.has_class_like_kind = 1` を要求し、さらに 2 つの CTE で残る誤検知を潰す: `metadata_raw_suppression` は同じ source file で合成別名 `FooAttribute` が class-like ターゲットに解決できているとき、生の C# `[Foo]` 行を落とす(`[Foo]` が `class FooAttribute` と無関係な同名 `class Foo` に fan-out しない)。`target_ambiguity` はスコープ内で同名 class-like ターゲットが複数あるとき metadata edge を落とす(曖昧な `[MyAudit]` は deps で fan-out させず、`impact` / `references` に判断を委ねる)。既存の `GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies`(`[JsonConverter(...)]` → `class JsonConverter : Attribute` をターゲット名に `Attribute` サフィックスがなくても固定)は、class-like ターゲットが 1 つしかないので `target_ambiguity` で残り、`is_metadata` / class-like 制限も受理し、合成 `JsonConverterAttribute` 別名は class-like に解決できないので `metadata_raw_suppression` も発火せず、そのまま通る。回帰テストとして `DbReaderTests.GetFileDependencies_CSharpAttributeRawDoesNotLeakToBareNameClass_WhenSuffixTargetExists`(別名が `class MyAuditAttribute` に解決できたとき生 `[MyAudit]` 行は抑止され、無関係な plain `class MyAudit` ファイルに依存しない)、`DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotLeakToSameNameMethodOrProperty`(完全形 `[MyAuditAttribute]` は同名メソッド / プロパティしか持たないファイルに依存しない)、`DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotFanOutWhenMultipleSameNameAttributeClasses`(別名前空間に同名 attribute クラスが 2 つあると metadata edge はあいまいとして落とす)を追加。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 +- **`deps` の C# attribute サフィックス別名を class-like ターゲットに限定し、`impact` の metadata bypass も class-like ターゲット名が名前空間をまたいで曖昧な場合は抑止するよう修正 (#293 追加対応)** — 直前の #293 追加対応では、`deps` の logical-reference CTE に C# 限定で `symbol_name || 'Attribute'` のサフィックス別名を UNION することで `[Foo]` の attribute consumer が `FooAttribute` クラスファイルへ到達するようにし、`impact`(`GetFileDependencyHintsToResolvedType`)も寄与参照に `attribute` / `annotation` が含まれていれば structured-evidence guard をバイパスするようにしていたが、どちらも広すぎた。`deps` 側では別名が純粋に名前だけで join していたため、`[Foo]` が同名の関数・プロパティ・変数(例: 無関係なヘルパーの `void MyAuditAttribute()`)を定義するファイルにも依存を張ってしまっていた。`impact` 側の metadata bypass も、対象名が一意に class-like 定義を指すかを確認していなかったため、`MyAuditAttribute` のような短い名前が複数の namespace に存在すると rename / remove の blast-radius が過剰報告されていた(`attribute` / `annotation` 参照行は短い名前しか保持できず曖昧性解消ができない)。`DbReader.GetFileDependencies` は合成された別名行に `is_attribute_alias = 1`(本物の行は `0`)をタグ付けし、`source_name_counts` では `MIN` でフラグを伝播し、`edges` CTE の join で別名 edge をターゲット側 kind が `class` / `struct` / `interface` のときだけに制限するようにした。`target_files` CTE も `DISTINCT (path, lang, name, kind)` から `GROUP BY (path, lang, name)` + `MAX(...)` の `has_class_like_kind` フラグに切り替えたため、class と同名関数(例: C# コンストラクタ `Target()` と `class Target`)を同じファイルで定義しているケースでもターゲット行が 2 本に増えず `deps` の参照数を水増ししない。`DbReader.GetFileDependencyHintsToResolvedType` は新ヘルパー `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` を呼び、*active な impact スコープ内* で同名の class-like 定義数を数える(graph 対応言語に加えて、呼び出し側の `--lang` / `--path` / `--exclude-path` / `--exclude-tests` を同じフィルタで適用)。in-scope でその数が 1 以下のときに限り metadata evidence-guard bypass を適用する。スコープ内で曖昧なときは metadata 単独 edge も通常の `SourceFileHasStructuredTypeEvidence` に戻り、純 `[MyAudit]` consumer は誤帰属せずに落ちる。スコープ尊重が重要: 別言語 / 別 path / test ツリーにある同名クラスが C# の metadata bypass を潰してはならない(参照側が既に graph 対応 join で言語修飾されているため、曖昧性は scope 内で判定すべき)。README の `impact` 契約も「active な `--lang` / `--path` / `--exclude-path` / `--exclude-tests` スコープ」と明記している。既存の C# attribute deps テスト `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`、曖昧ではない単一定義の `impact` テスト、および既存の graph-queries dedupe テストは全てグリーンのまま。回帰テストとして `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets`(同名関数があっても虚偽 edge が出ないこと)、`DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget`(impact スコープ内で `MyAuditAttribute` が複数ある場合に metadata heuristic hint を抑止)、`DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous`(単一クラス定義のみのときは従来どおり `[MyAudit]` consumer を浮かせる)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope`(別言語の同名 annotation は `--lang csharp` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope`(スコープ外 `src/B/` の同名クラスは `src/A/` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests`(test ツリーの同名クラスは `--exclude-tests` クエリを潰さない)を追加。`ResolveImpactFallbackNames` も併せて引き締め、C# の `Attribute` サフィックス別名は *解決済み定義自身* の名前にのみ適用するようにした: 以前は同一ファイル内の fallback 名全てに別名を付けていたため、`FooAttribute` を定義するファイルに nested で `BarAttribute` が存在するだけで `impact FooAttribute` が `[Bar]` 利用サイトを `FooAttribute` の影響範囲として誤報告していた。`DbReaderTests.GetFileDependencyHints_CSharpAttributeSuffixAlias_DoesNotLeakToSameFileSiblings` を追加して固定。さらに `GetFileDependencies` を再構成し、metadata 行(`attribute` / `annotation`)が bare-name の call-graph ターゲットに漏れないようにした: `logical_references` CTE に `is_metadata` フラグを付与し、`source_name_counts` の GROUP BY を `(source_file_id, source_path, source_lang, symbol_name, is_attribute_alias, is_metadata)` に拡張して metadata と非 metadata で同じ symbol_name の行を別グループ化する。`edges` CTE は全 metadata edge(合成別名だけでなく実体行も)に `tf.has_class_like_kind = 1` を要求し、さらに 2 つの CTE で残る誤検知を潰す: `metadata_raw_suppression` は同じ source file で合成別名 `FooAttribute` が class-like ターゲットに解決できているとき、生の C# `[Foo]` 行を落とす(`[Foo]` が `class FooAttribute` と無関係な同名 `class Foo` に fan-out しない)。`target_ambiguity` はスコープ内で同名 class-like ターゲットが複数あるとき metadata edge を落とす(曖昧な `[MyAudit]` は deps で fan-out させず、`impact` / `references` に判断を委ねる)。既存の `GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies`(`[JsonConverter(...)]` → `class JsonConverter : Attribute` をターゲット名に `Attribute` サフィックスがなくても固定)は、class-like ターゲットが 1 つしかないので `target_ambiguity` で残り、`is_metadata` / class-like 制限も受理し、合成 `JsonConverterAttribute` 別名は class-like に解決できないので `metadata_raw_suppression` も発火せず、そのまま通る。回帰テストとして `DbReaderTests.GetFileDependencies_CSharpAttributeRawDoesNotLeakToBareNameClass_WhenSuffixTargetExists`(別名が `class MyAuditAttribute` に解決できたとき生 `[MyAudit]` 行は抑止され、無関係な plain `class MyAudit` ファイルに依存しない)、`DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotLeakToSameNameMethodOrProperty`(完全形 `[MyAuditAttribute]` は同名メソッド / プロパティしか持たないファイルに依存しない)、`DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotFanOutWhenMultipleSameNameAttributeClasses`(別名前空間に同名 attribute クラスが 2 つあると metadata edge はあいまいとして落とす)を追加。加えて `impact` の曖昧性ガード (`IsMetadataTargetUnambiguous`) と `deps` の曖昧性 CTE (`target_ambiguity`) も、path 単位ではなく symbol identity 単位で class-like 定義を数えるように引き締めた: `IsMetadataTargetUnambiguous` は `SELECT DISTINCT f.path, s.line, s.name` に変更し、C# でよくある 1 つの .cs ファイル内に別名前空間で同名 class-like が 2 つ定義されている形 (`namespace A { class FooAttribute { } } namespace B { class FooAttribute { } }`) でも ambiguity を 2 として検出する。`target_ambiguity` は `target_files` を `files` + `symbols` に JOIN し直して `COUNT(*)` を class-like symbol 行に対して取るため、同じ同名ファイル内重複ケースでも `[Foo]` metadata edge を正しく落とせる。path 単位カウントでは `SELECT DISTINCT f.path` も `target_files`(`dst.path` で GROUP BY)も同一ファイル内の 2 定義を 1 行に潰してしまうため、従来は count=1 として曖昧性を取りこぼし、`cdidx deps` と `cdidx impact` が同名 short-name metadata 参照を確定的な依存 / blast-radius edge として出していた。これは曖昧性に "duplicates in one file" を含める前提の設計契約に反していた。`deps` と `impact` 両方で symbol-identity 単位カウントを固定する `DbReaderTests.GetFileDependencies_CSharpAttributeAmbiguityCountsSameFileDuplicateClassDefinitions` と `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CountsSameFileDuplicateDefinitions` を追加。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 - **MCP の `callers` / `callees` の `kind` schema・CLI help・README の `--kind` オプション行が metadata kinds を有効フィルタ値として広告しないよう修正 (#293 追加対応)** — `callers` / `callees` の handler は `kind: "attribute"` / `kind: "annotation"` を usage error(CLI)/ `isError` ツールレスポンス(MCP)で拒否するにもかかわらず、MCP ツール schema の `kind` プロパティ説明は 5 種すべてを列挙した `"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"` のままで、`cdidx --help` の `--kind` 行も `callers` / `callees` を `references` と同じ行にまとめていた。さらに README の `--kind` オプション表(英語 436 行目 / 日本語 1307 行目)も、`callers` / `callees` で `--kind attribute` / `--kind annotation` を渡せるかのように案内していた。schema と handler の契約を一致させ、`callers` / `callees` の `kind` description を `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."` に修正し、CLI の `--kind` help 行も `references`(5 種すべて受理)と `callers` / `callees`(call-graph 種別のみ、metadata kinds は拒否して `references` へ誘導)を分けるようにし、README の `--kind` 行(英語・日本語の両方)も `callers` / `callees` が `--kind attribute|annotation` を usage error で拒否すること、metadata 列挙の正規経路が `references --kind attribute|annotation` であることを明示する文言に直した。MCP の `references` ツール説明は全 reference kind を受け付ける契約のままなので変更なし。修正後の schema 文言を固定する `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` を追加。対象: `src/CodeIndex/Mcp/McpToolDefinitions.cs`、`src/CodeIndex/Cli/ConsoleUi.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`、`README.md`。 - **`deps`・`impact`・`references`・`inspect`・`analyze_symbol` が metadata 参照を compile-time 依存として保持し、C# の attribute サフィックス命名規約を正規化 (#293 追加対応)** — `cdidx deps` の実装である `DbReader.GetFileDependencies` と、`cdidx impact` / MCP `impact_analysis` の heuristic file-hint フォールバックである `DbReader.GetFileDependencyHintsToResolvedType` は、logical reference CTE に metadata 参照も含める。attribute クラスを rename / 削除すれば `[JsonConverterAttribute]` のような注釈サイトも compile-time で壊れるため、ファイル単位の blast-radius 分析はそれらも本物の依存 edge として表示する必要があり、`logical_reference_kind` では `attribute` / `annotation` として正しくラベル付けされる(分類修正前は同じサイトが phantom `call` として漏れていた)。さらに `GetFileDependencies` は C# の attribute 命名規約を正規化する: 慣用的な `[Foo]` は `symbol_name = "Foo"` で保存されるが、定義クラスは `FooAttribute` 命名となるため、logical-reference CTE に C# `attribute` 種別の行をサフィックス `'Attribute'` を付与した別名コピーとして UNION ALL で追加する(元の symbol_name が既に `Attribute` で終わっている場合は重複しないようスキップ)。これにより target ファイル join が attribute クラス側のファイルへ到達する。`GetFileDependencyHintsToResolvedType` はさらに candidate ごとに `has_metadata_ref` フラグを集約し、寄与する参照が metadata だけである edge については `SourceFileHasStructuredTypeEvidence` guard をバイパスするため、`[MyAudit] class Svc` のような純 attribute consumer ファイルも `impact MyAuditAttribute` で出るようになった(以前は filter で落ちていた)。`ResolveImpactFallbackNames` も対称的に、`Attribute` で終わる C# クラス名にはサフィックスを外した別名を追加するため、BFS を使わない file-hint 経路でも source symbol_name が定義クラス名と一致しない `[Foo]` サイトを発見できる。同じ正規化をシンボル単位の参照 reader にも伝播する: `references`・`inspect`・MCP `analyze_symbol` を支える `DbReader.SearchReferences`・`CountSearchReferences`・`CountSearchReferencesTotal` は、新しいヘルパー `ComputeCSharpAttributeSuffixAlias` で C# 限定の `queryAttributeAlias` を計算し、exact(folded と NOCASE)・substring の両方の WHERE 句に OR で追加する。これにより `references MyAuditAttribute`(および `inspect MyAuditAttribute` / `analyze_symbol MyAuditAttribute`)でも慣用的な `[MyAudit]` 参照サイトが `deps` / `impact` と一貫して surface する。substring モードでは alias を二つ目の LIKE にせず exact-OR で束ねているため、ユーザーが `MyAuditAttribute` と打った時に `FooAuditLog` のような無関係な名前に over-match しない。alias は C# 限定なので、Java / Kotlin / Scala / TypeScript の annotation サイトには影響しない。`callers` / `callees` は共有の call-graph 種別 (`call` / `instantiate` / `subscribe`) に絞って `--kind attribute|annotation` を CLI / MCP 境界で拒否する契約なので、alias は意図的に適用しない(dynamic call graph と dependency graph は別契約)。さらに alias 節が誤一致を生まないようスコープする: `ComputeCSharpAttributeSuffixAlias` は `referenceKind` が `attribute` 以外の call-graph 種別のときは `null` を返す(`references FooAttribute --kind call` は厳格一致のままで `Foo()` に一致しない)、3 つの reader メソッドの SQL alias 節は `f.lang = 'csharp' AND r.reference_kind = 'attribute'` に限定するため、`--lang` / `--kind` 無指定の `references FooAttribute` でも Java の annotation や C# の call 行を拾わない。suffix 検出も `OrdinalIgnoreCase` にして case-insensitive 化したため、`references myauditattribute` / `inspect MyAuditATTRIBUTE` のような小文字・混在ケースでも alias `MyAudit` を生成でき、周辺の NOCASE / folded 契約と整合する。回帰テスト `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies`(runtime と metadata の両方の edge が出ることを固定)、`DbReaderTests.GetFileDependencies_MatchesCSharpAttributeSuffixConvention`(`deps` 経由での `[MyAudit]` → `MyAuditAttribute` 正規化)、`DbReaderTests.SearchReferences_MatchesCSharpAttributeSuffixConvention_Substring` / `_Exact` / `_CSharpAttributeSuffixAliasDoesNotBleedToOtherLanguages`(`references` / `inspect` / `analyze_symbol` での同一正規化と C# 限定スコープ)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_NotAppliedToCallKind`(`--kind call` で alias が発火しないこと)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_UnscopedLangStillLimitsToCSharpAttributeRows`(無指定でも Java annotation や C# call 行に一致しないこと)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_CaseInsensitiveQuery`(小文字・混在ケースのクエリでも alias を生成)を追加。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 - **`callers` / `callees` が CLI と MCP 境界で `--kind attribute|annotation` を拒否し、Swift / Gradle の `@` metadata を `annotation` に再分類 (#293 追加対応)** — metadata 行 (`attribute` / `annotation`) は注釈対象そのものではなく body-range の外側シンボルに帰属するため、`callers Obsolete --kind attribute` は `[Obsolete] void M()` を `M` ではなく外側クラスの下に黙って返し、`[assembly: CLSCompliant]` や Kotlin の `@file:JvmName` のような file-level target は `container_name = NULL` で完全脱落していた。`QueryCommandRunner.RunCallers` / `RunCallees` は `--kind attribute` / `--kind annotation` を `CommandExitCodes.UsageError` で弾き、MCP の `callers` / `callees` ツールは `isError` ツールレスポンスを返すようにした。どちらの経路も、metadata 列挙の正しいパスである `references --kind attribute|annotation` へ誘導する。あわせて `AnnotationLanguages` に `swift` と `gradle` を追加し、Swift の `@available(iOS 13.0, *)` / `@objc` / `@MainActor` や Gradle / Groovy の `@CompileStatic` / `@TaskAction` を `call` ではなく `annotation` として記録するようにした(修正前は引数付きの `@Name(args)` 形は `callers` / `callees` / `hotspots` / `impact` に phantom call edge として漏れ、引数なしの `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` 形はこの集合で gate されていた no-arg annotation 出力から外れてインデックス自体に残らなかった)。回帰テストとして `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError`(theory 4 ケース)、`McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError`(theory 4 ケース)、`ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`、`ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation` を追加。対象: `src/CodeIndex/Cli/QueryCommandRunner.cs`、`src/CodeIndex/Mcp/McpToolHandlers.cs`、`src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 diff --git a/src/CodeIndex/Database/DbReader.cs b/src/CodeIndex/Database/DbReader.cs index ab393d49d9..5df7778456 100644 --- a/src/CodeIndex/Database/DbReader.cs +++ b/src/CodeIndex/Database/DbReader.cs @@ -1934,9 +1934,20 @@ private bool IsMetadataTargetUnambiguous( return false; using var cmd = _conn.CreateCommand(); var supportedLangFilter = BuildGraphSupportedLanguagePredicate(cmd, "f", "metadataAmbigLang"); + // Count at symbol-identity level (path + line + name) rather than at path + // level, so two same-named class-like definitions in the same source file + // (e.g. `namespace A { class MyAuditAttribute { } } namespace B { class + // MyAuditAttribute { } }` both in one .cs file) still register as ambiguous. + // DISTINCT f.path alone would collapse them to 1 and falsely trigger the + // metadata bypass. + // 曖昧性は path 単位ではなく symbol identity 単位 (path + line + name) で数える。 + // 同じ .cs ファイル内に別名前空間で同名の class-like が 2 つあるケース + // (例: `namespace A { class MyAuditAttribute { } } namespace B { class + // MyAuditAttribute { } }`) でも ambiguity を 2 として検出できる。DISTINCT + // f.path のままだと 1 に潰れ、metadata bypass が誤って有効化される。 var sql = $@" SELECT COUNT(*) FROM ( - SELECT DISTINCT f.path + SELECT DISTINCT f.path, s.line, s.name FROM symbols s JOIN files f ON s.file_id = f.id WHERE s.name = @metadataAmbigName COLLATE NOCASE @@ -2690,19 +2701,37 @@ JOIN target_files tf_alias AND lrp.symbol_name NOT LIKE '%Attribute' ), target_ambiguity AS ( - -- Count distinct class-like target files per (target_lang, symbol_name). - -- Metadata references ([Foo] / @Foo) must not fan out to multiple - -- same-name attribute / annotation class definitions; when ambiguous, - -- drop the metadata edge and let `impact` / `references` guide the user. - -- (target_lang, symbol_name) 単位で class 系 target ファイル数を数える。 - -- metadata 参照 ([Foo] / @Foo) は同名 attribute / annotation クラスが - -- 複数あるとき fan-out させず、その metadata エッジを落とす。 - SELECT target_lang, - symbol_name, - COUNT(DISTINCT target_path) AS class_like_target_count - FROM target_files - WHERE has_class_like_kind = 1 - GROUP BY target_lang, symbol_name + -- Count class-like definitions at symbol-identity level rather than + -- file level. Two same-named class-like definitions in the same file + -- (e.g. `namespace A { class FooAttribute { } } namespace B { class + -- FooAttribute { } }` both inside one .cs file) collapse to a single + -- target_files row because target_files is GROUPed by dst.path, so + -- COUNT(DISTINCT target_path) alone would see count=1 and falsely + -- treat the metadata target as unambiguous. Joining target_files back + -- through files + symbols recovers the per-definition row count while + -- still inheriting target_files' lang / path / graph-supported scope + -- (since the join only keeps rows whose (path, lang, name) already + -- appear in target_files). + -- class-like 定義は path 単位ではなく symbol identity 単位で数える。 + -- 同じ .cs ファイル内に別名前空間で同名 class-like が 2 つあるケースは + -- target_files (dst.path で GROUP BY) 上では 1 行に潰れており、 + -- COUNT(DISTINCT target_path) だけでは count=1 となり metadata target + -- が一意と誤判定される。target_files から files + symbols に JOIN し直す + -- ことで定義単位の件数を復元する。JOIN が target_files 既存行にしか + -- 当たらないため、lang / path / graph-supported スコープはそのまま継承。 + SELECT tf.target_lang, + tf.symbol_name, + COUNT(*) AS class_like_target_count + FROM target_files tf + JOIN files dst + ON dst.path = tf.target_path + AND dst.lang = tf.target_lang + JOIN symbols s + ON s.file_id = dst.id + AND s.name = tf.symbol_name + AND s.kind IN ('class','struct','interface') + WHERE tf.has_class_like_kind = 1 + GROUP BY tf.target_lang, tf.symbol_name ), edges AS ( SELECT snc.source_path, diff --git a/tests/CodeIndex.Tests/DbReaderTests.cs b/tests/CodeIndex.Tests/DbReaderTests.cs index 4d18e0c91d..2d43a3a347 100644 --- a/tests/CodeIndex.Tests/DbReaderTests.cs +++ b/tests/CodeIndex.Tests/DbReaderTests.cs @@ -2439,6 +2439,62 @@ public class Svc Assert.DoesNotContain(dependencies, d => d.SourcePath == "src/Svc.cs" && d.TargetPath == "src/Helpers.cs"); } + [Fact] + public void GetFileDependencies_CSharpAttributeAmbiguityCountsSameFileDuplicateClassDefinitions() + { + // issue #293 follow-up: when a single source file defines TWO same-named + // class-like attribute targets under different namespaces (idiomatic C# + // with multiple `namespace { ... }` blocks in one .cs file), the metadata + // edge must be dropped as ambiguous just like the multi-file case. A + // path-level count (COUNT DISTINCT target_path) would see `count = 1` + // because both definitions live in the same file, so the previous + // target_ambiguity CTE falsely treated the target as unambiguous. The + // rewritten CTE joins back through files + symbols so it counts at + // symbol-identity level and correctly sees `count = 2`. + // issue #293 補足: 1 つの .cs ファイルに別名前空間で同名 class-like が 2 つ + // 定義されている場合 (C# でよくある `namespace { ... }` 複数ブロック形式) + // でも、複数ファイルのときと同様に metadata edge は ambiguous として落とす + // 必要がある。path 単位 (COUNT DISTINCT target_path) だと両方が同じ file に + // あるため count=1 となり、従来の target_ambiguity では誤って一意扱いされた。 + // 書き直した CTE は files + symbols に JOIN し直すため、symbol identity 単位 + // で count=2 を正しく検出する。 + InsertIndexedFile("src/MyAuditAttribute.cs", "csharp", + """ + using System; + + namespace A + { + [AttributeUsage(AttributeTargets.Class)] + public sealed class MyAuditAttribute : Attribute + { + } + } + + namespace B + { + [AttributeUsage(AttributeTargets.Class)] + public sealed class MyAuditAttribute : Attribute + { + } + } + """); + InsertIndexedFile("src/Svc.cs", "csharp", + """ + [MyAudit] + public class Svc + { + } + """); + + var dependencies = _reader.GetFileDependencies(limit: 10, lang: "csharp"); + + // Even though both MyAuditAttribute definitions live in the same file, the + // metadata reference is still ambiguous and must not produce a deps edge. + // 同じファイル内にある 2 つの MyAuditAttribute 定義でも metadata 参照は + // 曖昧扱いのため、deps edge を出してはならない。 + Assert.DoesNotContain(dependencies, d => d.SourcePath == "src/Svc.cs" && d.TargetPath == "src/MyAuditAttribute.cs"); + } + [Fact] public void GetFileDependencies_CSharpAttributeDoesNotFanOutWhenMultipleSameNameAttributeClasses() { @@ -2810,6 +2866,66 @@ public class Svc Assert.Contains(result.FileImpacts, f => f.SourcePath == "src/Svc.cs" && f.TargetPath == "src/MyAuditAttribute.cs"); } + [Fact] + public void GetFileDependencyHints_MetadataBypassAmbiguityGuard_CountsSameFileDuplicateDefinitions() + { + // issue #293 follow-up: the `impact` metadata bypass ambiguity guard must + // count class-like definitions at symbol-identity level rather than at path + // level. A single .cs file with two same-named `MyAuditAttribute` class + // declarations under different namespaces is still ambiguous — metadata + // reference rows only keep the short name `MyAudit` and cannot resolve + // between `A.MyAuditAttribute` and `B.MyAuditAttribute`. Previously the + // guard counted `SELECT DISTINCT f.path` so both definitions collapsed to + // 1 and the bypass falsely fired, mis-attributing `[MyAudit]` consumers to + // the impact of a specific target when the true resolution is unknown. + // issue #293 補足: `impact` の metadata bypass 曖昧性ガードは、path 単位 + // ではなく symbol identity 単位で class-like 定義を数える必要がある。1 つの + // .cs ファイル内に別名前空間で `MyAuditAttribute` が 2 つ定義されていても、 + // metadata 参照は短縮名 `MyAudit` しか持たず `A.MyAuditAttribute` と + // `B.MyAuditAttribute` を区別できないため依然として曖昧。従来は + // `SELECT DISTINCT f.path` で数えていたため両定義が 1 に潰れ、bypass が + // 誤って発動し `[MyAudit]` consumer を特定 target の影響範囲へ誤帰属させていた。 + InsertIndexedFile("src/MyAuditAttribute.cs", "csharp", + """ + using System; + + namespace A + { + [AttributeUsage(AttributeTargets.Class)] + public sealed class MyAuditAttribute : Attribute + { + } + } + + namespace B + { + [AttributeUsage(AttributeTargets.Class)] + public sealed class MyAuditAttribute : Attribute + { + } + } + """); + InsertIndexedFile("src/Svc.cs", "csharp", + """ + [MyAudit] + public class Svc + { + } + """); + + var result = _reader.AnalyzeImpact("MyAuditAttribute", maxDepth: 3, limit: 20, lang: "csharp"); + + // Two same-named class-like definitions in one file still make the target + // ambiguous, so the `[MyAudit]` consumer must not surface as a file-level + // impact hint — the metadata evidence bypass should fall through to the + // normal structured-evidence check, which `[MyAudit]`-only consumers fail. + // 同じファイル内の 2 つの同名 class-like 定義でも target は曖昧なので、 + // `[MyAudit]` consumer は file-level impact hint に現れてはいけない。 + // metadata evidence bypass は通常の structured-evidence 判定へフォール + // スルーし、pure `[MyAudit]` consumer はそこで落ちる。 + Assert.DoesNotContain(result.FileImpacts, f => f.SourcePath == "src/Svc.cs" && f.TargetPath == "src/MyAuditAttribute.cs"); + } + [Fact] public void GetFileDependencyHints_CSharpAttributeSuffixAlias_DoesNotLeakToSameFileSiblings() { From 17a7b717fab8c6b63cb1cdae08d138dafe617b86 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 18 Apr 2026 15:02:25 +0900 Subject: [PATCH 25/30] Fix #293 escape IsMetadataTargetUnambiguous LIKE params and accept generic no-arg C# attributes - IsMetadataTargetUnambiguous now wraps path / exclude-path parameters through EscapeLikeQuery and binds them as `%...%`, matching other reader paths (search / references / deps). Without the wrap, CLI-style `--path src/A/` would not match any in-scope definitions, the ambiguity count would underflow to 1, and the metadata bypass would falsely fire even when two same-named attribute classes sit side-by-side in the requested subtree. - CSharpNoArgAttributeRegex now accepts an optional generic argument list `(?:\s*<[^>\n]+>)?` after the attribute name, so `[MyAudit]`, `[assembly: MyAttr]`, and multi-line `[\n MyAttr\n]` are indexed as `attribute` references and routed through the suffix alias to the real `FooAttribute` class file by deps / impact / references / inspect / analyze_symbol. - Added regression tests: ReferenceExtractorTests.Extract_CsharpGenericNoArgAttribute_ClassifiedAsAttribute DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_StillIndexedAndResolvesToAttributeClass DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_AssemblyTarget_IsIndexed DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CliPathPatternEscaping_SuppressesWhenInScopeIsAmbiguous - Updated CHANGELOG (English + Japanese) to reflect the Round 15 additions. Co-Authored-By: Claude Opus 4 --- CHANGELOG.md | 4 +- src/CodeIndex/Database/DbReader.cs | 17 ++- src/CodeIndex/Indexer/ReferenceExtractor.cs | 2 +- tests/CodeIndex.Tests/DbReaderTests.cs | 125 ++++++++++++++++++ .../ReferenceExtractorTests.cs | 29 ++++ 5 files changed, 172 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13b0f212a3..40645d6182 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/), - **Developer Guide now distinguishes SQLite's role from "zero dependencies" and labels release numbers as examples** — Clarified that cdidx remains a zero-configuration, single-file CLI with exactly one production dependency (`Microsoft.Data.Sqlite`), and made the release-workflow checklist explicitly state that `1.9.0` is only an example version to substitute during an actual release. Affected: `DEVELOPER_GUIDE.md`. #### Fixed -- **`deps` C# attribute suffix alias now only matches class-like targets, and `impact` metadata bypass is suppressed when the class-like target name is ambiguous across namespaces (#293 follow-up)** — The earlier `#293` follow-up made `deps` UNION a C#-only `symbol_name || 'Attribute'` suffix alias into the logical-reference CTE so `[Foo]` attribute consumers could reach `FooAttribute` class files, and made `impact` (`GetFileDependencyHintsToResolvedType`) bypass the structured-evidence guard whenever any contributing reference was `attribute` / `annotation`. Both changes were too broad. On `deps` the alias joined purely by name, so `[Foo]` would also depend on any file that merely defined a function / property / variable named `FooAttribute` (e.g. `void MyAuditAttribute()` in an unrelated helper). On `impact` the metadata bypass did not check whether the target name was uniquely a class-like definition, so when the same unqualified name (e.g. `MyAuditAttribute`) existed in multiple namespaces the rename/remove blast radius was over-reported — attribute / annotation reference rows only keep the short name and cannot be disambiguated. `DbReader.GetFileDependencies` now tags synthetic alias rows with `is_attribute_alias = 1` (real rows are `0`), propagates the flag through `source_name_counts` via `MIN`, and restricts alias edges to class-like target kinds (`class` / `struct` / `interface`) in the `edges` CTE join. The `target_files` CTE is also switched from `DISTINCT` over `(path, lang, name, kind)` to `GROUP BY (path, lang, name)` with a `MAX(...)` `has_class_like_kind` flag, so files that define both a class and a same-named function (e.g. the C# constructor `Target()` inside `class Target`) no longer fan out into two target rows and inflate the `deps` reference count. `DbReader.GetFileDependencyHintsToResolvedType` now calls a new `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` helper that counts class-like definitions with the same unqualified name *within the active impact scope* (graph-supported languages, plus the caller's `--lang` / `--path` / `--exclude-path` / `--exclude-tests` filters); the metadata evidence-guard bypass is only applied when that in-scope count is ≤ 1. If the name is ambiguous within scope, metadata-only edges fall through to the normal `SourceFileHasStructuredTypeEvidence` check, so pure `[MyAudit]` consumers are dropped instead of being mis-attributed. Scoping matters: a same-named class in an unrelated language / path / test tree must not suppress the C# metadata bypass, because attribute reference rows are already language-qualified through the graph-supported reference join, and the README contract for `impact` explicitly promises "active `--lang` / `--path` / `--exclude-path` / `--exclude-tests` scope". The C# attribute deps test `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`, the unambiguous single-definition `impact` test, and the pre-existing graph-queries dedupe test all still pass. Added `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets` (ambiguous same-name function must not produce a spurious edge), `DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget` (two `MyAuditAttribute` classes within the same impact scope suppress the metadata heuristic hint), `DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous` (single class definition still surfaces the `[MyAudit]` consumer), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope` (same-named Java annotation must not suppress a `--lang csharp` query), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope` (out-of-scope `src/B/` same-named class must not suppress a `src/A/`-scoped query), and `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests` (test-tree same-named class must not suppress an `--exclude-tests` query). `ResolveImpactFallbackNames` is additionally tightened so the C# `Attribute`-suffix alias is only applied to the resolved definition's own name: previously the alias was applied to every same-file fallback name, so a nested `BarAttribute` inside the file that defines `FooAttribute` caused `impact FooAttribute` to falsely attribute `[Bar]` use sites to `FooAttribute`'s blast radius. Added `DbReaderTests.GetFileDependencyHints_CSharpAttributeSuffixAlias_DoesNotLeakToSameFileSiblings` to pin this. `GetFileDependencies` is additionally restructured so metadata rows (`attribute` / `annotation`) cannot leak onto bare-name call-graph targets: the `logical_references` CTE now carries an `is_metadata` flag, `source_name_counts` groups by `(source_file_id, source_path, source_lang, symbol_name, is_attribute_alias, is_metadata)` so metadata and non-metadata rows with the same symbol_name stay separate, the `edges` CTE restricts ALL metadata edges (not just synthetic aliases) to `tf.has_class_like_kind = 1`, and two new CTEs resolve the remaining false-positive shapes: `metadata_raw_suppression` drops the raw C# `[Foo]` row when the synthetic `FooAttribute` alias already resolves to a class-like target in the same source file (so `[Foo]` does not fan out to both `class FooAttribute` and a plain `class Foo` that shares the bare name), and `target_ambiguity` drops metadata edges when the same-named symbol resolves to multiple class-like definitions in scope (so ambiguous `[MyAudit]` usage is left to `impact` / `references` to disambiguate rather than being fanned out to every same-named attribute class). The existing `GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies` test (which pins `[JsonConverter(...)]` → `class JsonConverter : Attribute` for a target class whose name does NOT end in `Attribute`) still passes because only one class-like target exists in scope, so `target_ambiguity` keeps it, the `is_metadata` / class-like restriction accepts it, and `metadata_raw_suppression` finds no synthetic `JsonConverterAttribute` alias target and therefore does not suppress the raw row. Added `DbReaderTests.GetFileDependencies_CSharpAttributeRawDoesNotLeakToBareNameClass_WhenSuffixTargetExists` (raw `[MyAudit]` row is suppressed when the alias resolves to `class MyAuditAttribute`, so `[MyAudit]` does NOT depend on an unrelated plain `class MyAudit` file), `DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotLeakToSameNameMethodOrProperty` (fully-qualified `[MyAuditAttribute]` metadata does NOT depend on a file that only defines a method / property named `MyAuditAttribute`), and `DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotFanOutWhenMultipleSameNameAttributeClasses` (two same-named attribute classes in different namespaces cause the metadata edge to be dropped as ambiguous). Both the `impact` ambiguity guard (`IsMetadataTargetUnambiguous`) and the `deps` ambiguity CTE (`target_ambiguity`) are additionally tightened to count class-like definitions at symbol-identity level instead of path level: `IsMetadataTargetUnambiguous` now uses `SELECT DISTINCT f.path, s.line, s.name` so two same-named class-like definitions in one source file (e.g. idiomatic C# `namespace A { class FooAttribute { } } namespace B { class FooAttribute { } }` in a single .cs file) register as ambiguous; `target_ambiguity` joins `target_files` back through `files` + `symbols` with `COUNT(*)` over class-like symbol rows so the same same-file duplicate case correctly drops the `[Foo]` metadata edge. Path-level counting previously treated these same-file duplicates as count=1 because `SELECT DISTINCT f.path` and `target_files` (grouped by `dst.path`) both collapse the two definitions into a single row, so `cdidx deps` and `cdidx impact` were confidently reporting an ambiguous short-name metadata reference as a definite dependency / blast-radius edge — contradicting the design intent that "ambiguity" includes duplicates in one file. Added `DbReaderTests.GetFileDependencies_CSharpAttributeAmbiguityCountsSameFileDuplicateClassDefinitions` and `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CountsSameFileDuplicateDefinitions` to pin the symbol-identity-level counting for both `deps` and `impact`. Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. +- **`deps` C# attribute suffix alias now only matches class-like targets, and `impact` metadata bypass is suppressed when the class-like target name is ambiguous across namespaces (#293 follow-up)** — The earlier `#293` follow-up made `deps` UNION a C#-only `symbol_name || 'Attribute'` suffix alias into the logical-reference CTE so `[Foo]` attribute consumers could reach `FooAttribute` class files, and made `impact` (`GetFileDependencyHintsToResolvedType`) bypass the structured-evidence guard whenever any contributing reference was `attribute` / `annotation`. Both changes were too broad. On `deps` the alias joined purely by name, so `[Foo]` would also depend on any file that merely defined a function / property / variable named `FooAttribute` (e.g. `void MyAuditAttribute()` in an unrelated helper). On `impact` the metadata bypass did not check whether the target name was uniquely a class-like definition, so when the same unqualified name (e.g. `MyAuditAttribute`) existed in multiple namespaces the rename/remove blast radius was over-reported — attribute / annotation reference rows only keep the short name and cannot be disambiguated. `DbReader.GetFileDependencies` now tags synthetic alias rows with `is_attribute_alias = 1` (real rows are `0`), propagates the flag through `source_name_counts` via `MIN`, and restricts alias edges to class-like target kinds (`class` / `struct` / `interface`) in the `edges` CTE join. The `target_files` CTE is also switched from `DISTINCT` over `(path, lang, name, kind)` to `GROUP BY (path, lang, name)` with a `MAX(...)` `has_class_like_kind` flag, so files that define both a class and a same-named function (e.g. the C# constructor `Target()` inside `class Target`) no longer fan out into two target rows and inflate the `deps` reference count. `DbReader.GetFileDependencyHintsToResolvedType` now calls a new `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` helper that counts class-like definitions with the same unqualified name *within the active impact scope* (graph-supported languages, plus the caller's `--lang` / `--path` / `--exclude-path` / `--exclude-tests` filters); the metadata evidence-guard bypass is only applied when that in-scope count is ≤ 1. If the name is ambiguous within scope, metadata-only edges fall through to the normal `SourceFileHasStructuredTypeEvidence` check, so pure `[MyAudit]` consumers are dropped instead of being mis-attributed. Scoping matters: a same-named class in an unrelated language / path / test tree must not suppress the C# metadata bypass, because attribute reference rows are already language-qualified through the graph-supported reference join, and the README contract for `impact` explicitly promises "active `--lang` / `--path` / `--exclude-path` / `--exclude-tests` scope". The C# attribute deps test `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`, the unambiguous single-definition `impact` test, and the pre-existing graph-queries dedupe test all still pass. Added `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets` (ambiguous same-name function must not produce a spurious edge), `DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget` (two `MyAuditAttribute` classes within the same impact scope suppress the metadata heuristic hint), `DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous` (single class definition still surfaces the `[MyAudit]` consumer), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope` (same-named Java annotation must not suppress a `--lang csharp` query), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope` (out-of-scope `src/B/` same-named class must not suppress a `src/A/`-scoped query), and `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests` (test-tree same-named class must not suppress an `--exclude-tests` query). `ResolveImpactFallbackNames` is additionally tightened so the C# `Attribute`-suffix alias is only applied to the resolved definition's own name: previously the alias was applied to every same-file fallback name, so a nested `BarAttribute` inside the file that defines `FooAttribute` caused `impact FooAttribute` to falsely attribute `[Bar]` use sites to `FooAttribute`'s blast radius. Added `DbReaderTests.GetFileDependencyHints_CSharpAttributeSuffixAlias_DoesNotLeakToSameFileSiblings` to pin this. `GetFileDependencies` is additionally restructured so metadata rows (`attribute` / `annotation`) cannot leak onto bare-name call-graph targets: the `logical_references` CTE now carries an `is_metadata` flag, `source_name_counts` groups by `(source_file_id, source_path, source_lang, symbol_name, is_attribute_alias, is_metadata)` so metadata and non-metadata rows with the same symbol_name stay separate, the `edges` CTE restricts ALL metadata edges (not just synthetic aliases) to `tf.has_class_like_kind = 1`, and two new CTEs resolve the remaining false-positive shapes: `metadata_raw_suppression` drops the raw C# `[Foo]` row when the synthetic `FooAttribute` alias already resolves to a class-like target in the same source file (so `[Foo]` does not fan out to both `class FooAttribute` and a plain `class Foo` that shares the bare name), and `target_ambiguity` drops metadata edges when the same-named symbol resolves to multiple class-like definitions in scope (so ambiguous `[MyAudit]` usage is left to `impact` / `references` to disambiguate rather than being fanned out to every same-named attribute class). The existing `GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies` test (which pins `[JsonConverter(...)]` → `class JsonConverter : Attribute` for a target class whose name does NOT end in `Attribute`) still passes because only one class-like target exists in scope, so `target_ambiguity` keeps it, the `is_metadata` / class-like restriction accepts it, and `metadata_raw_suppression` finds no synthetic `JsonConverterAttribute` alias target and therefore does not suppress the raw row. Added `DbReaderTests.GetFileDependencies_CSharpAttributeRawDoesNotLeakToBareNameClass_WhenSuffixTargetExists` (raw `[MyAudit]` row is suppressed when the alias resolves to `class MyAuditAttribute`, so `[MyAudit]` does NOT depend on an unrelated plain `class MyAudit` file), `DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotLeakToSameNameMethodOrProperty` (fully-qualified `[MyAuditAttribute]` metadata does NOT depend on a file that only defines a method / property named `MyAuditAttribute`), and `DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotFanOutWhenMultipleSameNameAttributeClasses` (two same-named attribute classes in different namespaces cause the metadata edge to be dropped as ambiguous). Both the `impact` ambiguity guard (`IsMetadataTargetUnambiguous`) and the `deps` ambiguity CTE (`target_ambiguity`) are additionally tightened to count class-like definitions at symbol-identity level instead of path level: `IsMetadataTargetUnambiguous` now uses `SELECT DISTINCT f.path, s.line, s.name` so two same-named class-like definitions in one source file (e.g. idiomatic C# `namespace A { class FooAttribute { } } namespace B { class FooAttribute { } }` in a single .cs file) register as ambiguous; `target_ambiguity` joins `target_files` back through `files` + `symbols` with `COUNT(*)` over class-like symbol rows so the same same-file duplicate case correctly drops the `[Foo]` metadata edge. Path-level counting previously treated these same-file duplicates as count=1 because `SELECT DISTINCT f.path` and `target_files` (grouped by `dst.path`) both collapse the two definitions into a single row, so `cdidx deps` and `cdidx impact` were confidently reporting an ambiguous short-name metadata reference as a definite dependency / blast-radius edge — contradicting the design intent that "ambiguity" includes duplicates in one file. Added `DbReaderTests.GetFileDependencies_CSharpAttributeAmbiguityCountsSameFileDuplicateClassDefinitions` and `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CountsSameFileDuplicateDefinitions` to pin the symbol-identity-level counting for both `deps` and `impact`. `IsMetadataTargetUnambiguous` is additionally tightened to wrap `--path` / `--exclude-path` parameters through `EscapeLikeQuery` and bind them as `%...%` patterns, matching the LIKE semantics used by the rest of the reader (search / references / deps etc.); without the wrap the in-scope class-like count would underflow to 1 even when two same-named attribute classes sit side-by-side in the requested CLI subtree such as `--path src/A/`, and the metadata bypass would falsely fire. Added `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CliPathPatternEscaping_SuppressesWhenInScopeIsAmbiguous` to pin this. `CSharpNoArgAttributeRegex` is additionally widened to accept an optional generic-argument list after the attribute name (`(?:\s*<[^>\n]+>)?`) so generic no-arg attribute forms such as `[MyAudit]`, `[assembly: MyAttr]`, and the multi-line `[\n MyAttr\n]` are indexed as `attribute` references instead of falling through both `CallRegex` (requires `(`) and the old no-arg regex (which ended at `]` / `,` / EOL and therefore rejected `` between the name and `]`). Without the widening those sites produced zero reference rows, so `deps` / `impact` / `references` / `inspect` / `analyze_symbol` all missed the dependency edge to the real `FooAttribute` class file. Added `ReferenceExtractorTests.Extract_CsharpGenericNoArgAttribute_ClassifiedAsAttribute`, `DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_StillIndexedAndResolvesToAttributeClass`, and `DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_AssemblyTarget_IsIndexed` to pin the end-to-end behavior. Affected: `src/CodeIndex/Database/DbReader.cs`, `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. - **MCP `callers` / `callees` `kind` schema, CLI help, and README `--kind` option row no longer advertise metadata kinds as valid filter values (#293 follow-up)** — The `callers` / `callees` handlers reject `kind: "attribute"` / `kind: "annotation"` with a usage error (CLI) / `isError` tool response (MCP), but the MCP tool schema's `kind` property description still listed all five kinds as if they were valid filter values (`"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"`), `cdidx --help` similarly lumped `callers` / `callees` together with `references` for the `--kind` help row, and the README `--kind` option table (EN line 436 / JP line 1307) still directed readers to pass `--kind attribute` or `--kind annotation` on `callers` / `callees`. Schema and handler now agree: the `callers` / `callees` `kind` description now reads `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."`, the CLI `--kind` help row splits `references` (accepts all five kinds) from `callers` / `callees` (call-graph kinds only, metadata rejected, redirects to `references`), and the README `--kind` row (both English and Japanese) now explicitly states that `callers` / `callees` reject `--kind attribute|annotation` with a usage error and points to `references --kind attribute|annotation` as the correct metadata-enumeration path. The MCP `references` tool description is unchanged because it still accepts every indexed kind. Added `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` to pin the corrected schema wording. Affected: `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `src/CodeIndex/Cli/ConsoleUi.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `README.md`. - **`deps`, `impact`, `references`, `inspect`, and `analyze_symbol` retain metadata references as compile-time dependency edges and canonicalize the C# attribute-suffix convention (#293 follow-up)** — `DbReader.GetFileDependencies` (backing `cdidx deps`) and `DbReader.GetFileDependencyHintsToResolvedType` (backing `cdidx impact` / MCP `impact_analysis` heuristic file-hint fallback) keep metadata references in their logical reference CTE. Renaming or removing an attribute class still breaks annotated sites such as `[JsonConverterAttribute]` at compile time, so file-level blast-radius analysis must surface those edges as real dependencies — now labeled `attribute` / `annotation` in `logical_reference_kind` (previously these same sites leaked in as phantom `call` rows before classification was corrected). `GetFileDependencies` additionally canonicalizes the C# attribute naming convention: idiomatic `[Foo]` usage is stored with `symbol_name = "Foo"` but the defining class is `FooAttribute`, so the logical-reference CTE now UNION-ALLs a suffix-aliased copy of every C# `attribute`-kind row with `'Attribute'` appended (only when the original symbol_name does not already end in `Attribute`) so the target-file join reaches the attribute class file. `GetFileDependencyHintsToResolvedType` also aggregates a per-candidate `has_metadata_ref` flag and bypasses the `SourceFileHasStructuredTypeEvidence` guard for edges whose contributing references are purely metadata, so pure-attribute consumer files such as `[MyAudit] class Svc` now surface through `impact MyAuditAttribute` instead of being filtered out. `ResolveImpactFallbackNames` symmetrically adds suffix-stripped aliases for C# class names ending in `Attribute`, so the BFS-less file-hint path can find `[Foo]` sites whose source symbol_name does not match the defining class name. The same canonicalization is propagated to the symbol-level reference reader: `DbReader.SearchReferences`, `CountSearchReferences`, and `CountSearchReferencesTotal` (backing `references`, `inspect`, MCP `analyze_symbol`) now compute a C#-only `queryAttributeAlias` (defined in the new `ComputeCSharpAttributeSuffixAlias` helper) and OR it into both exact (folded and NOCASE) and substring WHERE clauses, so `references MyAuditAttribute` (and `inspect MyAuditAttribute` / `analyze_symbol MyAuditAttribute`) surface the idiomatic `[MyAudit]` call site consistently with `deps` / `impact`. Substring mode uses an exact-OR for the alias (not a second LIKE) so user queries like `MyAuditAttribute` do not over-match unrelated names such as `FooAuditLog`. The alias is C# only, so Java / Kotlin / Scala / TypeScript annotation sites remain unaffected. `callers` / `callees` intentionally skip the alias because they filter to the shared call-graph kinds (`call`, `instantiate`, `subscribe`) and reject `--kind attribute|annotation` at the CLI / MCP boundary — those commands model the dynamic call graph, not the dependency graph. The alias branch is additionally scoped so it cannot produce false positives: `ComputeCSharpAttributeSuffixAlias` returns `null` when `referenceKind` is a non-`attribute` call-graph kind (so `references FooAttribute --kind call` stays strict and does not match `Foo()`), and the SQL alias disjunct in all three reader methods is clamped to `f.lang = 'csharp' AND r.reference_kind = 'attribute'` so an unscoped `references FooAttribute` (no `--lang` / no `--kind`) still cannot bleed into Java annotations or C# call rows. Suffix detection is also case-insensitive (`OrdinalIgnoreCase`) so `references myauditattribute` and `inspect MyAuditATTRIBUTE` still produce the `MyAudit` alias, consistent with the NOCASE / folded contract of the surrounding exact / substring query paths. Added regression coverage `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies` (runtime + metadata edges both surface), `DbReaderTests.GetFileDependencies_MatchesCSharpAttributeSuffixConvention` (`[MyAudit]` → `MyAuditAttribute` canonicalization through `deps`), `DbReaderTests.SearchReferences_MatchesCSharpAttributeSuffixConvention_Substring` / `_Exact` / `_CSharpAttributeSuffixAliasDoesNotBleedToOtherLanguages` (same canonicalization through `references` / `inspect` / `analyze_symbol`, plus C#-only scope guard), `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_NotAppliedToCallKind` (alias must not fire under `--kind call`), `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_UnscopedLangStillLimitsToCSharpAttributeRows` (unscoped query must not match Java annotations or C# call rows), and `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_CaseInsensitiveQuery` (lowercase / mixed-case queries still produce the alias). Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. - **`callers` / `callees` now reject `--kind attribute|annotation` at the CLI and MCP boundary, and Swift / Gradle `@` metadata is reclassified as `annotation` (#293 follow-up)** — Metadata rows (`attribute` / `annotation`) are attributed to their enclosing body-range symbol rather than the annotated target, so `callers Obsolete --kind attribute` would silently return `[Obsolete] void M()` under the enclosing class (not `M`), and file-level targets such as `[assembly: CLSCompliant]` or Kotlin `@file:JvmName` would drop entirely because `container_name` is `NULL`. `QueryCommandRunner.RunCallers` / `RunCallees` now reject `--kind attribute` and `--kind annotation` with `CommandExitCodes.UsageError`, and the MCP `callers` / `callees` tools return an `isError` tool response; both paths direct users to `references --kind attribute|annotation`, which IS a correct metadata enumeration path. Separately, `AnnotationLanguages` now includes `swift` and `gradle`, so Swift `@available(iOS 13.0, *)` / `@objc` / `@MainActor` and Gradle/Groovy `@CompileStatic` / `@TaskAction` are recorded as `annotation` instead of `call` (previously the `@Name(args)` forms leaked into `callers` / `callees` / `hotspots` / `impact` as phantom call edges, and the no-arg `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` forms dropped from the index entirely because no-arg annotation emission was gated to languages in this set). Added `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError` (theory, 4 cases), `McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError` (theory, 4 cases), `ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`, and `ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation`. Affected: `src/CodeIndex/Cli/QueryCommandRunner.cs`, `src/CodeIndex/Mcp/McpToolHandlers.cs`, `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. @@ -686,7 +686,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **DEVELOPER_GUIDE が SQLite の位置付けとリリース番号の例示を明確化** — cdidx が「設定ゼロ・単一ファイル・本番依存1個」の CLI であり、その唯一の本番依存が `Microsoft.Data.Sqlite` であることを明記した。あわせて、リリース手順のチェックリスト中に出てくる `1.9.0` は固定値ではなく例示であり、実際のリリース番号へ読み替える前提だと明示した。対象: `DEVELOPER_GUIDE.md`。 #### 修正 -- **`deps` の C# attribute サフィックス別名を class-like ターゲットに限定し、`impact` の metadata bypass も class-like ターゲット名が名前空間をまたいで曖昧な場合は抑止するよう修正 (#293 追加対応)** — 直前の #293 追加対応では、`deps` の logical-reference CTE に C# 限定で `symbol_name || 'Attribute'` のサフィックス別名を UNION することで `[Foo]` の attribute consumer が `FooAttribute` クラスファイルへ到達するようにし、`impact`(`GetFileDependencyHintsToResolvedType`)も寄与参照に `attribute` / `annotation` が含まれていれば structured-evidence guard をバイパスするようにしていたが、どちらも広すぎた。`deps` 側では別名が純粋に名前だけで join していたため、`[Foo]` が同名の関数・プロパティ・変数(例: 無関係なヘルパーの `void MyAuditAttribute()`)を定義するファイルにも依存を張ってしまっていた。`impact` 側の metadata bypass も、対象名が一意に class-like 定義を指すかを確認していなかったため、`MyAuditAttribute` のような短い名前が複数の namespace に存在すると rename / remove の blast-radius が過剰報告されていた(`attribute` / `annotation` 参照行は短い名前しか保持できず曖昧性解消ができない)。`DbReader.GetFileDependencies` は合成された別名行に `is_attribute_alias = 1`(本物の行は `0`)をタグ付けし、`source_name_counts` では `MIN` でフラグを伝播し、`edges` CTE の join で別名 edge をターゲット側 kind が `class` / `struct` / `interface` のときだけに制限するようにした。`target_files` CTE も `DISTINCT (path, lang, name, kind)` から `GROUP BY (path, lang, name)` + `MAX(...)` の `has_class_like_kind` フラグに切り替えたため、class と同名関数(例: C# コンストラクタ `Target()` と `class Target`)を同じファイルで定義しているケースでもターゲット行が 2 本に増えず `deps` の参照数を水増ししない。`DbReader.GetFileDependencyHintsToResolvedType` は新ヘルパー `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` を呼び、*active な impact スコープ内* で同名の class-like 定義数を数える(graph 対応言語に加えて、呼び出し側の `--lang` / `--path` / `--exclude-path` / `--exclude-tests` を同じフィルタで適用)。in-scope でその数が 1 以下のときに限り metadata evidence-guard bypass を適用する。スコープ内で曖昧なときは metadata 単独 edge も通常の `SourceFileHasStructuredTypeEvidence` に戻り、純 `[MyAudit]` consumer は誤帰属せずに落ちる。スコープ尊重が重要: 別言語 / 別 path / test ツリーにある同名クラスが C# の metadata bypass を潰してはならない(参照側が既に graph 対応 join で言語修飾されているため、曖昧性は scope 内で判定すべき)。README の `impact` 契約も「active な `--lang` / `--path` / `--exclude-path` / `--exclude-tests` スコープ」と明記している。既存の C# attribute deps テスト `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`、曖昧ではない単一定義の `impact` テスト、および既存の graph-queries dedupe テストは全てグリーンのまま。回帰テストとして `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets`(同名関数があっても虚偽 edge が出ないこと)、`DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget`(impact スコープ内で `MyAuditAttribute` が複数ある場合に metadata heuristic hint を抑止)、`DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous`(単一クラス定義のみのときは従来どおり `[MyAudit]` consumer を浮かせる)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope`(別言語の同名 annotation は `--lang csharp` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope`(スコープ外 `src/B/` の同名クラスは `src/A/` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests`(test ツリーの同名クラスは `--exclude-tests` クエリを潰さない)を追加。`ResolveImpactFallbackNames` も併せて引き締め、C# の `Attribute` サフィックス別名は *解決済み定義自身* の名前にのみ適用するようにした: 以前は同一ファイル内の fallback 名全てに別名を付けていたため、`FooAttribute` を定義するファイルに nested で `BarAttribute` が存在するだけで `impact FooAttribute` が `[Bar]` 利用サイトを `FooAttribute` の影響範囲として誤報告していた。`DbReaderTests.GetFileDependencyHints_CSharpAttributeSuffixAlias_DoesNotLeakToSameFileSiblings` を追加して固定。さらに `GetFileDependencies` を再構成し、metadata 行(`attribute` / `annotation`)が bare-name の call-graph ターゲットに漏れないようにした: `logical_references` CTE に `is_metadata` フラグを付与し、`source_name_counts` の GROUP BY を `(source_file_id, source_path, source_lang, symbol_name, is_attribute_alias, is_metadata)` に拡張して metadata と非 metadata で同じ symbol_name の行を別グループ化する。`edges` CTE は全 metadata edge(合成別名だけでなく実体行も)に `tf.has_class_like_kind = 1` を要求し、さらに 2 つの CTE で残る誤検知を潰す: `metadata_raw_suppression` は同じ source file で合成別名 `FooAttribute` が class-like ターゲットに解決できているとき、生の C# `[Foo]` 行を落とす(`[Foo]` が `class FooAttribute` と無関係な同名 `class Foo` に fan-out しない)。`target_ambiguity` はスコープ内で同名 class-like ターゲットが複数あるとき metadata edge を落とす(曖昧な `[MyAudit]` は deps で fan-out させず、`impact` / `references` に判断を委ねる)。既存の `GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies`(`[JsonConverter(...)]` → `class JsonConverter : Attribute` をターゲット名に `Attribute` サフィックスがなくても固定)は、class-like ターゲットが 1 つしかないので `target_ambiguity` で残り、`is_metadata` / class-like 制限も受理し、合成 `JsonConverterAttribute` 別名は class-like に解決できないので `metadata_raw_suppression` も発火せず、そのまま通る。回帰テストとして `DbReaderTests.GetFileDependencies_CSharpAttributeRawDoesNotLeakToBareNameClass_WhenSuffixTargetExists`(別名が `class MyAuditAttribute` に解決できたとき生 `[MyAudit]` 行は抑止され、無関係な plain `class MyAudit` ファイルに依存しない)、`DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotLeakToSameNameMethodOrProperty`(完全形 `[MyAuditAttribute]` は同名メソッド / プロパティしか持たないファイルに依存しない)、`DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotFanOutWhenMultipleSameNameAttributeClasses`(別名前空間に同名 attribute クラスが 2 つあると metadata edge はあいまいとして落とす)を追加。加えて `impact` の曖昧性ガード (`IsMetadataTargetUnambiguous`) と `deps` の曖昧性 CTE (`target_ambiguity`) も、path 単位ではなく symbol identity 単位で class-like 定義を数えるように引き締めた: `IsMetadataTargetUnambiguous` は `SELECT DISTINCT f.path, s.line, s.name` に変更し、C# でよくある 1 つの .cs ファイル内に別名前空間で同名 class-like が 2 つ定義されている形 (`namespace A { class FooAttribute { } } namespace B { class FooAttribute { } }`) でも ambiguity を 2 として検出する。`target_ambiguity` は `target_files` を `files` + `symbols` に JOIN し直して `COUNT(*)` を class-like symbol 行に対して取るため、同じ同名ファイル内重複ケースでも `[Foo]` metadata edge を正しく落とせる。path 単位カウントでは `SELECT DISTINCT f.path` も `target_files`(`dst.path` で GROUP BY)も同一ファイル内の 2 定義を 1 行に潰してしまうため、従来は count=1 として曖昧性を取りこぼし、`cdidx deps` と `cdidx impact` が同名 short-name metadata 参照を確定的な依存 / blast-radius edge として出していた。これは曖昧性に "duplicates in one file" を含める前提の設計契約に反していた。`deps` と `impact` 両方で symbol-identity 単位カウントを固定する `DbReaderTests.GetFileDependencies_CSharpAttributeAmbiguityCountsSameFileDuplicateClassDefinitions` と `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CountsSameFileDuplicateDefinitions` を追加。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 +- **`deps` の C# attribute サフィックス別名を class-like ターゲットに限定し、`impact` の metadata bypass も class-like ターゲット名が名前空間をまたいで曖昧な場合は抑止するよう修正 (#293 追加対応)** — 直前の #293 追加対応では、`deps` の logical-reference CTE に C# 限定で `symbol_name || 'Attribute'` のサフィックス別名を UNION することで `[Foo]` の attribute consumer が `FooAttribute` クラスファイルへ到達するようにし、`impact`(`GetFileDependencyHintsToResolvedType`)も寄与参照に `attribute` / `annotation` が含まれていれば structured-evidence guard をバイパスするようにしていたが、どちらも広すぎた。`deps` 側では別名が純粋に名前だけで join していたため、`[Foo]` が同名の関数・プロパティ・変数(例: 無関係なヘルパーの `void MyAuditAttribute()`)を定義するファイルにも依存を張ってしまっていた。`impact` 側の metadata bypass も、対象名が一意に class-like 定義を指すかを確認していなかったため、`MyAuditAttribute` のような短い名前が複数の namespace に存在すると rename / remove の blast-radius が過剰報告されていた(`attribute` / `annotation` 参照行は短い名前しか保持できず曖昧性解消ができない)。`DbReader.GetFileDependencies` は合成された別名行に `is_attribute_alias = 1`(本物の行は `0`)をタグ付けし、`source_name_counts` では `MIN` でフラグを伝播し、`edges` CTE の join で別名 edge をターゲット側 kind が `class` / `struct` / `interface` のときだけに制限するようにした。`target_files` CTE も `DISTINCT (path, lang, name, kind)` から `GROUP BY (path, lang, name)` + `MAX(...)` の `has_class_like_kind` フラグに切り替えたため、class と同名関数(例: C# コンストラクタ `Target()` と `class Target`)を同じファイルで定義しているケースでもターゲット行が 2 本に増えず `deps` の参照数を水増ししない。`DbReader.GetFileDependencyHintsToResolvedType` は新ヘルパー `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` を呼び、*active な impact スコープ内* で同名の class-like 定義数を数える(graph 対応言語に加えて、呼び出し側の `--lang` / `--path` / `--exclude-path` / `--exclude-tests` を同じフィルタで適用)。in-scope でその数が 1 以下のときに限り metadata evidence-guard bypass を適用する。スコープ内で曖昧なときは metadata 単独 edge も通常の `SourceFileHasStructuredTypeEvidence` に戻り、純 `[MyAudit]` consumer は誤帰属せずに落ちる。スコープ尊重が重要: 別言語 / 別 path / test ツリーにある同名クラスが C# の metadata bypass を潰してはならない(参照側が既に graph 対応 join で言語修飾されているため、曖昧性は scope 内で判定すべき)。README の `impact` 契約も「active な `--lang` / `--path` / `--exclude-path` / `--exclude-tests` スコープ」と明記している。既存の C# attribute deps テスト `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`、曖昧ではない単一定義の `impact` テスト、および既存の graph-queries dedupe テストは全てグリーンのまま。回帰テストとして `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets`(同名関数があっても虚偽 edge が出ないこと)、`DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget`(impact スコープ内で `MyAuditAttribute` が複数ある場合に metadata heuristic hint を抑止)、`DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous`(単一クラス定義のみのときは従来どおり `[MyAudit]` consumer を浮かせる)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope`(別言語の同名 annotation は `--lang csharp` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope`(スコープ外 `src/B/` の同名クラスは `src/A/` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests`(test ツリーの同名クラスは `--exclude-tests` クエリを潰さない)を追加。`ResolveImpactFallbackNames` も併せて引き締め、C# の `Attribute` サフィックス別名は *解決済み定義自身* の名前にのみ適用するようにした: 以前は同一ファイル内の fallback 名全てに別名を付けていたため、`FooAttribute` を定義するファイルに nested で `BarAttribute` が存在するだけで `impact FooAttribute` が `[Bar]` 利用サイトを `FooAttribute` の影響範囲として誤報告していた。`DbReaderTests.GetFileDependencyHints_CSharpAttributeSuffixAlias_DoesNotLeakToSameFileSiblings` を追加して固定。さらに `GetFileDependencies` を再構成し、metadata 行(`attribute` / `annotation`)が bare-name の call-graph ターゲットに漏れないようにした: `logical_references` CTE に `is_metadata` フラグを付与し、`source_name_counts` の GROUP BY を `(source_file_id, source_path, source_lang, symbol_name, is_attribute_alias, is_metadata)` に拡張して metadata と非 metadata で同じ symbol_name の行を別グループ化する。`edges` CTE は全 metadata edge(合成別名だけでなく実体行も)に `tf.has_class_like_kind = 1` を要求し、さらに 2 つの CTE で残る誤検知を潰す: `metadata_raw_suppression` は同じ source file で合成別名 `FooAttribute` が class-like ターゲットに解決できているとき、生の C# `[Foo]` 行を落とす(`[Foo]` が `class FooAttribute` と無関係な同名 `class Foo` に fan-out しない)。`target_ambiguity` はスコープ内で同名 class-like ターゲットが複数あるとき metadata edge を落とす(曖昧な `[MyAudit]` は deps で fan-out させず、`impact` / `references` に判断を委ねる)。既存の `GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies`(`[JsonConverter(...)]` → `class JsonConverter : Attribute` をターゲット名に `Attribute` サフィックスがなくても固定)は、class-like ターゲットが 1 つしかないので `target_ambiguity` で残り、`is_metadata` / class-like 制限も受理し、合成 `JsonConverterAttribute` 別名は class-like に解決できないので `metadata_raw_suppression` も発火せず、そのまま通る。回帰テストとして `DbReaderTests.GetFileDependencies_CSharpAttributeRawDoesNotLeakToBareNameClass_WhenSuffixTargetExists`(別名が `class MyAuditAttribute` に解決できたとき生 `[MyAudit]` 行は抑止され、無関係な plain `class MyAudit` ファイルに依存しない)、`DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotLeakToSameNameMethodOrProperty`(完全形 `[MyAuditAttribute]` は同名メソッド / プロパティしか持たないファイルに依存しない)、`DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotFanOutWhenMultipleSameNameAttributeClasses`(別名前空間に同名 attribute クラスが 2 つあると metadata edge はあいまいとして落とす)を追加。加えて `impact` の曖昧性ガード (`IsMetadataTargetUnambiguous`) と `deps` の曖昧性 CTE (`target_ambiguity`) も、path 単位ではなく symbol identity 単位で class-like 定義を数えるように引き締めた: `IsMetadataTargetUnambiguous` は `SELECT DISTINCT f.path, s.line, s.name` に変更し、C# でよくある 1 つの .cs ファイル内に別名前空間で同名 class-like が 2 つ定義されている形 (`namespace A { class FooAttribute { } } namespace B { class FooAttribute { } }`) でも ambiguity を 2 として検出する。`target_ambiguity` は `target_files` を `files` + `symbols` に JOIN し直して `COUNT(*)` を class-like symbol 行に対して取るため、同じ同名ファイル内重複ケースでも `[Foo]` metadata edge を正しく落とせる。path 単位カウントでは `SELECT DISTINCT f.path` も `target_files`(`dst.path` で GROUP BY)も同一ファイル内の 2 定義を 1 行に潰してしまうため、従来は count=1 として曖昧性を取りこぼし、`cdidx deps` と `cdidx impact` が同名 short-name metadata 参照を確定的な依存 / blast-radius edge として出していた。これは曖昧性に "duplicates in one file" を含める前提の設計契約に反していた。`deps` と `impact` 両方で symbol-identity 単位カウントを固定する `DbReaderTests.GetFileDependencies_CSharpAttributeAmbiguityCountsSameFileDuplicateClassDefinitions` と `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CountsSameFileDuplicateDefinitions` を追加。さらに `IsMetadataTargetUnambiguous` の `--path` / `--exclude-path` パラメータを、他の reader 経路(search / references / deps 等)と同じ LIKE セマンティクスに揃えるため `EscapeLikeQuery` でエスケープしたうえで `%...%` で包んでバインドするようにした。生値のままだと `--path src/A/` のような CLI 形では LIKE が一致せず、リクエストされたサブツリーに同名 attribute クラスが 2 件並んでいても in-scope class-like カウントが 1 に過小化され、本来抑止すべき metadata bypass が誤発火してしまっていた。`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CliPathPatternEscaping_SuppressesWhenInScopeIsAmbiguous` を追加して固定する。あわせて `CSharpNoArgAttributeRegex` を拡張し、属性名の直後に任意のジェネリック引数リスト `(?:\s*<[^>\n]+>)?` を許容するようにしたため、`[MyAudit]`・`[assembly: MyAttr]`・複数行の `[\n MyAttr\n]` のような引数なしジェネリック属性も `attribute` 参照としてインデックスされるようになった。従来は `CallRegex`(`(` 必須)でも旧 no-arg regex(`]` / `,` / EOL で終端しなければならず、名前と `]` の間に `` があると拒否)でも拾えず、参照行が 0 件となり、`deps` / `impact` / `references` / `inspect` / `analyze_symbol` が実属性クラス `FooAttribute` への依存エッジを全て取りこぼしていた。`ReferenceExtractorTests.Extract_CsharpGenericNoArgAttribute_ClassifiedAsAttribute`、`DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_StillIndexedAndResolvesToAttributeClass`、`DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_AssemblyTarget_IsIndexed` を追加して end-to-end の振る舞いを固定する。対象: `src/CodeIndex/Database/DbReader.cs`、`src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 - **MCP の `callers` / `callees` の `kind` schema・CLI help・README の `--kind` オプション行が metadata kinds を有効フィルタ値として広告しないよう修正 (#293 追加対応)** — `callers` / `callees` の handler は `kind: "attribute"` / `kind: "annotation"` を usage error(CLI)/ `isError` ツールレスポンス(MCP)で拒否するにもかかわらず、MCP ツール schema の `kind` プロパティ説明は 5 種すべてを列挙した `"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"` のままで、`cdidx --help` の `--kind` 行も `callers` / `callees` を `references` と同じ行にまとめていた。さらに README の `--kind` オプション表(英語 436 行目 / 日本語 1307 行目)も、`callers` / `callees` で `--kind attribute` / `--kind annotation` を渡せるかのように案内していた。schema と handler の契約を一致させ、`callers` / `callees` の `kind` description を `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."` に修正し、CLI の `--kind` help 行も `references`(5 種すべて受理)と `callers` / `callees`(call-graph 種別のみ、metadata kinds は拒否して `references` へ誘導)を分けるようにし、README の `--kind` 行(英語・日本語の両方)も `callers` / `callees` が `--kind attribute|annotation` を usage error で拒否すること、metadata 列挙の正規経路が `references --kind attribute|annotation` であることを明示する文言に直した。MCP の `references` ツール説明は全 reference kind を受け付ける契約のままなので変更なし。修正後の schema 文言を固定する `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` を追加。対象: `src/CodeIndex/Mcp/McpToolDefinitions.cs`、`src/CodeIndex/Cli/ConsoleUi.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`、`README.md`。 - **`deps`・`impact`・`references`・`inspect`・`analyze_symbol` が metadata 参照を compile-time 依存として保持し、C# の attribute サフィックス命名規約を正規化 (#293 追加対応)** — `cdidx deps` の実装である `DbReader.GetFileDependencies` と、`cdidx impact` / MCP `impact_analysis` の heuristic file-hint フォールバックである `DbReader.GetFileDependencyHintsToResolvedType` は、logical reference CTE に metadata 参照も含める。attribute クラスを rename / 削除すれば `[JsonConverterAttribute]` のような注釈サイトも compile-time で壊れるため、ファイル単位の blast-radius 分析はそれらも本物の依存 edge として表示する必要があり、`logical_reference_kind` では `attribute` / `annotation` として正しくラベル付けされる(分類修正前は同じサイトが phantom `call` として漏れていた)。さらに `GetFileDependencies` は C# の attribute 命名規約を正規化する: 慣用的な `[Foo]` は `symbol_name = "Foo"` で保存されるが、定義クラスは `FooAttribute` 命名となるため、logical-reference CTE に C# `attribute` 種別の行をサフィックス `'Attribute'` を付与した別名コピーとして UNION ALL で追加する(元の symbol_name が既に `Attribute` で終わっている場合は重複しないようスキップ)。これにより target ファイル join が attribute クラス側のファイルへ到達する。`GetFileDependencyHintsToResolvedType` はさらに candidate ごとに `has_metadata_ref` フラグを集約し、寄与する参照が metadata だけである edge については `SourceFileHasStructuredTypeEvidence` guard をバイパスするため、`[MyAudit] class Svc` のような純 attribute consumer ファイルも `impact MyAuditAttribute` で出るようになった(以前は filter で落ちていた)。`ResolveImpactFallbackNames` も対称的に、`Attribute` で終わる C# クラス名にはサフィックスを外した別名を追加するため、BFS を使わない file-hint 経路でも source symbol_name が定義クラス名と一致しない `[Foo]` サイトを発見できる。同じ正規化をシンボル単位の参照 reader にも伝播する: `references`・`inspect`・MCP `analyze_symbol` を支える `DbReader.SearchReferences`・`CountSearchReferences`・`CountSearchReferencesTotal` は、新しいヘルパー `ComputeCSharpAttributeSuffixAlias` で C# 限定の `queryAttributeAlias` を計算し、exact(folded と NOCASE)・substring の両方の WHERE 句に OR で追加する。これにより `references MyAuditAttribute`(および `inspect MyAuditAttribute` / `analyze_symbol MyAuditAttribute`)でも慣用的な `[MyAudit]` 参照サイトが `deps` / `impact` と一貫して surface する。substring モードでは alias を二つ目の LIKE にせず exact-OR で束ねているため、ユーザーが `MyAuditAttribute` と打った時に `FooAuditLog` のような無関係な名前に over-match しない。alias は C# 限定なので、Java / Kotlin / Scala / TypeScript の annotation サイトには影響しない。`callers` / `callees` は共有の call-graph 種別 (`call` / `instantiate` / `subscribe`) に絞って `--kind attribute|annotation` を CLI / MCP 境界で拒否する契約なので、alias は意図的に適用しない(dynamic call graph と dependency graph は別契約)。さらに alias 節が誤一致を生まないようスコープする: `ComputeCSharpAttributeSuffixAlias` は `referenceKind` が `attribute` 以外の call-graph 種別のときは `null` を返す(`references FooAttribute --kind call` は厳格一致のままで `Foo()` に一致しない)、3 つの reader メソッドの SQL alias 節は `f.lang = 'csharp' AND r.reference_kind = 'attribute'` に限定するため、`--lang` / `--kind` 無指定の `references FooAttribute` でも Java の annotation や C# の call 行を拾わない。suffix 検出も `OrdinalIgnoreCase` にして case-insensitive 化したため、`references myauditattribute` / `inspect MyAuditATTRIBUTE` のような小文字・混在ケースでも alias `MyAudit` を生成でき、周辺の NOCASE / folded 契約と整合する。回帰テスト `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies`(runtime と metadata の両方の edge が出ることを固定)、`DbReaderTests.GetFileDependencies_MatchesCSharpAttributeSuffixConvention`(`deps` 経由での `[MyAudit]` → `MyAuditAttribute` 正規化)、`DbReaderTests.SearchReferences_MatchesCSharpAttributeSuffixConvention_Substring` / `_Exact` / `_CSharpAttributeSuffixAliasDoesNotBleedToOtherLanguages`(`references` / `inspect` / `analyze_symbol` での同一正規化と C# 限定スコープ)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_NotAppliedToCallKind`(`--kind call` で alias が発火しないこと)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_UnscopedLangStillLimitsToCSharpAttributeRows`(無指定でも Java annotation や C# call 行に一致しないこと)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_CaseInsensitiveQuery`(小文字・混在ケースのクエリでも alias を生成)を追加。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 - **`callers` / `callees` が CLI と MCP 境界で `--kind attribute|annotation` を拒否し、Swift / Gradle の `@` metadata を `annotation` に再分類 (#293 追加対応)** — metadata 行 (`attribute` / `annotation`) は注釈対象そのものではなく body-range の外側シンボルに帰属するため、`callers Obsolete --kind attribute` は `[Obsolete] void M()` を `M` ではなく外側クラスの下に黙って返し、`[assembly: CLSCompliant]` や Kotlin の `@file:JvmName` のような file-level target は `container_name = NULL` で完全脱落していた。`QueryCommandRunner.RunCallers` / `RunCallees` は `--kind attribute` / `--kind annotation` を `CommandExitCodes.UsageError` で弾き、MCP の `callers` / `callees` ツールは `isError` ツールレスポンスを返すようにした。どちらの経路も、metadata 列挙の正しいパスである `references --kind attribute|annotation` へ誘導する。あわせて `AnnotationLanguages` に `swift` と `gradle` を追加し、Swift の `@available(iOS 13.0, *)` / `@objc` / `@MainActor` や Gradle / Groovy の `@CompileStatic` / `@TaskAction` を `call` ではなく `annotation` として記録するようにした(修正前は引数付きの `@Name(args)` 形は `callers` / `callees` / `hotspots` / `impact` に phantom call edge として漏れ、引数なしの `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` 形はこの集合で gate されていた no-arg annotation 出力から外れてインデックス自体に残らなかった)。回帰テストとして `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError`(theory 4 ケース)、`McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError`(theory 4 ケース)、`ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`、`ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation` を追加。対象: `src/CodeIndex/Cli/QueryCommandRunner.cs`、`src/CodeIndex/Mcp/McpToolHandlers.cs`、`src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 diff --git a/src/CodeIndex/Database/DbReader.cs b/src/CodeIndex/Database/DbReader.cs index 5df7778456..aad715f5ed 100644 --- a/src/CodeIndex/Database/DbReader.cs +++ b/src/CodeIndex/Database/DbReader.cs @@ -1958,13 +1958,26 @@ AND s.kind IN ('class','struct','interface') sql += " AND f.lang = @metadataAmbigLangFilter"; cmd.Parameters.AddWithValue("@metadataAmbigLangFilter", lang); } + // Path / exclude-path parameters must be wrapped with `%...%` and escaped + // through EscapeLikeQuery so the LIKE semantics match the rest of the + // reader (search / references / callers / deps etc.). Passing the raw + // CLI value would require an anchored path like `%src/A/%` to match, so + // normal `--path src/A/` invocations would see zero in-scope definitions, + // the ambiguity count would underflow to 1, and the metadata bypass + // would falsely fire on what are actually ambiguous targets. + // path / exclude-path のパラメータは他の読み取り経路 (search / references / + // callers / deps 等) と同じ LIKE セマンティクスに合わせるため、 + // EscapeLikeQuery でエスケープした上で `%...%` で包んでバインドする。生値の + // まま渡すと、通常の `--path src/A/` のような呼び出しでは LIKE が一致せず、 + // 曖昧性カウントが 1 に過小化され、本来抑止すべき metadata bypass が + // 誤って発動してしまう。 if (pathPatterns is { Count: > 0 }) { var ors = new List(pathPatterns.Count); for (int i = 0; i < pathPatterns.Count; i++) { ors.Add($"f.path LIKE @metadataAmbigPath{i} ESCAPE '\\'"); - cmd.Parameters.AddWithValue($"@metadataAmbigPath{i}", pathPatterns[i]); + cmd.Parameters.AddWithValue($"@metadataAmbigPath{i}", $"%{EscapeLikeQuery(pathPatterns[i])}%"); } sql += " AND (" + string.Join(" OR ", ors) + ")"; } @@ -1973,7 +1986,7 @@ AND s.kind IN ('class','struct','interface') for (int i = 0; i < excludePathPatterns.Count; i++) { sql += $" AND f.path NOT LIKE @metadataAmbigExcludePath{i} ESCAPE '\\'"; - cmd.Parameters.AddWithValue($"@metadataAmbigExcludePath{i}", excludePathPatterns[i]); + cmd.Parameters.AddWithValue($"@metadataAmbigExcludePath{i}", $"%{EscapeLikeQuery(excludePathPatterns[i])}%"); } } if (excludeTests) diff --git a/src/CodeIndex/Indexer/ReferenceExtractor.cs b/src/CodeIndex/Indexer/ReferenceExtractor.cs index 58a7a115c2..94875a875f 100644 --- a/src/CodeIndex/Indexer/ReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/ReferenceExtractor.cs @@ -149,7 +149,7 @@ public static class ReferenceExtractor // (CallRegex 経路)や `.` / `::`(qualifier 継続)なら名前を確定させず、行末(`$`)・`]`・`,` // のいずれかで初めて採用する。 private static readonly Regex CSharpNoArgAttributeRegex = new( - @"(?[A-Za-z_]\w*)\s*(?=[\],]|$)", + @"(?[A-Za-z_]\w*)(?:\s*<[^>\n]+>)?\s*(?=[\],]|$)", RegexOptions.Compiled); // No-arg Java-family annotation (`@Deprecated`, `@Override`, `@org.junit.Test`, `@field:Deprecated`). diff --git a/tests/CodeIndex.Tests/DbReaderTests.cs b/tests/CodeIndex.Tests/DbReaderTests.cs index 2d43a3a347..8d7188d2af 100644 --- a/tests/CodeIndex.Tests/DbReaderTests.cs +++ b/tests/CodeIndex.Tests/DbReaderTests.cs @@ -2348,6 +2348,79 @@ public class Svc Assert.Contains(dependencies, d => d.SourcePath == "src/Svc.cs" && d.TargetPath == "src/MyAuditAttribute.cs"); } + [Fact] + public void GetFileDependencies_CSharpGenericNoArgAttribute_StillIndexedAndResolvesToAttributeClass() + { + // issue #293 round-15 follow-up: generic no-arg C# attributes like + // `[MyAudit]` and multi-line `[\n MyAttr\n]` must still be + // indexed as `attribute` references so `deps` can route them through + // the suffix-alias synthesizer to the real attribute class file. + // Before the regex was widened these forms fell through both CallRegex + // (no `(`) and the no-arg regex (generic `<...>` after the name broke + // the `(?=[\],]|$)` anchor), producing zero edges. + // issue #293 round-15 補足: `[MyAudit]` や複数行の `[\n MyAttr\n]` + // のようなジェネリック引数なし属性も `attribute` として取り込まれ、 + // suffix alias を経由して実属性クラスへの依存エッジに正規化される + // こと。正規表現の拡張前は両 regex とも拾えず、エッジが 0 件だった。 + InsertIndexedFile("src/MyAuditAttribute.cs", "csharp", + """ + using System; + + public sealed class MyAuditAttribute : Attribute + { + } + """); + InsertIndexedFile("src/MyAttrAttribute.cs", "csharp", + """ + using System; + + public sealed class MyAttrAttribute : Attribute + { + } + """); + InsertIndexedFile("src/Svc.cs", "csharp", + """ + [MyAudit] + [ + MyAttr + ] + public class Svc + { + } + """); + + var dependencies = _reader.GetFileDependencies(limit: 20, lang: "csharp"); + + Assert.Contains(dependencies, d => d.SourcePath == "src/Svc.cs" && d.TargetPath == "src/MyAuditAttribute.cs"); + Assert.Contains(dependencies, d => d.SourcePath == "src/Svc.cs" && d.TargetPath == "src/MyAttrAttribute.cs"); + } + + [Fact] + public void GetFileDependencies_CSharpGenericNoArgAttribute_AssemblyTarget_IsIndexed() + { + // issue #293 round-15 follow-up: `[assembly: MyAttr]` — assembly + // targeted generic no-arg attribute must also reach the attribute class. + // issue #293 round-15 補足: `[assembly: MyAttr]` のような + // assembly targeted ジェネリック引数なし属性も同様にインデックスされ、 + // attribute クラスに解決されること。 + InsertIndexedFile("src/MyAttrAttribute.cs", "csharp", + """ + using System; + + public sealed class MyAttrAttribute : Attribute + { + } + """); + InsertIndexedFile("src/AssemblyInfo.cs", "csharp", + """ + [assembly: MyAttr] + """); + + var dependencies = _reader.GetFileDependencies(limit: 20, lang: "csharp"); + + Assert.Contains(dependencies, d => d.SourcePath == "src/AssemblyInfo.cs" && d.TargetPath == "src/MyAttrAttribute.cs"); + } + [Fact] public void GetFileDependencies_CSharpAttributeRawDoesNotLeakToBareNameClass_WhenSuffixTargetExists() { @@ -3057,6 +3130,58 @@ public sealed class MyAuditAttribute : System.Attribute Assert.Contains(result.FileImpacts, f => f.SourcePath == "src/A/Svc.cs" && f.TargetPath == "src/A/MyAuditAttribute.cs"); } + [Fact] + public void GetFileDependencyHints_MetadataBypassAmbiguityGuard_CliPathPatternEscaping_SuppressesWhenInScopeIsAmbiguous() + { + // issue #293 round-15 follow-up: path / exclude-path parameters must be + // wrapped with `%...%` and routed through EscapeLikeQuery so the LIKE + // predicate accepts CLI-style prefixes like `src/A/`. Without the wrap + // the ambiguity count would underflow to 1 (unambiguous), and the + // metadata bypass would falsely fire even though two MyAuditAttribute + // classes exist side-by-side in the requested subtree. + // issue #293 round-15 補足: path / exclude-path のバインドは他の reader + // 経路と同じ `%...%` + EscapeLikeQuery に揃える必要がある。生値で渡すと + // `src/A/` のような CLI 形では LIKE が一致せず、要求したサブツリーに + // 同名 MyAuditAttribute が 2 件存在しても曖昧性カウントが 1 に落ち、 + // 本来抑止すべき metadata bypass が誤発火してしまう。 + InsertIndexedFile("src/A/Inner1/MyAuditAttribute.cs", "csharp", + """ + namespace A.Inner1; + + public sealed class MyAuditAttribute : System.Attribute + { + } + """); + InsertIndexedFile("src/A/Inner2/MyAuditAttribute.cs", "csharp", + """ + namespace A.Inner2; + + public sealed class MyAuditAttribute : System.Attribute + { + } + """); + InsertIndexedFile("src/A/Svc.cs", "csharp", + """ + namespace A; + + [MyAudit] + public class Svc + { + } + """); + + var result = _reader.AnalyzeImpact( + "MyAuditAttribute", + maxDepth: 3, + limit: 20, + lang: "csharp", + pathPatterns: new[] { "src/A/" }); + + Assert.DoesNotContain(result.FileImpacts, f => + f.SourcePath == "src/A/Svc.cs" && + (f.TargetPath == "src/A/Inner1/MyAuditAttribute.cs" || f.TargetPath == "src/A/Inner2/MyAuditAttribute.cs")); + } + [Fact] public void GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests() { diff --git a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs index 6e6b80bbc1..139066f958 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs @@ -1930,6 +1930,35 @@ public class C Assert.DoesNotContain(references, r => r.ReferenceKind == "call"); } + [Fact] + public void Extract_CsharpGenericNoArgAttribute_ClassifiedAsAttribute() + { + // Regression (issue #293 follow-up): generic no-arg C# attributes such as + // `[MyAudit]`, `[assembly: MyAttr]`, and multi-line `[\n MyAttr\n]` + // must still classify as `attribute`. The no-arg attribute regex must accept an + // optional generic argument list after the name so these references are indexed. + // リグレッション (issue #293 補足): `[MyAudit]` などのジェネリック引数なし属性、 + // `[assembly: MyAttr]` のような assembly targeted 形、そして複数行の + // `[\n MyAttr\n]` も `attribute` として取り込まれること。 + const string content = """ + [assembly: MyAttr] + [MyAudit] + [ + MyAttr + ] + public class C + { + } + """; + + var references = ReferenceExtractor.Extract(1, "csharp", content, []); + + var myAudit = Assert.Single(references.Where(r => r.SymbolName == "MyAudit")); + Assert.Equal("attribute", myAudit.ReferenceKind); + Assert.Equal(2, references.Count(r => r.SymbolName == "MyAttr" && r.ReferenceKind == "attribute")); + Assert.DoesNotContain(references, r => r.ReferenceKind == "call"); + } + [Fact] public void Extract_CsharpNoArgParameterAttribute_ClassifiedAsAttribute() { From 4ca3a0203bbfe8f5e25c2a2dd88947bb369d6178 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 18 Apr 2026 16:28:15 +0900 Subject: [PATCH 26/30] Fix #293 restrict C# metadata target ambiguity to Attribute-derived classes and accept nested generic no-arg attributes Both the `impact` ambiguity guard (IsMetadataTargetUnambiguous) and the `deps` ambiguity path (target_files / target_ambiguity) now treat a C# class as a valid `[Attr]` metadata target only when its signature inherits from an Attribute-suffixed base (signature LIKE '%: %Attribute%'). A plain `class MyAuditAttribute { }` without `: Attribute` inheritance is not a valid `[MyAudit]` target at compile time; counting it as an ambiguity candidate under-reported `impact` blast radius and suppressed real `deps` metadata edges when a same-named plain class coexisted with the real attribute class. Also widen CSharpNoArgAttributeRegex's generic segment to a non-greedy `[^\n]+?` so nested generic attribute usages like `[MyAttr>]` are indexed as attribute references. Co-Authored-By: Claude Opus 4 --- CHANGELOG.md | 4 +- src/CodeIndex/Database/DbReader.cs | 50 ++++++++++- src/CodeIndex/Indexer/ReferenceExtractor.cs | 2 +- tests/CodeIndex.Tests/DbReaderTests.cs | 89 +++++++++++++++++++ .../ReferenceExtractorTests.cs | 35 ++++++++ 5 files changed, 173 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40645d6182..2b22b528cd 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/), - **Developer Guide now distinguishes SQLite's role from "zero dependencies" and labels release numbers as examples** — Clarified that cdidx remains a zero-configuration, single-file CLI with exactly one production dependency (`Microsoft.Data.Sqlite`), and made the release-workflow checklist explicitly state that `1.9.0` is only an example version to substitute during an actual release. Affected: `DEVELOPER_GUIDE.md`. #### Fixed -- **`deps` C# attribute suffix alias now only matches class-like targets, and `impact` metadata bypass is suppressed when the class-like target name is ambiguous across namespaces (#293 follow-up)** — The earlier `#293` follow-up made `deps` UNION a C#-only `symbol_name || 'Attribute'` suffix alias into the logical-reference CTE so `[Foo]` attribute consumers could reach `FooAttribute` class files, and made `impact` (`GetFileDependencyHintsToResolvedType`) bypass the structured-evidence guard whenever any contributing reference was `attribute` / `annotation`. Both changes were too broad. On `deps` the alias joined purely by name, so `[Foo]` would also depend on any file that merely defined a function / property / variable named `FooAttribute` (e.g. `void MyAuditAttribute()` in an unrelated helper). On `impact` the metadata bypass did not check whether the target name was uniquely a class-like definition, so when the same unqualified name (e.g. `MyAuditAttribute`) existed in multiple namespaces the rename/remove blast radius was over-reported — attribute / annotation reference rows only keep the short name and cannot be disambiguated. `DbReader.GetFileDependencies` now tags synthetic alias rows with `is_attribute_alias = 1` (real rows are `0`), propagates the flag through `source_name_counts` via `MIN`, and restricts alias edges to class-like target kinds (`class` / `struct` / `interface`) in the `edges` CTE join. The `target_files` CTE is also switched from `DISTINCT` over `(path, lang, name, kind)` to `GROUP BY (path, lang, name)` with a `MAX(...)` `has_class_like_kind` flag, so files that define both a class and a same-named function (e.g. the C# constructor `Target()` inside `class Target`) no longer fan out into two target rows and inflate the `deps` reference count. `DbReader.GetFileDependencyHintsToResolvedType` now calls a new `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` helper that counts class-like definitions with the same unqualified name *within the active impact scope* (graph-supported languages, plus the caller's `--lang` / `--path` / `--exclude-path` / `--exclude-tests` filters); the metadata evidence-guard bypass is only applied when that in-scope count is ≤ 1. If the name is ambiguous within scope, metadata-only edges fall through to the normal `SourceFileHasStructuredTypeEvidence` check, so pure `[MyAudit]` consumers are dropped instead of being mis-attributed. Scoping matters: a same-named class in an unrelated language / path / test tree must not suppress the C# metadata bypass, because attribute reference rows are already language-qualified through the graph-supported reference join, and the README contract for `impact` explicitly promises "active `--lang` / `--path` / `--exclude-path` / `--exclude-tests` scope". The C# attribute deps test `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`, the unambiguous single-definition `impact` test, and the pre-existing graph-queries dedupe test all still pass. Added `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets` (ambiguous same-name function must not produce a spurious edge), `DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget` (two `MyAuditAttribute` classes within the same impact scope suppress the metadata heuristic hint), `DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous` (single class definition still surfaces the `[MyAudit]` consumer), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope` (same-named Java annotation must not suppress a `--lang csharp` query), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope` (out-of-scope `src/B/` same-named class must not suppress a `src/A/`-scoped query), and `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests` (test-tree same-named class must not suppress an `--exclude-tests` query). `ResolveImpactFallbackNames` is additionally tightened so the C# `Attribute`-suffix alias is only applied to the resolved definition's own name: previously the alias was applied to every same-file fallback name, so a nested `BarAttribute` inside the file that defines `FooAttribute` caused `impact FooAttribute` to falsely attribute `[Bar]` use sites to `FooAttribute`'s blast radius. Added `DbReaderTests.GetFileDependencyHints_CSharpAttributeSuffixAlias_DoesNotLeakToSameFileSiblings` to pin this. `GetFileDependencies` is additionally restructured so metadata rows (`attribute` / `annotation`) cannot leak onto bare-name call-graph targets: the `logical_references` CTE now carries an `is_metadata` flag, `source_name_counts` groups by `(source_file_id, source_path, source_lang, symbol_name, is_attribute_alias, is_metadata)` so metadata and non-metadata rows with the same symbol_name stay separate, the `edges` CTE restricts ALL metadata edges (not just synthetic aliases) to `tf.has_class_like_kind = 1`, and two new CTEs resolve the remaining false-positive shapes: `metadata_raw_suppression` drops the raw C# `[Foo]` row when the synthetic `FooAttribute` alias already resolves to a class-like target in the same source file (so `[Foo]` does not fan out to both `class FooAttribute` and a plain `class Foo` that shares the bare name), and `target_ambiguity` drops metadata edges when the same-named symbol resolves to multiple class-like definitions in scope (so ambiguous `[MyAudit]` usage is left to `impact` / `references` to disambiguate rather than being fanned out to every same-named attribute class). The existing `GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies` test (which pins `[JsonConverter(...)]` → `class JsonConverter : Attribute` for a target class whose name does NOT end in `Attribute`) still passes because only one class-like target exists in scope, so `target_ambiguity` keeps it, the `is_metadata` / class-like restriction accepts it, and `metadata_raw_suppression` finds no synthetic `JsonConverterAttribute` alias target and therefore does not suppress the raw row. Added `DbReaderTests.GetFileDependencies_CSharpAttributeRawDoesNotLeakToBareNameClass_WhenSuffixTargetExists` (raw `[MyAudit]` row is suppressed when the alias resolves to `class MyAuditAttribute`, so `[MyAudit]` does NOT depend on an unrelated plain `class MyAudit` file), `DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotLeakToSameNameMethodOrProperty` (fully-qualified `[MyAuditAttribute]` metadata does NOT depend on a file that only defines a method / property named `MyAuditAttribute`), and `DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotFanOutWhenMultipleSameNameAttributeClasses` (two same-named attribute classes in different namespaces cause the metadata edge to be dropped as ambiguous). Both the `impact` ambiguity guard (`IsMetadataTargetUnambiguous`) and the `deps` ambiguity CTE (`target_ambiguity`) are additionally tightened to count class-like definitions at symbol-identity level instead of path level: `IsMetadataTargetUnambiguous` now uses `SELECT DISTINCT f.path, s.line, s.name` so two same-named class-like definitions in one source file (e.g. idiomatic C# `namespace A { class FooAttribute { } } namespace B { class FooAttribute { } }` in a single .cs file) register as ambiguous; `target_ambiguity` joins `target_files` back through `files` + `symbols` with `COUNT(*)` over class-like symbol rows so the same same-file duplicate case correctly drops the `[Foo]` metadata edge. Path-level counting previously treated these same-file duplicates as count=1 because `SELECT DISTINCT f.path` and `target_files` (grouped by `dst.path`) both collapse the two definitions into a single row, so `cdidx deps` and `cdidx impact` were confidently reporting an ambiguous short-name metadata reference as a definite dependency / blast-radius edge — contradicting the design intent that "ambiguity" includes duplicates in one file. Added `DbReaderTests.GetFileDependencies_CSharpAttributeAmbiguityCountsSameFileDuplicateClassDefinitions` and `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CountsSameFileDuplicateDefinitions` to pin the symbol-identity-level counting for both `deps` and `impact`. `IsMetadataTargetUnambiguous` is additionally tightened to wrap `--path` / `--exclude-path` parameters through `EscapeLikeQuery` and bind them as `%...%` patterns, matching the LIKE semantics used by the rest of the reader (search / references / deps etc.); without the wrap the in-scope class-like count would underflow to 1 even when two same-named attribute classes sit side-by-side in the requested CLI subtree such as `--path src/A/`, and the metadata bypass would falsely fire. Added `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CliPathPatternEscaping_SuppressesWhenInScopeIsAmbiguous` to pin this. `CSharpNoArgAttributeRegex` is additionally widened to accept an optional generic-argument list after the attribute name (`(?:\s*<[^>\n]+>)?`) so generic no-arg attribute forms such as `[MyAudit]`, `[assembly: MyAttr]`, and the multi-line `[\n MyAttr\n]` are indexed as `attribute` references instead of falling through both `CallRegex` (requires `(`) and the old no-arg regex (which ended at `]` / `,` / EOL and therefore rejected `` between the name and `]`). Without the widening those sites produced zero reference rows, so `deps` / `impact` / `references` / `inspect` / `analyze_symbol` all missed the dependency edge to the real `FooAttribute` class file. Added `ReferenceExtractorTests.Extract_CsharpGenericNoArgAttribute_ClassifiedAsAttribute`, `DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_StillIndexedAndResolvesToAttributeClass`, and `DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_AssemblyTarget_IsIndexed` to pin the end-to-end behavior. Affected: `src/CodeIndex/Database/DbReader.cs`, `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. +- **`deps` C# attribute suffix alias now only matches class-like targets, and `impact` metadata bypass is suppressed when the class-like target name is ambiguous across namespaces (#293 follow-up)** — The earlier `#293` follow-up made `deps` UNION a C#-only `symbol_name || 'Attribute'` suffix alias into the logical-reference CTE so `[Foo]` attribute consumers could reach `FooAttribute` class files, and made `impact` (`GetFileDependencyHintsToResolvedType`) bypass the structured-evidence guard whenever any contributing reference was `attribute` / `annotation`. Both changes were too broad. On `deps` the alias joined purely by name, so `[Foo]` would also depend on any file that merely defined a function / property / variable named `FooAttribute` (e.g. `void MyAuditAttribute()` in an unrelated helper). On `impact` the metadata bypass did not check whether the target name was uniquely a class-like definition, so when the same unqualified name (e.g. `MyAuditAttribute`) existed in multiple namespaces the rename/remove blast radius was over-reported — attribute / annotation reference rows only keep the short name and cannot be disambiguated. `DbReader.GetFileDependencies` now tags synthetic alias rows with `is_attribute_alias = 1` (real rows are `0`), propagates the flag through `source_name_counts` via `MIN`, and restricts alias edges to class-like target kinds (`class` / `struct` / `interface`) in the `edges` CTE join. The `target_files` CTE is also switched from `DISTINCT` over `(path, lang, name, kind)` to `GROUP BY (path, lang, name)` with a `MAX(...)` `has_class_like_kind` flag, so files that define both a class and a same-named function (e.g. the C# constructor `Target()` inside `class Target`) no longer fan out into two target rows and inflate the `deps` reference count. `DbReader.GetFileDependencyHintsToResolvedType` now calls a new `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` helper that counts class-like definitions with the same unqualified name *within the active impact scope* (graph-supported languages, plus the caller's `--lang` / `--path` / `--exclude-path` / `--exclude-tests` filters); the metadata evidence-guard bypass is only applied when that in-scope count is ≤ 1. If the name is ambiguous within scope, metadata-only edges fall through to the normal `SourceFileHasStructuredTypeEvidence` check, so pure `[MyAudit]` consumers are dropped instead of being mis-attributed. Scoping matters: a same-named class in an unrelated language / path / test tree must not suppress the C# metadata bypass, because attribute reference rows are already language-qualified through the graph-supported reference join, and the README contract for `impact` explicitly promises "active `--lang` / `--path` / `--exclude-path` / `--exclude-tests` scope". The C# attribute deps test `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`, the unambiguous single-definition `impact` test, and the pre-existing graph-queries dedupe test all still pass. Added `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets` (ambiguous same-name function must not produce a spurious edge), `DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget` (two `MyAuditAttribute` classes within the same impact scope suppress the metadata heuristic hint), `DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous` (single class definition still surfaces the `[MyAudit]` consumer), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope` (same-named Java annotation must not suppress a `--lang csharp` query), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope` (out-of-scope `src/B/` same-named class must not suppress a `src/A/`-scoped query), and `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests` (test-tree same-named class must not suppress an `--exclude-tests` query). `ResolveImpactFallbackNames` is additionally tightened so the C# `Attribute`-suffix alias is only applied to the resolved definition's own name: previously the alias was applied to every same-file fallback name, so a nested `BarAttribute` inside the file that defines `FooAttribute` caused `impact FooAttribute` to falsely attribute `[Bar]` use sites to `FooAttribute`'s blast radius. Added `DbReaderTests.GetFileDependencyHints_CSharpAttributeSuffixAlias_DoesNotLeakToSameFileSiblings` to pin this. `GetFileDependencies` is additionally restructured so metadata rows (`attribute` / `annotation`) cannot leak onto bare-name call-graph targets: the `logical_references` CTE now carries an `is_metadata` flag, `source_name_counts` groups by `(source_file_id, source_path, source_lang, symbol_name, is_attribute_alias, is_metadata)` so metadata and non-metadata rows with the same symbol_name stay separate, the `edges` CTE restricts ALL metadata edges (not just synthetic aliases) to `tf.has_class_like_kind = 1`, and two new CTEs resolve the remaining false-positive shapes: `metadata_raw_suppression` drops the raw C# `[Foo]` row when the synthetic `FooAttribute` alias already resolves to a class-like target in the same source file (so `[Foo]` does not fan out to both `class FooAttribute` and a plain `class Foo` that shares the bare name), and `target_ambiguity` drops metadata edges when the same-named symbol resolves to multiple class-like definitions in scope (so ambiguous `[MyAudit]` usage is left to `impact` / `references` to disambiguate rather than being fanned out to every same-named attribute class). The existing `GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies` test (which pins `[JsonConverter(...)]` → `class JsonConverter : Attribute` for a target class whose name does NOT end in `Attribute`) still passes because only one class-like target exists in scope, so `target_ambiguity` keeps it, the `is_metadata` / class-like restriction accepts it, and `metadata_raw_suppression` finds no synthetic `JsonConverterAttribute` alias target and therefore does not suppress the raw row. Added `DbReaderTests.GetFileDependencies_CSharpAttributeRawDoesNotLeakToBareNameClass_WhenSuffixTargetExists` (raw `[MyAudit]` row is suppressed when the alias resolves to `class MyAuditAttribute`, so `[MyAudit]` does NOT depend on an unrelated plain `class MyAudit` file), `DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotLeakToSameNameMethodOrProperty` (fully-qualified `[MyAuditAttribute]` metadata does NOT depend on a file that only defines a method / property named `MyAuditAttribute`), and `DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotFanOutWhenMultipleSameNameAttributeClasses` (two same-named attribute classes in different namespaces cause the metadata edge to be dropped as ambiguous). Both the `impact` ambiguity guard (`IsMetadataTargetUnambiguous`) and the `deps` ambiguity CTE (`target_ambiguity`) are additionally tightened to count class-like definitions at symbol-identity level instead of path level: `IsMetadataTargetUnambiguous` now uses `SELECT DISTINCT f.path, s.line, s.name` so two same-named class-like definitions in one source file (e.g. idiomatic C# `namespace A { class FooAttribute { } } namespace B { class FooAttribute { } }` in a single .cs file) register as ambiguous; `target_ambiguity` joins `target_files` back through `files` + `symbols` with `COUNT(*)` over class-like symbol rows so the same same-file duplicate case correctly drops the `[Foo]` metadata edge. Path-level counting previously treated these same-file duplicates as count=1 because `SELECT DISTINCT f.path` and `target_files` (grouped by `dst.path`) both collapse the two definitions into a single row, so `cdidx deps` and `cdidx impact` were confidently reporting an ambiguous short-name metadata reference as a definite dependency / blast-radius edge — contradicting the design intent that "ambiguity" includes duplicates in one file. Added `DbReaderTests.GetFileDependencies_CSharpAttributeAmbiguityCountsSameFileDuplicateClassDefinitions` and `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CountsSameFileDuplicateDefinitions` to pin the symbol-identity-level counting for both `deps` and `impact`. `IsMetadataTargetUnambiguous` is additionally tightened to wrap `--path` / `--exclude-path` parameters through `EscapeLikeQuery` and bind them as `%...%` patterns, matching the LIKE semantics used by the rest of the reader (search / references / deps etc.); without the wrap the in-scope class-like count would underflow to 1 even when two same-named attribute classes sit side-by-side in the requested CLI subtree such as `--path src/A/`, and the metadata bypass would falsely fire. Added `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CliPathPatternEscaping_SuppressesWhenInScopeIsAmbiguous` to pin this. `CSharpNoArgAttributeRegex` is additionally widened to accept an optional generic-argument list after the attribute name (`(?:\s*<[^\n]+?>)?`) so generic no-arg attribute forms such as `[MyAudit]`, `[assembly: MyAttr]`, the multi-line `[\n MyAttr\n]`, and nested-generic forms like `[MyAttr>]` / `[MyOther>>]` are indexed as `attribute` references instead of falling through both `CallRegex` (requires `(`) and the old no-arg regex (which ended at `]` / `,` / EOL and therefore rejected `` between the name and `]`). The inner generic segment now uses a non-greedy `[^\n]+?` so nested `>` tokens (e.g. the closing `>` of an inner `Dictionary`) no longer terminate the match prematurely — the old `[^>\n]+` excluded `>` and therefore broke on the first inner `>`, dropping the reference row for nested-generic attribute usages. Without the widening those sites produced zero reference rows, so `deps` / `impact` / `references` / `inspect` / `analyze_symbol` all missed the dependency edge to the real `FooAttribute` class file. Added `ReferenceExtractorTests.Extract_CsharpGenericNoArgAttribute_ClassifiedAsAttribute`, `ReferenceExtractorTests.Extract_CsharpNestedGenericNoArgAttribute_ClassifiedAsAttribute`, `DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_StillIndexedAndResolvesToAttributeClass`, `DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_AssemblyTarget_IsIndexed`, and `DbReaderTests.GetFileDependencies_CSharpNestedGenericNoArgAttribute_ResolvesToAttributeClass` to pin the end-to-end behavior. Both the `impact` ambiguity guard (`IsMetadataTargetUnambiguous`) and the `deps` ambiguity path (`target_files` / `target_ambiguity`) are additionally tightened to treat a C# class as a valid `[Attr]` metadata target ONLY when its signature inherits from an `Attribute`-suffixed base class (`signature LIKE '%: %Attribute%'`). Plain `class MyAuditAttribute { }` without `: Attribute` inheritance is not a valid `[MyAudit]` target at compile time, so counting it as an ambiguity candidate would under-report the `impact` blast radius and suppress a real `deps` metadata edge when a same-named plain class coexists with the real attribute class. `target_files` now exposes a new `has_metadata_target_kind` MAX column that applies the same C# signature filter, and `metadata_raw_suppression` / `target_ambiguity` / the metadata clause in `edges` are all switched to `has_metadata_target_kind`; Java / Kotlin / Scala targets remain unaffected because their metadata markers cannot be captured with a single portable signature LIKE, so they keep the broader class-like candidate set. Added `DbReaderTests.GetFileDependencies_CSharp_PlainClassWithAttributeSuffixName_DoesNotCountAsAmbiguity` to pin the `deps` path. Affected: `src/CodeIndex/Database/DbReader.cs`, `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. - **MCP `callers` / `callees` `kind` schema, CLI help, and README `--kind` option row no longer advertise metadata kinds as valid filter values (#293 follow-up)** — The `callers` / `callees` handlers reject `kind: "attribute"` / `kind: "annotation"` with a usage error (CLI) / `isError` tool response (MCP), but the MCP tool schema's `kind` property description still listed all five kinds as if they were valid filter values (`"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"`), `cdidx --help` similarly lumped `callers` / `callees` together with `references` for the `--kind` help row, and the README `--kind` option table (EN line 436 / JP line 1307) still directed readers to pass `--kind attribute` or `--kind annotation` on `callers` / `callees`. Schema and handler now agree: the `callers` / `callees` `kind` description now reads `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."`, the CLI `--kind` help row splits `references` (accepts all five kinds) from `callers` / `callees` (call-graph kinds only, metadata rejected, redirects to `references`), and the README `--kind` row (both English and Japanese) now explicitly states that `callers` / `callees` reject `--kind attribute|annotation` with a usage error and points to `references --kind attribute|annotation` as the correct metadata-enumeration path. The MCP `references` tool description is unchanged because it still accepts every indexed kind. Added `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` to pin the corrected schema wording. Affected: `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `src/CodeIndex/Cli/ConsoleUi.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `README.md`. - **`deps`, `impact`, `references`, `inspect`, and `analyze_symbol` retain metadata references as compile-time dependency edges and canonicalize the C# attribute-suffix convention (#293 follow-up)** — `DbReader.GetFileDependencies` (backing `cdidx deps`) and `DbReader.GetFileDependencyHintsToResolvedType` (backing `cdidx impact` / MCP `impact_analysis` heuristic file-hint fallback) keep metadata references in their logical reference CTE. Renaming or removing an attribute class still breaks annotated sites such as `[JsonConverterAttribute]` at compile time, so file-level blast-radius analysis must surface those edges as real dependencies — now labeled `attribute` / `annotation` in `logical_reference_kind` (previously these same sites leaked in as phantom `call` rows before classification was corrected). `GetFileDependencies` additionally canonicalizes the C# attribute naming convention: idiomatic `[Foo]` usage is stored with `symbol_name = "Foo"` but the defining class is `FooAttribute`, so the logical-reference CTE now UNION-ALLs a suffix-aliased copy of every C# `attribute`-kind row with `'Attribute'` appended (only when the original symbol_name does not already end in `Attribute`) so the target-file join reaches the attribute class file. `GetFileDependencyHintsToResolvedType` also aggregates a per-candidate `has_metadata_ref` flag and bypasses the `SourceFileHasStructuredTypeEvidence` guard for edges whose contributing references are purely metadata, so pure-attribute consumer files such as `[MyAudit] class Svc` now surface through `impact MyAuditAttribute` instead of being filtered out. `ResolveImpactFallbackNames` symmetrically adds suffix-stripped aliases for C# class names ending in `Attribute`, so the BFS-less file-hint path can find `[Foo]` sites whose source symbol_name does not match the defining class name. The same canonicalization is propagated to the symbol-level reference reader: `DbReader.SearchReferences`, `CountSearchReferences`, and `CountSearchReferencesTotal` (backing `references`, `inspect`, MCP `analyze_symbol`) now compute a C#-only `queryAttributeAlias` (defined in the new `ComputeCSharpAttributeSuffixAlias` helper) and OR it into both exact (folded and NOCASE) and substring WHERE clauses, so `references MyAuditAttribute` (and `inspect MyAuditAttribute` / `analyze_symbol MyAuditAttribute`) surface the idiomatic `[MyAudit]` call site consistently with `deps` / `impact`. Substring mode uses an exact-OR for the alias (not a second LIKE) so user queries like `MyAuditAttribute` do not over-match unrelated names such as `FooAuditLog`. The alias is C# only, so Java / Kotlin / Scala / TypeScript annotation sites remain unaffected. `callers` / `callees` intentionally skip the alias because they filter to the shared call-graph kinds (`call`, `instantiate`, `subscribe`) and reject `--kind attribute|annotation` at the CLI / MCP boundary — those commands model the dynamic call graph, not the dependency graph. The alias branch is additionally scoped so it cannot produce false positives: `ComputeCSharpAttributeSuffixAlias` returns `null` when `referenceKind` is a non-`attribute` call-graph kind (so `references FooAttribute --kind call` stays strict and does not match `Foo()`), and the SQL alias disjunct in all three reader methods is clamped to `f.lang = 'csharp' AND r.reference_kind = 'attribute'` so an unscoped `references FooAttribute` (no `--lang` / no `--kind`) still cannot bleed into Java annotations or C# call rows. Suffix detection is also case-insensitive (`OrdinalIgnoreCase`) so `references myauditattribute` and `inspect MyAuditATTRIBUTE` still produce the `MyAudit` alias, consistent with the NOCASE / folded contract of the surrounding exact / substring query paths. Added regression coverage `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies` (runtime + metadata edges both surface), `DbReaderTests.GetFileDependencies_MatchesCSharpAttributeSuffixConvention` (`[MyAudit]` → `MyAuditAttribute` canonicalization through `deps`), `DbReaderTests.SearchReferences_MatchesCSharpAttributeSuffixConvention_Substring` / `_Exact` / `_CSharpAttributeSuffixAliasDoesNotBleedToOtherLanguages` (same canonicalization through `references` / `inspect` / `analyze_symbol`, plus C#-only scope guard), `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_NotAppliedToCallKind` (alias must not fire under `--kind call`), `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_UnscopedLangStillLimitsToCSharpAttributeRows` (unscoped query must not match Java annotations or C# call rows), and `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_CaseInsensitiveQuery` (lowercase / mixed-case queries still produce the alias). Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. - **`callers` / `callees` now reject `--kind attribute|annotation` at the CLI and MCP boundary, and Swift / Gradle `@` metadata is reclassified as `annotation` (#293 follow-up)** — Metadata rows (`attribute` / `annotation`) are attributed to their enclosing body-range symbol rather than the annotated target, so `callers Obsolete --kind attribute` would silently return `[Obsolete] void M()` under the enclosing class (not `M`), and file-level targets such as `[assembly: CLSCompliant]` or Kotlin `@file:JvmName` would drop entirely because `container_name` is `NULL`. `QueryCommandRunner.RunCallers` / `RunCallees` now reject `--kind attribute` and `--kind annotation` with `CommandExitCodes.UsageError`, and the MCP `callers` / `callees` tools return an `isError` tool response; both paths direct users to `references --kind attribute|annotation`, which IS a correct metadata enumeration path. Separately, `AnnotationLanguages` now includes `swift` and `gradle`, so Swift `@available(iOS 13.0, *)` / `@objc` / `@MainActor` and Gradle/Groovy `@CompileStatic` / `@TaskAction` are recorded as `annotation` instead of `call` (previously the `@Name(args)` forms leaked into `callers` / `callees` / `hotspots` / `impact` as phantom call edges, and the no-arg `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` forms dropped from the index entirely because no-arg annotation emission was gated to languages in this set). Added `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError` (theory, 4 cases), `McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError` (theory, 4 cases), `ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`, and `ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation`. Affected: `src/CodeIndex/Cli/QueryCommandRunner.cs`, `src/CodeIndex/Mcp/McpToolHandlers.cs`, `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. @@ -686,7 +686,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **DEVELOPER_GUIDE が SQLite の位置付けとリリース番号の例示を明確化** — cdidx が「設定ゼロ・単一ファイル・本番依存1個」の CLI であり、その唯一の本番依存が `Microsoft.Data.Sqlite` であることを明記した。あわせて、リリース手順のチェックリスト中に出てくる `1.9.0` は固定値ではなく例示であり、実際のリリース番号へ読み替える前提だと明示した。対象: `DEVELOPER_GUIDE.md`。 #### 修正 -- **`deps` の C# attribute サフィックス別名を class-like ターゲットに限定し、`impact` の metadata bypass も class-like ターゲット名が名前空間をまたいで曖昧な場合は抑止するよう修正 (#293 追加対応)** — 直前の #293 追加対応では、`deps` の logical-reference CTE に C# 限定で `symbol_name || 'Attribute'` のサフィックス別名を UNION することで `[Foo]` の attribute consumer が `FooAttribute` クラスファイルへ到達するようにし、`impact`(`GetFileDependencyHintsToResolvedType`)も寄与参照に `attribute` / `annotation` が含まれていれば structured-evidence guard をバイパスするようにしていたが、どちらも広すぎた。`deps` 側では別名が純粋に名前だけで join していたため、`[Foo]` が同名の関数・プロパティ・変数(例: 無関係なヘルパーの `void MyAuditAttribute()`)を定義するファイルにも依存を張ってしまっていた。`impact` 側の metadata bypass も、対象名が一意に class-like 定義を指すかを確認していなかったため、`MyAuditAttribute` のような短い名前が複数の namespace に存在すると rename / remove の blast-radius が過剰報告されていた(`attribute` / `annotation` 参照行は短い名前しか保持できず曖昧性解消ができない)。`DbReader.GetFileDependencies` は合成された別名行に `is_attribute_alias = 1`(本物の行は `0`)をタグ付けし、`source_name_counts` では `MIN` でフラグを伝播し、`edges` CTE の join で別名 edge をターゲット側 kind が `class` / `struct` / `interface` のときだけに制限するようにした。`target_files` CTE も `DISTINCT (path, lang, name, kind)` から `GROUP BY (path, lang, name)` + `MAX(...)` の `has_class_like_kind` フラグに切り替えたため、class と同名関数(例: C# コンストラクタ `Target()` と `class Target`)を同じファイルで定義しているケースでもターゲット行が 2 本に増えず `deps` の参照数を水増ししない。`DbReader.GetFileDependencyHintsToResolvedType` は新ヘルパー `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` を呼び、*active な impact スコープ内* で同名の class-like 定義数を数える(graph 対応言語に加えて、呼び出し側の `--lang` / `--path` / `--exclude-path` / `--exclude-tests` を同じフィルタで適用)。in-scope でその数が 1 以下のときに限り metadata evidence-guard bypass を適用する。スコープ内で曖昧なときは metadata 単独 edge も通常の `SourceFileHasStructuredTypeEvidence` に戻り、純 `[MyAudit]` consumer は誤帰属せずに落ちる。スコープ尊重が重要: 別言語 / 別 path / test ツリーにある同名クラスが C# の metadata bypass を潰してはならない(参照側が既に graph 対応 join で言語修飾されているため、曖昧性は scope 内で判定すべき)。README の `impact` 契約も「active な `--lang` / `--path` / `--exclude-path` / `--exclude-tests` スコープ」と明記している。既存の C# attribute deps テスト `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`、曖昧ではない単一定義の `impact` テスト、および既存の graph-queries dedupe テストは全てグリーンのまま。回帰テストとして `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets`(同名関数があっても虚偽 edge が出ないこと)、`DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget`(impact スコープ内で `MyAuditAttribute` が複数ある場合に metadata heuristic hint を抑止)、`DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous`(単一クラス定義のみのときは従来どおり `[MyAudit]` consumer を浮かせる)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope`(別言語の同名 annotation は `--lang csharp` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope`(スコープ外 `src/B/` の同名クラスは `src/A/` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests`(test ツリーの同名クラスは `--exclude-tests` クエリを潰さない)を追加。`ResolveImpactFallbackNames` も併せて引き締め、C# の `Attribute` サフィックス別名は *解決済み定義自身* の名前にのみ適用するようにした: 以前は同一ファイル内の fallback 名全てに別名を付けていたため、`FooAttribute` を定義するファイルに nested で `BarAttribute` が存在するだけで `impact FooAttribute` が `[Bar]` 利用サイトを `FooAttribute` の影響範囲として誤報告していた。`DbReaderTests.GetFileDependencyHints_CSharpAttributeSuffixAlias_DoesNotLeakToSameFileSiblings` を追加して固定。さらに `GetFileDependencies` を再構成し、metadata 行(`attribute` / `annotation`)が bare-name の call-graph ターゲットに漏れないようにした: `logical_references` CTE に `is_metadata` フラグを付与し、`source_name_counts` の GROUP BY を `(source_file_id, source_path, source_lang, symbol_name, is_attribute_alias, is_metadata)` に拡張して metadata と非 metadata で同じ symbol_name の行を別グループ化する。`edges` CTE は全 metadata edge(合成別名だけでなく実体行も)に `tf.has_class_like_kind = 1` を要求し、さらに 2 つの CTE で残る誤検知を潰す: `metadata_raw_suppression` は同じ source file で合成別名 `FooAttribute` が class-like ターゲットに解決できているとき、生の C# `[Foo]` 行を落とす(`[Foo]` が `class FooAttribute` と無関係な同名 `class Foo` に fan-out しない)。`target_ambiguity` はスコープ内で同名 class-like ターゲットが複数あるとき metadata edge を落とす(曖昧な `[MyAudit]` は deps で fan-out させず、`impact` / `references` に判断を委ねる)。既存の `GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies`(`[JsonConverter(...)]` → `class JsonConverter : Attribute` をターゲット名に `Attribute` サフィックスがなくても固定)は、class-like ターゲットが 1 つしかないので `target_ambiguity` で残り、`is_metadata` / class-like 制限も受理し、合成 `JsonConverterAttribute` 別名は class-like に解決できないので `metadata_raw_suppression` も発火せず、そのまま通る。回帰テストとして `DbReaderTests.GetFileDependencies_CSharpAttributeRawDoesNotLeakToBareNameClass_WhenSuffixTargetExists`(別名が `class MyAuditAttribute` に解決できたとき生 `[MyAudit]` 行は抑止され、無関係な plain `class MyAudit` ファイルに依存しない)、`DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotLeakToSameNameMethodOrProperty`(完全形 `[MyAuditAttribute]` は同名メソッド / プロパティしか持たないファイルに依存しない)、`DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotFanOutWhenMultipleSameNameAttributeClasses`(別名前空間に同名 attribute クラスが 2 つあると metadata edge はあいまいとして落とす)を追加。加えて `impact` の曖昧性ガード (`IsMetadataTargetUnambiguous`) と `deps` の曖昧性 CTE (`target_ambiguity`) も、path 単位ではなく symbol identity 単位で class-like 定義を数えるように引き締めた: `IsMetadataTargetUnambiguous` は `SELECT DISTINCT f.path, s.line, s.name` に変更し、C# でよくある 1 つの .cs ファイル内に別名前空間で同名 class-like が 2 つ定義されている形 (`namespace A { class FooAttribute { } } namespace B { class FooAttribute { } }`) でも ambiguity を 2 として検出する。`target_ambiguity` は `target_files` を `files` + `symbols` に JOIN し直して `COUNT(*)` を class-like symbol 行に対して取るため、同じ同名ファイル内重複ケースでも `[Foo]` metadata edge を正しく落とせる。path 単位カウントでは `SELECT DISTINCT f.path` も `target_files`(`dst.path` で GROUP BY)も同一ファイル内の 2 定義を 1 行に潰してしまうため、従来は count=1 として曖昧性を取りこぼし、`cdidx deps` と `cdidx impact` が同名 short-name metadata 参照を確定的な依存 / blast-radius edge として出していた。これは曖昧性に "duplicates in one file" を含める前提の設計契約に反していた。`deps` と `impact` 両方で symbol-identity 単位カウントを固定する `DbReaderTests.GetFileDependencies_CSharpAttributeAmbiguityCountsSameFileDuplicateClassDefinitions` と `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CountsSameFileDuplicateDefinitions` を追加。さらに `IsMetadataTargetUnambiguous` の `--path` / `--exclude-path` パラメータを、他の reader 経路(search / references / deps 等)と同じ LIKE セマンティクスに揃えるため `EscapeLikeQuery` でエスケープしたうえで `%...%` で包んでバインドするようにした。生値のままだと `--path src/A/` のような CLI 形では LIKE が一致せず、リクエストされたサブツリーに同名 attribute クラスが 2 件並んでいても in-scope class-like カウントが 1 に過小化され、本来抑止すべき metadata bypass が誤発火してしまっていた。`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CliPathPatternEscaping_SuppressesWhenInScopeIsAmbiguous` を追加して固定する。あわせて `CSharpNoArgAttributeRegex` を拡張し、属性名の直後に任意のジェネリック引数リスト `(?:\s*<[^>\n]+>)?` を許容するようにしたため、`[MyAudit]`・`[assembly: MyAttr]`・複数行の `[\n MyAttr\n]` のような引数なしジェネリック属性も `attribute` 参照としてインデックスされるようになった。従来は `CallRegex`(`(` 必須)でも旧 no-arg regex(`]` / `,` / EOL で終端しなければならず、名前と `]` の間に `` があると拒否)でも拾えず、参照行が 0 件となり、`deps` / `impact` / `references` / `inspect` / `analyze_symbol` が実属性クラス `FooAttribute` への依存エッジを全て取りこぼしていた。`ReferenceExtractorTests.Extract_CsharpGenericNoArgAttribute_ClassifiedAsAttribute`、`DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_StillIndexedAndResolvesToAttributeClass`、`DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_AssemblyTarget_IsIndexed` を追加して end-to-end の振る舞いを固定する。対象: `src/CodeIndex/Database/DbReader.cs`、`src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 +- **`deps` の C# attribute サフィックス別名を class-like ターゲットに限定し、`impact` の metadata bypass も class-like ターゲット名が名前空間をまたいで曖昧な場合は抑止するよう修正 (#293 追加対応)** — 直前の #293 追加対応では、`deps` の logical-reference CTE に C# 限定で `symbol_name || 'Attribute'` のサフィックス別名を UNION することで `[Foo]` の attribute consumer が `FooAttribute` クラスファイルへ到達するようにし、`impact`(`GetFileDependencyHintsToResolvedType`)も寄与参照に `attribute` / `annotation` が含まれていれば structured-evidence guard をバイパスするようにしていたが、どちらも広すぎた。`deps` 側では別名が純粋に名前だけで join していたため、`[Foo]` が同名の関数・プロパティ・変数(例: 無関係なヘルパーの `void MyAuditAttribute()`)を定義するファイルにも依存を張ってしまっていた。`impact` 側の metadata bypass も、対象名が一意に class-like 定義を指すかを確認していなかったため、`MyAuditAttribute` のような短い名前が複数の namespace に存在すると rename / remove の blast-radius が過剰報告されていた(`attribute` / `annotation` 参照行は短い名前しか保持できず曖昧性解消ができない)。`DbReader.GetFileDependencies` は合成された別名行に `is_attribute_alias = 1`(本物の行は `0`)をタグ付けし、`source_name_counts` では `MIN` でフラグを伝播し、`edges` CTE の join で別名 edge をターゲット側 kind が `class` / `struct` / `interface` のときだけに制限するようにした。`target_files` CTE も `DISTINCT (path, lang, name, kind)` から `GROUP BY (path, lang, name)` + `MAX(...)` の `has_class_like_kind` フラグに切り替えたため、class と同名関数(例: C# コンストラクタ `Target()` と `class Target`)を同じファイルで定義しているケースでもターゲット行が 2 本に増えず `deps` の参照数を水増ししない。`DbReader.GetFileDependencyHintsToResolvedType` は新ヘルパー `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` を呼び、*active な impact スコープ内* で同名の class-like 定義数を数える(graph 対応言語に加えて、呼び出し側の `--lang` / `--path` / `--exclude-path` / `--exclude-tests` を同じフィルタで適用)。in-scope でその数が 1 以下のときに限り metadata evidence-guard bypass を適用する。スコープ内で曖昧なときは metadata 単独 edge も通常の `SourceFileHasStructuredTypeEvidence` に戻り、純 `[MyAudit]` consumer は誤帰属せずに落ちる。スコープ尊重が重要: 別言語 / 別 path / test ツリーにある同名クラスが C# の metadata bypass を潰してはならない(参照側が既に graph 対応 join で言語修飾されているため、曖昧性は scope 内で判定すべき)。README の `impact` 契約も「active な `--lang` / `--path` / `--exclude-path` / `--exclude-tests` スコープ」と明記している。既存の C# attribute deps テスト `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`、曖昧ではない単一定義の `impact` テスト、および既存の graph-queries dedupe テストは全てグリーンのまま。回帰テストとして `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets`(同名関数があっても虚偽 edge が出ないこと)、`DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget`(impact スコープ内で `MyAuditAttribute` が複数ある場合に metadata heuristic hint を抑止)、`DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous`(単一クラス定義のみのときは従来どおり `[MyAudit]` consumer を浮かせる)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope`(別言語の同名 annotation は `--lang csharp` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope`(スコープ外 `src/B/` の同名クラスは `src/A/` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests`(test ツリーの同名クラスは `--exclude-tests` クエリを潰さない)を追加。`ResolveImpactFallbackNames` も併せて引き締め、C# の `Attribute` サフィックス別名は *解決済み定義自身* の名前にのみ適用するようにした: 以前は同一ファイル内の fallback 名全てに別名を付けていたため、`FooAttribute` を定義するファイルに nested で `BarAttribute` が存在するだけで `impact FooAttribute` が `[Bar]` 利用サイトを `FooAttribute` の影響範囲として誤報告していた。`DbReaderTests.GetFileDependencyHints_CSharpAttributeSuffixAlias_DoesNotLeakToSameFileSiblings` を追加して固定。さらに `GetFileDependencies` を再構成し、metadata 行(`attribute` / `annotation`)が bare-name の call-graph ターゲットに漏れないようにした: `logical_references` CTE に `is_metadata` フラグを付与し、`source_name_counts` の GROUP BY を `(source_file_id, source_path, source_lang, symbol_name, is_attribute_alias, is_metadata)` に拡張して metadata と非 metadata で同じ symbol_name の行を別グループ化する。`edges` CTE は全 metadata edge(合成別名だけでなく実体行も)に `tf.has_class_like_kind = 1` を要求し、さらに 2 つの CTE で残る誤検知を潰す: `metadata_raw_suppression` は同じ source file で合成別名 `FooAttribute` が class-like ターゲットに解決できているとき、生の C# `[Foo]` 行を落とす(`[Foo]` が `class FooAttribute` と無関係な同名 `class Foo` に fan-out しない)。`target_ambiguity` はスコープ内で同名 class-like ターゲットが複数あるとき metadata edge を落とす(曖昧な `[MyAudit]` は deps で fan-out させず、`impact` / `references` に判断を委ねる)。既存の `GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies`(`[JsonConverter(...)]` → `class JsonConverter : Attribute` をターゲット名に `Attribute` サフィックスがなくても固定)は、class-like ターゲットが 1 つしかないので `target_ambiguity` で残り、`is_metadata` / class-like 制限も受理し、合成 `JsonConverterAttribute` 別名は class-like に解決できないので `metadata_raw_suppression` も発火せず、そのまま通る。回帰テストとして `DbReaderTests.GetFileDependencies_CSharpAttributeRawDoesNotLeakToBareNameClass_WhenSuffixTargetExists`(別名が `class MyAuditAttribute` に解決できたとき生 `[MyAudit]` 行は抑止され、無関係な plain `class MyAudit` ファイルに依存しない)、`DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotLeakToSameNameMethodOrProperty`(完全形 `[MyAuditAttribute]` は同名メソッド / プロパティしか持たないファイルに依存しない)、`DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotFanOutWhenMultipleSameNameAttributeClasses`(別名前空間に同名 attribute クラスが 2 つあると metadata edge はあいまいとして落とす)を追加。加えて `impact` の曖昧性ガード (`IsMetadataTargetUnambiguous`) と `deps` の曖昧性 CTE (`target_ambiguity`) も、path 単位ではなく symbol identity 単位で class-like 定義を数えるように引き締めた: `IsMetadataTargetUnambiguous` は `SELECT DISTINCT f.path, s.line, s.name` に変更し、C# でよくある 1 つの .cs ファイル内に別名前空間で同名 class-like が 2 つ定義されている形 (`namespace A { class FooAttribute { } } namespace B { class FooAttribute { } }`) でも ambiguity を 2 として検出する。`target_ambiguity` は `target_files` を `files` + `symbols` に JOIN し直して `COUNT(*)` を class-like symbol 行に対して取るため、同じ同名ファイル内重複ケースでも `[Foo]` metadata edge を正しく落とせる。path 単位カウントでは `SELECT DISTINCT f.path` も `target_files`(`dst.path` で GROUP BY)も同一ファイル内の 2 定義を 1 行に潰してしまうため、従来は count=1 として曖昧性を取りこぼし、`cdidx deps` と `cdidx impact` が同名 short-name metadata 参照を確定的な依存 / blast-radius edge として出していた。これは曖昧性に "duplicates in one file" を含める前提の設計契約に反していた。`deps` と `impact` 両方で symbol-identity 単位カウントを固定する `DbReaderTests.GetFileDependencies_CSharpAttributeAmbiguityCountsSameFileDuplicateClassDefinitions` と `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CountsSameFileDuplicateDefinitions` を追加。さらに `IsMetadataTargetUnambiguous` の `--path` / `--exclude-path` パラメータを、他の reader 経路(search / references / deps 等)と同じ LIKE セマンティクスに揃えるため `EscapeLikeQuery` でエスケープしたうえで `%...%` で包んでバインドするようにした。生値のままだと `--path src/A/` のような CLI 形では LIKE が一致せず、リクエストされたサブツリーに同名 attribute クラスが 2 件並んでいても in-scope class-like カウントが 1 に過小化され、本来抑止すべき metadata bypass が誤発火してしまっていた。`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CliPathPatternEscaping_SuppressesWhenInScopeIsAmbiguous` を追加して固定する。あわせて `CSharpNoArgAttributeRegex` を拡張し、属性名の直後に任意のジェネリック引数リスト `(?:\s*<[^\n]+?>)?` を許容するようにしたため、`[MyAudit]`・`[assembly: MyAttr]`・複数行の `[\n MyAttr\n]`・ネストジェネリックの `[MyAttr>]` や `[MyOther>>]` のような引数なしジェネリック属性も `attribute` 参照としてインデックスされるようになった。内側ジェネリックセグメントは非貪欲な `[^\n]+?` に変更し、内側の `>`(例: `Dictionary` を閉じる `>`)で match が打ち切られないようにした — 従来の `[^>\n]+` は `>` を除外していたため最初の内側 `>` で崩れ、ネストジェネリック属性の参照行が落ちていた。従来は `CallRegex`(`(` 必須)でも旧 no-arg regex(`]` / `,` / EOL で終端しなければならず、名前と `]` の間に `` があると拒否)でも拾えず、参照行が 0 件となり、`deps` / `impact` / `references` / `inspect` / `analyze_symbol` が実属性クラス `FooAttribute` への依存エッジを全て取りこぼしていた。`ReferenceExtractorTests.Extract_CsharpGenericNoArgAttribute_ClassifiedAsAttribute`、`ReferenceExtractorTests.Extract_CsharpNestedGenericNoArgAttribute_ClassifiedAsAttribute`、`DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_StillIndexedAndResolvesToAttributeClass`、`DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_AssemblyTarget_IsIndexed`、`DbReaderTests.GetFileDependencies_CSharpNestedGenericNoArgAttribute_ResolvesToAttributeClass` を追加して end-to-end の振る舞いを固定する。加えて、`impact` の曖昧性ガード (`IsMetadataTargetUnambiguous`) と `deps` の曖昧性判定 (`target_files` / `target_ambiguity`) は、C# クラスが有効な `[Attr]` metadata target になるのは「signature が `Attribute` サフィックス付き基底を継承している」場合 (`signature LIKE '%: %Attribute%'`) に限定するよう引き締めた。`: Attribute` 継承を書いていない plain `class MyAuditAttribute { }` はコンパイル時に `[MyAudit]` の target にならないため、曖昧性候補として数えると `impact` の影響範囲が過少報告され、`deps` 側でも同名 plain クラスが共存する scene で実 attribute クラスへの metadata edge が落ちてしまう。`target_files` に新しい `has_metadata_target_kind` MAX 列を追加して同じ C# signature フィルタを適用し、`metadata_raw_suppression` / `target_ambiguity` / `edges` の metadata 節はすべて `has_metadata_target_kind` に切り替えた。Java / Kotlin / Scala は portable な signature LIKE で metadata marker を表現しづらいため従来通り class-like 全体を候補に残す。`deps` 側の挙動を固定する `DbReaderTests.GetFileDependencies_CSharp_PlainClassWithAttributeSuffixName_DoesNotCountAsAmbiguity` を追加。対象: `src/CodeIndex/Database/DbReader.cs`、`src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 - **MCP の `callers` / `callees` の `kind` schema・CLI help・README の `--kind` オプション行が metadata kinds を有効フィルタ値として広告しないよう修正 (#293 追加対応)** — `callers` / `callees` の handler は `kind: "attribute"` / `kind: "annotation"` を usage error(CLI)/ `isError` ツールレスポンス(MCP)で拒否するにもかかわらず、MCP ツール schema の `kind` プロパティ説明は 5 種すべてを列挙した `"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"` のままで、`cdidx --help` の `--kind` 行も `callers` / `callees` を `references` と同じ行にまとめていた。さらに README の `--kind` オプション表(英語 436 行目 / 日本語 1307 行目)も、`callers` / `callees` で `--kind attribute` / `--kind annotation` を渡せるかのように案内していた。schema と handler の契約を一致させ、`callers` / `callees` の `kind` description を `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."` に修正し、CLI の `--kind` help 行も `references`(5 種すべて受理)と `callers` / `callees`(call-graph 種別のみ、metadata kinds は拒否して `references` へ誘導)を分けるようにし、README の `--kind` 行(英語・日本語の両方)も `callers` / `callees` が `--kind attribute|annotation` を usage error で拒否すること、metadata 列挙の正規経路が `references --kind attribute|annotation` であることを明示する文言に直した。MCP の `references` ツール説明は全 reference kind を受け付ける契約のままなので変更なし。修正後の schema 文言を固定する `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` を追加。対象: `src/CodeIndex/Mcp/McpToolDefinitions.cs`、`src/CodeIndex/Cli/ConsoleUi.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`、`README.md`。 - **`deps`・`impact`・`references`・`inspect`・`analyze_symbol` が metadata 参照を compile-time 依存として保持し、C# の attribute サフィックス命名規約を正規化 (#293 追加対応)** — `cdidx deps` の実装である `DbReader.GetFileDependencies` と、`cdidx impact` / MCP `impact_analysis` の heuristic file-hint フォールバックである `DbReader.GetFileDependencyHintsToResolvedType` は、logical reference CTE に metadata 参照も含める。attribute クラスを rename / 削除すれば `[JsonConverterAttribute]` のような注釈サイトも compile-time で壊れるため、ファイル単位の blast-radius 分析はそれらも本物の依存 edge として表示する必要があり、`logical_reference_kind` では `attribute` / `annotation` として正しくラベル付けされる(分類修正前は同じサイトが phantom `call` として漏れていた)。さらに `GetFileDependencies` は C# の attribute 命名規約を正規化する: 慣用的な `[Foo]` は `symbol_name = "Foo"` で保存されるが、定義クラスは `FooAttribute` 命名となるため、logical-reference CTE に C# `attribute` 種別の行をサフィックス `'Attribute'` を付与した別名コピーとして UNION ALL で追加する(元の symbol_name が既に `Attribute` で終わっている場合は重複しないようスキップ)。これにより target ファイル join が attribute クラス側のファイルへ到達する。`GetFileDependencyHintsToResolvedType` はさらに candidate ごとに `has_metadata_ref` フラグを集約し、寄与する参照が metadata だけである edge については `SourceFileHasStructuredTypeEvidence` guard をバイパスするため、`[MyAudit] class Svc` のような純 attribute consumer ファイルも `impact MyAuditAttribute` で出るようになった(以前は filter で落ちていた)。`ResolveImpactFallbackNames` も対称的に、`Attribute` で終わる C# クラス名にはサフィックスを外した別名を追加するため、BFS を使わない file-hint 経路でも source symbol_name が定義クラス名と一致しない `[Foo]` サイトを発見できる。同じ正規化をシンボル単位の参照 reader にも伝播する: `references`・`inspect`・MCP `analyze_symbol` を支える `DbReader.SearchReferences`・`CountSearchReferences`・`CountSearchReferencesTotal` は、新しいヘルパー `ComputeCSharpAttributeSuffixAlias` で C# 限定の `queryAttributeAlias` を計算し、exact(folded と NOCASE)・substring の両方の WHERE 句に OR で追加する。これにより `references MyAuditAttribute`(および `inspect MyAuditAttribute` / `analyze_symbol MyAuditAttribute`)でも慣用的な `[MyAudit]` 参照サイトが `deps` / `impact` と一貫して surface する。substring モードでは alias を二つ目の LIKE にせず exact-OR で束ねているため、ユーザーが `MyAuditAttribute` と打った時に `FooAuditLog` のような無関係な名前に over-match しない。alias は C# 限定なので、Java / Kotlin / Scala / TypeScript の annotation サイトには影響しない。`callers` / `callees` は共有の call-graph 種別 (`call` / `instantiate` / `subscribe`) に絞って `--kind attribute|annotation` を CLI / MCP 境界で拒否する契約なので、alias は意図的に適用しない(dynamic call graph と dependency graph は別契約)。さらに alias 節が誤一致を生まないようスコープする: `ComputeCSharpAttributeSuffixAlias` は `referenceKind` が `attribute` 以外の call-graph 種別のときは `null` を返す(`references FooAttribute --kind call` は厳格一致のままで `Foo()` に一致しない)、3 つの reader メソッドの SQL alias 節は `f.lang = 'csharp' AND r.reference_kind = 'attribute'` に限定するため、`--lang` / `--kind` 無指定の `references FooAttribute` でも Java の annotation や C# の call 行を拾わない。suffix 検出も `OrdinalIgnoreCase` にして case-insensitive 化したため、`references myauditattribute` / `inspect MyAuditATTRIBUTE` のような小文字・混在ケースでも alias `MyAudit` を生成でき、周辺の NOCASE / folded 契約と整合する。回帰テスト `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies`(runtime と metadata の両方の edge が出ることを固定)、`DbReaderTests.GetFileDependencies_MatchesCSharpAttributeSuffixConvention`(`deps` 経由での `[MyAudit]` → `MyAuditAttribute` 正規化)、`DbReaderTests.SearchReferences_MatchesCSharpAttributeSuffixConvention_Substring` / `_Exact` / `_CSharpAttributeSuffixAliasDoesNotBleedToOtherLanguages`(`references` / `inspect` / `analyze_symbol` での同一正規化と C# 限定スコープ)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_NotAppliedToCallKind`(`--kind call` で alias が発火しないこと)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_UnscopedLangStillLimitsToCSharpAttributeRows`(無指定でも Java annotation や C# call 行に一致しないこと)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_CaseInsensitiveQuery`(小文字・混在ケースのクエリでも alias を生成)を追加。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 - **`callers` / `callees` が CLI と MCP 境界で `--kind attribute|annotation` を拒否し、Swift / Gradle の `@` metadata を `annotation` に再分類 (#293 追加対応)** — metadata 行 (`attribute` / `annotation`) は注釈対象そのものではなく body-range の外側シンボルに帰属するため、`callers Obsolete --kind attribute` は `[Obsolete] void M()` を `M` ではなく外側クラスの下に黙って返し、`[assembly: CLSCompliant]` や Kotlin の `@file:JvmName` のような file-level target は `container_name = NULL` で完全脱落していた。`QueryCommandRunner.RunCallers` / `RunCallees` は `--kind attribute` / `--kind annotation` を `CommandExitCodes.UsageError` で弾き、MCP の `callers` / `callees` ツールは `isError` ツールレスポンスを返すようにした。どちらの経路も、metadata 列挙の正しいパスである `references --kind attribute|annotation` へ誘導する。あわせて `AnnotationLanguages` に `swift` と `gradle` を追加し、Swift の `@available(iOS 13.0, *)` / `@objc` / `@MainActor` や Gradle / Groovy の `@CompileStatic` / `@TaskAction` を `call` ではなく `annotation` として記録するようにした(修正前は引数付きの `@Name(args)` 形は `callers` / `callees` / `hotspots` / `impact` に phantom call edge として漏れ、引数なしの `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` 形はこの集合で gate されていた no-arg annotation 出力から外れてインデックス自体に残らなかった)。回帰テストとして `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError`(theory 4 ケース)、`McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError`(theory 4 ケース)、`ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`、`ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation` を追加。対象: `src/CodeIndex/Cli/QueryCommandRunner.cs`、`src/CodeIndex/Mcp/McpToolHandlers.cs`、`src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 diff --git a/src/CodeIndex/Database/DbReader.cs b/src/CodeIndex/Database/DbReader.cs index aad715f5ed..7f42ad140a 100644 --- a/src/CodeIndex/Database/DbReader.cs +++ b/src/CodeIndex/Database/DbReader.cs @@ -1945,6 +1945,22 @@ private bool IsMetadataTargetUnambiguous( // (例: `namespace A { class MyAuditAttribute { } } namespace B { class // MyAuditAttribute { } }`) でも ambiguity を 2 として検出できる。DISTINCT // f.path のままだと 1 に潰れ、metadata bypass が誤って有効化される。 + // For C# specifically, only count class-like definitions that are actually + // eligible as attribute metadata targets — i.e. signatures that contain + // `: ...Attribute` inheritance (e.g. `: Attribute`, `: System.Attribute`, + // `: BaseLogAttribute`). A plain `class MyAuditAttribute { }` without the + // Attribute-suffixed base class is not a valid target for `[MyAudit]` at + // compile time, so counting it as an ambiguity candidate would falsely + // suppress the metadata bypass and under-report `impact`. Other languages + // (Java `@interface`, Kotlin `annotation class`, etc.) still count every + // class-like definition because their metadata-target markers are not as + // easily captured by a single signature pattern. + // C# に限り、曖昧性候補は「Attribute を継承している」class-like 定義だけに絞る。 + // シグネチャに `: ...Attribute` を含むもの (`: Attribute`, `: System.Attribute`, + // `: BaseLogAttribute` 等) のみが `[MyAudit]` の実 target になり得るため、 + // `class MyAuditAttribute { }` のように継承を書いていない plain クラスを数える + // と `impact` が過少報告になる。他言語 (Java `@interface`, Kotlin `annotation + // class` 等) は単一 LIKE でマーカーを書きにくいので従来どおり class-like 全体を候補にする。 var sql = $@" SELECT COUNT(*) FROM ( SELECT DISTINCT f.path, s.line, s.name @@ -1952,6 +1968,7 @@ FROM symbols s JOIN files f ON s.file_id = f.id WHERE s.name = @metadataAmbigName COLLATE NOCASE AND s.kind IN ('class','struct','interface') + AND (f.lang != 'csharp' OR (s.signature IS NOT NULL AND s.signature LIKE '%: %Attribute%')) AND {supportedLangFilter}"; if (lang != null) { @@ -2666,10 +2683,26 @@ target_files AS ( -- いずれかが class 系であるかを MAX で覚える。kind を DISTINCT に含めると、 -- 同じ (path, lang, name) でも class と同名 function (C# のコンストラクタ等) -- が別行として残り、deps の参照カウントが膨らんでしまう。 + -- has_metadata_target_kind further narrows the class-like set to targets + -- that can legitimately be referenced as [Attribute] metadata. For C# + -- that additionally requires the class to inherit from something ending + -- in `Attribute` (signature LIKE '%: %Attribute%'); a plain + -- `class FooAttribute { }` without that inheritance is not a valid + -- [Foo] target at compile time and should not count as an ambiguity + -- candidate. Other languages keep the original class-like breadth. + -- has_metadata_target_kind は `[Attribute]` metadata target として + -- 有効な class-like のみを数える。C# に限り signature に + -- `: ...Attribute` を含むクラスだけを対象にする (`class FooAttribute { }` + -- のように Attribute 継承を書いていない plain クラスは `[Foo]` の target + -- にならないため)。Java/Kotlin 等は従来どおり class-like 全体を候補に残す。 SELECT dst.path AS target_path, dst.lang AS target_lang, s.name AS symbol_name, - MAX(CASE WHEN s.kind IN ('class','struct','interface') THEN 1 ELSE 0 END) AS has_class_like_kind + MAX(CASE WHEN s.kind IN ('class','struct','interface') THEN 1 ELSE 0 END) AS has_class_like_kind, + MAX(CASE WHEN s.kind IN ('class','struct','interface') + AND (dst.lang != 'csharp' + OR (s.signature IS NOT NULL AND s.signature LIKE '%: %Attribute%')) + THEN 1 ELSE 0 END) AS has_metadata_target_kind FROM symbols s JOIN files dst ON s.file_id = dst.id WHERE 1 = 1"; @@ -2708,7 +2741,7 @@ FROM logical_references_primary lrp JOIN target_files tf_alias ON tf_alias.target_lang = lrp.source_lang AND tf_alias.symbol_name = lrp.symbol_name || 'Attribute' - AND tf_alias.has_class_like_kind = 1 + AND tf_alias.has_metadata_target_kind = 1 WHERE lrp.source_lang = 'csharp' AND lrp.logical_reference_kind = 'attribute' AND lrp.symbol_name NOT LIKE '%Attribute' @@ -2743,7 +2776,16 @@ JOIN symbols s ON s.file_id = dst.id AND s.name = tf.symbol_name AND s.kind IN ('class','struct','interface') - WHERE tf.has_class_like_kind = 1 + -- Same C# metadata-eligibility filter as target_files: only count + -- attribute-derived classes for C#. Otherwise two same-named plain + -- classes (only one of which inherits from Attribute) would still + -- look ambiguous and suppress the metadata bypass. + -- target_files と同じ metadata 適格性フィルタ。C# では Attribute を + -- 継承している class のみを数え、plain 同名クラスが混在していても + -- ambiguity と誤判定しない。 + AND (dst.lang != 'csharp' + OR (s.signature IS NOT NULL AND s.signature LIKE '%: %Attribute%')) + WHERE tf.has_metadata_target_kind = 1 GROUP BY tf.target_lang, tf.symbol_name ), edges AS ( @@ -2773,7 +2815,7 @@ LEFT JOIN target_ambiguity ta -- 関数/プロパティ/変数を持つだけのファイルまで誤って依存してしまう。 -- 非 metadata の call-graph 参照は任意の kind に一致させて構わない -- (コンストラクタ呼び出しがクラス定義に結び付くケースなど)。 - AND (snc.is_metadata = 0 OR tf.has_class_like_kind = 1) + AND (snc.is_metadata = 0 OR tf.has_metadata_target_kind = 1) -- Drop raw C# '[Foo]' rows when the suffix alias already resolves -- to a class-like 'FooAttribute' target in the same source file. -- 同じ source file で suffix alias が class 系 'FooAttribute' に diff --git a/src/CodeIndex/Indexer/ReferenceExtractor.cs b/src/CodeIndex/Indexer/ReferenceExtractor.cs index 94875a875f..5b66d31c5d 100644 --- a/src/CodeIndex/Indexer/ReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/ReferenceExtractor.cs @@ -149,7 +149,7 @@ public static class ReferenceExtractor // (CallRegex 経路)や `.` / `::`(qualifier 継続)なら名前を確定させず、行末(`$`)・`]`・`,` // のいずれかで初めて採用する。 private static readonly Regex CSharpNoArgAttributeRegex = new( - @"(?[A-Za-z_]\w*)(?:\s*<[^>\n]+>)?\s*(?=[\],]|$)", + @"(?[A-Za-z_]\w*)(?:\s*<[^\n]+?>)?\s*(?=[\],]|$)", RegexOptions.Compiled); // No-arg Java-family annotation (`@Deprecated`, `@Override`, `@org.junit.Test`, `@field:Deprecated`). diff --git a/tests/CodeIndex.Tests/DbReaderTests.cs b/tests/CodeIndex.Tests/DbReaderTests.cs index 8d7188d2af..74cb3fda1e 100644 --- a/tests/CodeIndex.Tests/DbReaderTests.cs +++ b/tests/CodeIndex.Tests/DbReaderTests.cs @@ -3182,6 +3182,95 @@ public class Svc (f.TargetPath == "src/A/Inner1/MyAuditAttribute.cs" || f.TargetPath == "src/A/Inner2/MyAuditAttribute.cs")); } + [Fact] + public void GetFileDependencies_CSharp_PlainClassWithAttributeSuffixName_DoesNotCountAsAmbiguity() + { + // issue #293 round-16: same metadata-eligibility filter must apply to + // the `deps` command. target_files.has_metadata_target_kind and the + // target_ambiguity JOIN both require C# class-like targets to inherit + // from an Attribute-suffixed base, so a plain `MyAuditAttribute` + // cannot ambiguate the edge from `Svc.cs` to the real attribute class. + // issue #293 round-16: 同じ適格性フィルタを deps にも適用する。 + // target_files.has_metadata_target_kind と target_ambiguity JOIN は + // C# では Attribute suffix 継承を要求するため、plain `MyAuditAttribute` が + // 存在しても実 attribute クラスへのエッジは残る。 + InsertIndexedFile("src/Real/MyAuditAttribute.cs", "csharp", + """ + namespace Real; + + public sealed class MyAuditAttribute : System.Attribute + { + } + """); + InsertIndexedFile("src/Unrelated/MyAuditAttribute.cs", "csharp", + """ + namespace Unrelated; + + public sealed class MyAuditAttribute + { + } + """); + InsertIndexedFile("src/Real/Svc.cs", "csharp", + """ + namespace Real; + + [MyAudit] + public class Svc + { + } + """); + + var deps = _reader.GetFileDependencies( + limit: 50, + lang: "csharp"); + + Assert.Contains(deps, d => + d.SourcePath == "src/Real/Svc.cs" && + d.TargetPath == "src/Real/MyAuditAttribute.cs"); + Assert.DoesNotContain(deps, d => + d.SourcePath == "src/Real/Svc.cs" && + d.TargetPath == "src/Unrelated/MyAuditAttribute.cs"); + } + + [Fact] + public void GetFileDependencies_CSharpNestedGenericNoArgAttribute_ResolvesToAttributeClass() + { + // issue #293 round-16: the no-arg C# attribute regex must handle + // nested generic type arguments (e.g. `[MyAttr>]`). + // Previously the inner `<...>` segment excluded `>`, which broke on the + // first inner `>` and classified the reference as a call. + // issue #293 round-16: 引数なし C# 属性 regex が + // `[MyAttr>]` のようなネスト generic を + // 扱えること。以前は内側の `<...>` セグメントが `>` を除外していて、 + // 最初の内側 `>` で崩れて call として誤分類されていた。 + InsertIndexedFile("src/MyAttrAttribute.cs", "csharp", + """ + using System; + using System.Collections.Generic; + + public sealed class MyAttrAttribute : Attribute + { + } + """); + InsertIndexedFile("src/Svc.cs", "csharp", + """ + using System.Collections.Generic; + + [MyAttr>] + public class Svc + { + } + """); + + var deps = _reader.GetFileDependencies( + limit: 50, + lang: "csharp"); + + Assert.Contains(deps, d => + d.SourcePath == "src/Svc.cs" && + d.TargetPath == "src/MyAttrAttribute.cs"); + } + [Fact] public void GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests() { diff --git a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs index 139066f958..2e7bcb4795 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs @@ -1959,6 +1959,41 @@ public class C Assert.DoesNotContain(references, r => r.ReferenceKind == "call"); } + [Fact] + public void Extract_CsharpNestedGenericNoArgAttribute_ClassifiedAsAttribute() + { + // Regression (issue #293 round-16 follow-up): nested generic no-arg C# + // attributes such as `[MyAttr>]` and + // `[MyAttr>>]` must still classify as + // `attribute`. The previous `<[^>\n]+>` generic segment stopped at the + // first `>` and left the outer `>` dangling, so nested-generic + // attributes were silently dropped from the index. + // リグレッション (issue #293 round-16 補足): `[MyAttr>]` + // のような入れ子ジェネリック引数を持つ引数なし属性も `attribute` として + // 取り込まれること。`<...>` 内部で `>` を除外する以前の実装では最初の `>` + // で止まってしまい、nested generic 属性が黙って脱落していた。 + const string content = """ + [MyAttr>] + [MyOther>>] + [ + MyMulti>> + ] + public class C + { + } + """; + + var references = ReferenceExtractor.Extract(1, "csharp", content, []); + + var a = Assert.Single(references.Where(r => r.SymbolName == "MyAttr")); + Assert.Equal("attribute", a.ReferenceKind); + var b = Assert.Single(references.Where(r => r.SymbolName == "MyOther")); + Assert.Equal("attribute", b.ReferenceKind); + var c = Assert.Single(references.Where(r => r.SymbolName == "MyMulti")); + Assert.Equal("attribute", c.ReferenceKind); + Assert.DoesNotContain(references, r => r.ReferenceKind == "call"); + } + [Fact] public void Extract_CsharpNoArgParameterAttribute_ClassifiedAsAttribute() { From 8bcb08437466d502862a5125d214deefbeeef9e9 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 18 Apr 2026 16:55:23 +0900 Subject: [PATCH 27/30] Fix #293 accept indirect C# attribute inheritance and guard legacy DB without signature column Round-17 review follow-up: 1. Loosen the C# metadata-target filter from `signature LIKE '%: %Attribute%'` to `signature LIKE '%: %'`. Indirect inheritance like `class MyAuditAttribute : BaseAudit` where `BaseAudit : Attribute` is a valid `[MyAudit]` target at compile time, so requiring the immediate base class to end in `Attribute` wrongly excluded real attribute classes and dropped `deps` / `impact` edges. Transitive base-type resolution is not available at SQL time, so the inheritance-clause check is the best portable approximation. 2. Route the filter through a new `BuildCSharpMetadataTargetFilter` helper that degrades to `1 = 1` when the `symbols.signature` column is missing, so legacy / read-only DBs where `TryMigrateForRead` could not add the column do not crash with `no such column: s.signature` when running `deps` / `impact`. Added `DbReaderTests.GetFileDependencies_CSharp_IndirectAttributeInheritance_ResolvesAsMetadataTarget`. Co-Authored-By: Claude Opus 4 --- CHANGELOG.md | 4 +- src/CodeIndex/Database/DbReader.cs | 115 +++++++++++++++++-------- tests/CodeIndex.Tests/DbReaderTests.cs | 53 ++++++++++++ 3 files changed, 132 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b22b528cd..c659161e72 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/), - **Developer Guide now distinguishes SQLite's role from "zero dependencies" and labels release numbers as examples** — Clarified that cdidx remains a zero-configuration, single-file CLI with exactly one production dependency (`Microsoft.Data.Sqlite`), and made the release-workflow checklist explicitly state that `1.9.0` is only an example version to substitute during an actual release. Affected: `DEVELOPER_GUIDE.md`. #### Fixed -- **`deps` C# attribute suffix alias now only matches class-like targets, and `impact` metadata bypass is suppressed when the class-like target name is ambiguous across namespaces (#293 follow-up)** — The earlier `#293` follow-up made `deps` UNION a C#-only `symbol_name || 'Attribute'` suffix alias into the logical-reference CTE so `[Foo]` attribute consumers could reach `FooAttribute` class files, and made `impact` (`GetFileDependencyHintsToResolvedType`) bypass the structured-evidence guard whenever any contributing reference was `attribute` / `annotation`. Both changes were too broad. On `deps` the alias joined purely by name, so `[Foo]` would also depend on any file that merely defined a function / property / variable named `FooAttribute` (e.g. `void MyAuditAttribute()` in an unrelated helper). On `impact` the metadata bypass did not check whether the target name was uniquely a class-like definition, so when the same unqualified name (e.g. `MyAuditAttribute`) existed in multiple namespaces the rename/remove blast radius was over-reported — attribute / annotation reference rows only keep the short name and cannot be disambiguated. `DbReader.GetFileDependencies` now tags synthetic alias rows with `is_attribute_alias = 1` (real rows are `0`), propagates the flag through `source_name_counts` via `MIN`, and restricts alias edges to class-like target kinds (`class` / `struct` / `interface`) in the `edges` CTE join. The `target_files` CTE is also switched from `DISTINCT` over `(path, lang, name, kind)` to `GROUP BY (path, lang, name)` with a `MAX(...)` `has_class_like_kind` flag, so files that define both a class and a same-named function (e.g. the C# constructor `Target()` inside `class Target`) no longer fan out into two target rows and inflate the `deps` reference count. `DbReader.GetFileDependencyHintsToResolvedType` now calls a new `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` helper that counts class-like definitions with the same unqualified name *within the active impact scope* (graph-supported languages, plus the caller's `--lang` / `--path` / `--exclude-path` / `--exclude-tests` filters); the metadata evidence-guard bypass is only applied when that in-scope count is ≤ 1. If the name is ambiguous within scope, metadata-only edges fall through to the normal `SourceFileHasStructuredTypeEvidence` check, so pure `[MyAudit]` consumers are dropped instead of being mis-attributed. Scoping matters: a same-named class in an unrelated language / path / test tree must not suppress the C# metadata bypass, because attribute reference rows are already language-qualified through the graph-supported reference join, and the README contract for `impact` explicitly promises "active `--lang` / `--path` / `--exclude-path` / `--exclude-tests` scope". The C# attribute deps test `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`, the unambiguous single-definition `impact` test, and the pre-existing graph-queries dedupe test all still pass. Added `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets` (ambiguous same-name function must not produce a spurious edge), `DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget` (two `MyAuditAttribute` classes within the same impact scope suppress the metadata heuristic hint), `DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous` (single class definition still surfaces the `[MyAudit]` consumer), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope` (same-named Java annotation must not suppress a `--lang csharp` query), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope` (out-of-scope `src/B/` same-named class must not suppress a `src/A/`-scoped query), and `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests` (test-tree same-named class must not suppress an `--exclude-tests` query). `ResolveImpactFallbackNames` is additionally tightened so the C# `Attribute`-suffix alias is only applied to the resolved definition's own name: previously the alias was applied to every same-file fallback name, so a nested `BarAttribute` inside the file that defines `FooAttribute` caused `impact FooAttribute` to falsely attribute `[Bar]` use sites to `FooAttribute`'s blast radius. Added `DbReaderTests.GetFileDependencyHints_CSharpAttributeSuffixAlias_DoesNotLeakToSameFileSiblings` to pin this. `GetFileDependencies` is additionally restructured so metadata rows (`attribute` / `annotation`) cannot leak onto bare-name call-graph targets: the `logical_references` CTE now carries an `is_metadata` flag, `source_name_counts` groups by `(source_file_id, source_path, source_lang, symbol_name, is_attribute_alias, is_metadata)` so metadata and non-metadata rows with the same symbol_name stay separate, the `edges` CTE restricts ALL metadata edges (not just synthetic aliases) to `tf.has_class_like_kind = 1`, and two new CTEs resolve the remaining false-positive shapes: `metadata_raw_suppression` drops the raw C# `[Foo]` row when the synthetic `FooAttribute` alias already resolves to a class-like target in the same source file (so `[Foo]` does not fan out to both `class FooAttribute` and a plain `class Foo` that shares the bare name), and `target_ambiguity` drops metadata edges when the same-named symbol resolves to multiple class-like definitions in scope (so ambiguous `[MyAudit]` usage is left to `impact` / `references` to disambiguate rather than being fanned out to every same-named attribute class). The existing `GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies` test (which pins `[JsonConverter(...)]` → `class JsonConverter : Attribute` for a target class whose name does NOT end in `Attribute`) still passes because only one class-like target exists in scope, so `target_ambiguity` keeps it, the `is_metadata` / class-like restriction accepts it, and `metadata_raw_suppression` finds no synthetic `JsonConverterAttribute` alias target and therefore does not suppress the raw row. Added `DbReaderTests.GetFileDependencies_CSharpAttributeRawDoesNotLeakToBareNameClass_WhenSuffixTargetExists` (raw `[MyAudit]` row is suppressed when the alias resolves to `class MyAuditAttribute`, so `[MyAudit]` does NOT depend on an unrelated plain `class MyAudit` file), `DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotLeakToSameNameMethodOrProperty` (fully-qualified `[MyAuditAttribute]` metadata does NOT depend on a file that only defines a method / property named `MyAuditAttribute`), and `DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotFanOutWhenMultipleSameNameAttributeClasses` (two same-named attribute classes in different namespaces cause the metadata edge to be dropped as ambiguous). Both the `impact` ambiguity guard (`IsMetadataTargetUnambiguous`) and the `deps` ambiguity CTE (`target_ambiguity`) are additionally tightened to count class-like definitions at symbol-identity level instead of path level: `IsMetadataTargetUnambiguous` now uses `SELECT DISTINCT f.path, s.line, s.name` so two same-named class-like definitions in one source file (e.g. idiomatic C# `namespace A { class FooAttribute { } } namespace B { class FooAttribute { } }` in a single .cs file) register as ambiguous; `target_ambiguity` joins `target_files` back through `files` + `symbols` with `COUNT(*)` over class-like symbol rows so the same same-file duplicate case correctly drops the `[Foo]` metadata edge. Path-level counting previously treated these same-file duplicates as count=1 because `SELECT DISTINCT f.path` and `target_files` (grouped by `dst.path`) both collapse the two definitions into a single row, so `cdidx deps` and `cdidx impact` were confidently reporting an ambiguous short-name metadata reference as a definite dependency / blast-radius edge — contradicting the design intent that "ambiguity" includes duplicates in one file. Added `DbReaderTests.GetFileDependencies_CSharpAttributeAmbiguityCountsSameFileDuplicateClassDefinitions` and `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CountsSameFileDuplicateDefinitions` to pin the symbol-identity-level counting for both `deps` and `impact`. `IsMetadataTargetUnambiguous` is additionally tightened to wrap `--path` / `--exclude-path` parameters through `EscapeLikeQuery` and bind them as `%...%` patterns, matching the LIKE semantics used by the rest of the reader (search / references / deps etc.); without the wrap the in-scope class-like count would underflow to 1 even when two same-named attribute classes sit side-by-side in the requested CLI subtree such as `--path src/A/`, and the metadata bypass would falsely fire. Added `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CliPathPatternEscaping_SuppressesWhenInScopeIsAmbiguous` to pin this. `CSharpNoArgAttributeRegex` is additionally widened to accept an optional generic-argument list after the attribute name (`(?:\s*<[^\n]+?>)?`) so generic no-arg attribute forms such as `[MyAudit]`, `[assembly: MyAttr]`, the multi-line `[\n MyAttr\n]`, and nested-generic forms like `[MyAttr>]` / `[MyOther>>]` are indexed as `attribute` references instead of falling through both `CallRegex` (requires `(`) and the old no-arg regex (which ended at `]` / `,` / EOL and therefore rejected `` between the name and `]`). The inner generic segment now uses a non-greedy `[^\n]+?` so nested `>` tokens (e.g. the closing `>` of an inner `Dictionary`) no longer terminate the match prematurely — the old `[^>\n]+` excluded `>` and therefore broke on the first inner `>`, dropping the reference row for nested-generic attribute usages. Without the widening those sites produced zero reference rows, so `deps` / `impact` / `references` / `inspect` / `analyze_symbol` all missed the dependency edge to the real `FooAttribute` class file. Added `ReferenceExtractorTests.Extract_CsharpGenericNoArgAttribute_ClassifiedAsAttribute`, `ReferenceExtractorTests.Extract_CsharpNestedGenericNoArgAttribute_ClassifiedAsAttribute`, `DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_StillIndexedAndResolvesToAttributeClass`, `DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_AssemblyTarget_IsIndexed`, and `DbReaderTests.GetFileDependencies_CSharpNestedGenericNoArgAttribute_ResolvesToAttributeClass` to pin the end-to-end behavior. Both the `impact` ambiguity guard (`IsMetadataTargetUnambiguous`) and the `deps` ambiguity path (`target_files` / `target_ambiguity`) are additionally tightened to treat a C# class as a valid `[Attr]` metadata target ONLY when its signature inherits from an `Attribute`-suffixed base class (`signature LIKE '%: %Attribute%'`). Plain `class MyAuditAttribute { }` without `: Attribute` inheritance is not a valid `[MyAudit]` target at compile time, so counting it as an ambiguity candidate would under-report the `impact` blast radius and suppress a real `deps` metadata edge when a same-named plain class coexists with the real attribute class. `target_files` now exposes a new `has_metadata_target_kind` MAX column that applies the same C# signature filter, and `metadata_raw_suppression` / `target_ambiguity` / the metadata clause in `edges` are all switched to `has_metadata_target_kind`; Java / Kotlin / Scala targets remain unaffected because their metadata markers cannot be captured with a single portable signature LIKE, so they keep the broader class-like candidate set. Added `DbReaderTests.GetFileDependencies_CSharp_PlainClassWithAttributeSuffixName_DoesNotCountAsAmbiguity` to pin the `deps` path. Affected: `src/CodeIndex/Database/DbReader.cs`, `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. +- **`deps` C# attribute suffix alias now only matches class-like targets, and `impact` metadata bypass is suppressed when the class-like target name is ambiguous across namespaces (#293 follow-up)** — The earlier `#293` follow-up made `deps` UNION a C#-only `symbol_name || 'Attribute'` suffix alias into the logical-reference CTE so `[Foo]` attribute consumers could reach `FooAttribute` class files, and made `impact` (`GetFileDependencyHintsToResolvedType`) bypass the structured-evidence guard whenever any contributing reference was `attribute` / `annotation`. Both changes were too broad. On `deps` the alias joined purely by name, so `[Foo]` would also depend on any file that merely defined a function / property / variable named `FooAttribute` (e.g. `void MyAuditAttribute()` in an unrelated helper). On `impact` the metadata bypass did not check whether the target name was uniquely a class-like definition, so when the same unqualified name (e.g. `MyAuditAttribute`) existed in multiple namespaces the rename/remove blast radius was over-reported — attribute / annotation reference rows only keep the short name and cannot be disambiguated. `DbReader.GetFileDependencies` now tags synthetic alias rows with `is_attribute_alias = 1` (real rows are `0`), propagates the flag through `source_name_counts` via `MIN`, and restricts alias edges to class-like target kinds (`class` / `struct` / `interface`) in the `edges` CTE join. The `target_files` CTE is also switched from `DISTINCT` over `(path, lang, name, kind)` to `GROUP BY (path, lang, name)` with a `MAX(...)` `has_class_like_kind` flag, so files that define both a class and a same-named function (e.g. the C# constructor `Target()` inside `class Target`) no longer fan out into two target rows and inflate the `deps` reference count. `DbReader.GetFileDependencyHintsToResolvedType` now calls a new `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` helper that counts class-like definitions with the same unqualified name *within the active impact scope* (graph-supported languages, plus the caller's `--lang` / `--path` / `--exclude-path` / `--exclude-tests` filters); the metadata evidence-guard bypass is only applied when that in-scope count is ≤ 1. If the name is ambiguous within scope, metadata-only edges fall through to the normal `SourceFileHasStructuredTypeEvidence` check, so pure `[MyAudit]` consumers are dropped instead of being mis-attributed. Scoping matters: a same-named class in an unrelated language / path / test tree must not suppress the C# metadata bypass, because attribute reference rows are already language-qualified through the graph-supported reference join, and the README contract for `impact` explicitly promises "active `--lang` / `--path` / `--exclude-path` / `--exclude-tests` scope". The C# attribute deps test `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`, the unambiguous single-definition `impact` test, and the pre-existing graph-queries dedupe test all still pass. Added `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets` (ambiguous same-name function must not produce a spurious edge), `DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget` (two `MyAuditAttribute` classes within the same impact scope suppress the metadata heuristic hint), `DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous` (single class definition still surfaces the `[MyAudit]` consumer), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope` (same-named Java annotation must not suppress a `--lang csharp` query), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope` (out-of-scope `src/B/` same-named class must not suppress a `src/A/`-scoped query), and `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests` (test-tree same-named class must not suppress an `--exclude-tests` query). `ResolveImpactFallbackNames` is additionally tightened so the C# `Attribute`-suffix alias is only applied to the resolved definition's own name: previously the alias was applied to every same-file fallback name, so a nested `BarAttribute` inside the file that defines `FooAttribute` caused `impact FooAttribute` to falsely attribute `[Bar]` use sites to `FooAttribute`'s blast radius. Added `DbReaderTests.GetFileDependencyHints_CSharpAttributeSuffixAlias_DoesNotLeakToSameFileSiblings` to pin this. `GetFileDependencies` is additionally restructured so metadata rows (`attribute` / `annotation`) cannot leak onto bare-name call-graph targets: the `logical_references` CTE now carries an `is_metadata` flag, `source_name_counts` groups by `(source_file_id, source_path, source_lang, symbol_name, is_attribute_alias, is_metadata)` so metadata and non-metadata rows with the same symbol_name stay separate, the `edges` CTE restricts ALL metadata edges (not just synthetic aliases) to `tf.has_class_like_kind = 1`, and two new CTEs resolve the remaining false-positive shapes: `metadata_raw_suppression` drops the raw C# `[Foo]` row when the synthetic `FooAttribute` alias already resolves to a class-like target in the same source file (so `[Foo]` does not fan out to both `class FooAttribute` and a plain `class Foo` that shares the bare name), and `target_ambiguity` drops metadata edges when the same-named symbol resolves to multiple class-like definitions in scope (so ambiguous `[MyAudit]` usage is left to `impact` / `references` to disambiguate rather than being fanned out to every same-named attribute class). The existing `GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies` test (which pins `[JsonConverter(...)]` → `class JsonConverter : Attribute` for a target class whose name does NOT end in `Attribute`) still passes because only one class-like target exists in scope, so `target_ambiguity` keeps it, the `is_metadata` / class-like restriction accepts it, and `metadata_raw_suppression` finds no synthetic `JsonConverterAttribute` alias target and therefore does not suppress the raw row. Added `DbReaderTests.GetFileDependencies_CSharpAttributeRawDoesNotLeakToBareNameClass_WhenSuffixTargetExists` (raw `[MyAudit]` row is suppressed when the alias resolves to `class MyAuditAttribute`, so `[MyAudit]` does NOT depend on an unrelated plain `class MyAudit` file), `DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotLeakToSameNameMethodOrProperty` (fully-qualified `[MyAuditAttribute]` metadata does NOT depend on a file that only defines a method / property named `MyAuditAttribute`), and `DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotFanOutWhenMultipleSameNameAttributeClasses` (two same-named attribute classes in different namespaces cause the metadata edge to be dropped as ambiguous). Both the `impact` ambiguity guard (`IsMetadataTargetUnambiguous`) and the `deps` ambiguity CTE (`target_ambiguity`) are additionally tightened to count class-like definitions at symbol-identity level instead of path level: `IsMetadataTargetUnambiguous` now uses `SELECT DISTINCT f.path, s.line, s.name` so two same-named class-like definitions in one source file (e.g. idiomatic C# `namespace A { class FooAttribute { } } namespace B { class FooAttribute { } }` in a single .cs file) register as ambiguous; `target_ambiguity` joins `target_files` back through `files` + `symbols` with `COUNT(*)` over class-like symbol rows so the same same-file duplicate case correctly drops the `[Foo]` metadata edge. Path-level counting previously treated these same-file duplicates as count=1 because `SELECT DISTINCT f.path` and `target_files` (grouped by `dst.path`) both collapse the two definitions into a single row, so `cdidx deps` and `cdidx impact` were confidently reporting an ambiguous short-name metadata reference as a definite dependency / blast-radius edge — contradicting the design intent that "ambiguity" includes duplicates in one file. Added `DbReaderTests.GetFileDependencies_CSharpAttributeAmbiguityCountsSameFileDuplicateClassDefinitions` and `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CountsSameFileDuplicateDefinitions` to pin the symbol-identity-level counting for both `deps` and `impact`. `IsMetadataTargetUnambiguous` is additionally tightened to wrap `--path` / `--exclude-path` parameters through `EscapeLikeQuery` and bind them as `%...%` patterns, matching the LIKE semantics used by the rest of the reader (search / references / deps etc.); without the wrap the in-scope class-like count would underflow to 1 even when two same-named attribute classes sit side-by-side in the requested CLI subtree such as `--path src/A/`, and the metadata bypass would falsely fire. Added `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CliPathPatternEscaping_SuppressesWhenInScopeIsAmbiguous` to pin this. `CSharpNoArgAttributeRegex` is additionally widened to accept an optional generic-argument list after the attribute name (`(?:\s*<[^\n]+?>)?`) so generic no-arg attribute forms such as `[MyAudit]`, `[assembly: MyAttr]`, the multi-line `[\n MyAttr\n]`, and nested-generic forms like `[MyAttr>]` / `[MyOther>>]` are indexed as `attribute` references instead of falling through both `CallRegex` (requires `(`) and the old no-arg regex (which ended at `]` / `,` / EOL and therefore rejected `` between the name and `]`). The inner generic segment now uses a non-greedy `[^\n]+?` so nested `>` tokens (e.g. the closing `>` of an inner `Dictionary`) no longer terminate the match prematurely — the old `[^>\n]+` excluded `>` and therefore broke on the first inner `>`, dropping the reference row for nested-generic attribute usages. Without the widening those sites produced zero reference rows, so `deps` / `impact` / `references` / `inspect` / `analyze_symbol` all missed the dependency edge to the real `FooAttribute` class file. Added `ReferenceExtractorTests.Extract_CsharpGenericNoArgAttribute_ClassifiedAsAttribute`, `ReferenceExtractorTests.Extract_CsharpNestedGenericNoArgAttribute_ClassifiedAsAttribute`, `DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_StillIndexedAndResolvesToAttributeClass`, `DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_AssemblyTarget_IsIndexed`, and `DbReaderTests.GetFileDependencies_CSharpNestedGenericNoArgAttribute_ResolvesToAttributeClass` to pin the end-to-end behavior. Both the `impact` ambiguity guard (`IsMetadataTargetUnambiguous`) and the `deps` ambiguity path (`target_files` / `target_ambiguity`) are additionally tightened to treat a C# class as a valid `[Attr]` metadata target ONLY when its signature inherits from an `Attribute`-suffixed base class (`signature LIKE '%: %Attribute%'`). Plain `class MyAuditAttribute { }` without `: Attribute` inheritance is not a valid `[MyAudit]` target at compile time, so counting it as an ambiguity candidate would under-report the `impact` blast radius and suppress a real `deps` metadata edge when a same-named plain class coexists with the real attribute class. `target_files` now exposes a new `has_metadata_target_kind` MAX column that applies the same C# signature filter, and `metadata_raw_suppression` / `target_ambiguity` / the metadata clause in `edges` are all switched to `has_metadata_target_kind`; Java / Kotlin / Scala targets remain unaffected because their metadata markers cannot be captured with a single portable signature LIKE, so they keep the broader class-like candidate set. Added `DbReaderTests.GetFileDependencies_CSharp_PlainClassWithAttributeSuffixName_DoesNotCountAsAmbiguity` to pin the `deps` path. Round-17 follow-up loosens the C# metadata-target signature filter from `'%: %Attribute%'` (requires the immediate base class to end in `Attribute`) to `'%: %'` (any inheritance clause qualifies) so indirect inheritance like `class MyAuditAttribute : BaseAudit` where `BaseAudit : Attribute` is also recognized as a valid `[MyAudit]` target; transitive base-type resolution is not available at SQL time, so the inheritance-clause check is the best portable approximation — non-attribute derivations are over-accepted, but an invalid `[MyFoo]` against a non-attribute class would fail to compile and therefore does not appear as a real reference. The shared `BuildCSharpMetadataTargetFilter` helper additionally degrades to `1 = 1` (the pre-metadata-eligibility behavior) when the `symbols.signature` column is missing on legacy / read-only DBs where `TryMigrateForRead` could not add it, so `deps` / `impact` queries return results instead of crashing with `no such column: s.signature`. Added `DbReaderTests.GetFileDependencies_CSharp_IndirectAttributeInheritance_ResolvesAsMetadataTarget` to pin the indirect-inheritance path. Affected: `src/CodeIndex/Database/DbReader.cs`, `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. - **MCP `callers` / `callees` `kind` schema, CLI help, and README `--kind` option row no longer advertise metadata kinds as valid filter values (#293 follow-up)** — The `callers` / `callees` handlers reject `kind: "attribute"` / `kind: "annotation"` with a usage error (CLI) / `isError` tool response (MCP), but the MCP tool schema's `kind` property description still listed all five kinds as if they were valid filter values (`"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"`), `cdidx --help` similarly lumped `callers` / `callees` together with `references` for the `--kind` help row, and the README `--kind` option table (EN line 436 / JP line 1307) still directed readers to pass `--kind attribute` or `--kind annotation` on `callers` / `callees`. Schema and handler now agree: the `callers` / `callees` `kind` description now reads `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."`, the CLI `--kind` help row splits `references` (accepts all five kinds) from `callers` / `callees` (call-graph kinds only, metadata rejected, redirects to `references`), and the README `--kind` row (both English and Japanese) now explicitly states that `callers` / `callees` reject `--kind attribute|annotation` with a usage error and points to `references --kind attribute|annotation` as the correct metadata-enumeration path. The MCP `references` tool description is unchanged because it still accepts every indexed kind. Added `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` to pin the corrected schema wording. Affected: `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `src/CodeIndex/Cli/ConsoleUi.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `README.md`. - **`deps`, `impact`, `references`, `inspect`, and `analyze_symbol` retain metadata references as compile-time dependency edges and canonicalize the C# attribute-suffix convention (#293 follow-up)** — `DbReader.GetFileDependencies` (backing `cdidx deps`) and `DbReader.GetFileDependencyHintsToResolvedType` (backing `cdidx impact` / MCP `impact_analysis` heuristic file-hint fallback) keep metadata references in their logical reference CTE. Renaming or removing an attribute class still breaks annotated sites such as `[JsonConverterAttribute]` at compile time, so file-level blast-radius analysis must surface those edges as real dependencies — now labeled `attribute` / `annotation` in `logical_reference_kind` (previously these same sites leaked in as phantom `call` rows before classification was corrected). `GetFileDependencies` additionally canonicalizes the C# attribute naming convention: idiomatic `[Foo]` usage is stored with `symbol_name = "Foo"` but the defining class is `FooAttribute`, so the logical-reference CTE now UNION-ALLs a suffix-aliased copy of every C# `attribute`-kind row with `'Attribute'` appended (only when the original symbol_name does not already end in `Attribute`) so the target-file join reaches the attribute class file. `GetFileDependencyHintsToResolvedType` also aggregates a per-candidate `has_metadata_ref` flag and bypasses the `SourceFileHasStructuredTypeEvidence` guard for edges whose contributing references are purely metadata, so pure-attribute consumer files such as `[MyAudit] class Svc` now surface through `impact MyAuditAttribute` instead of being filtered out. `ResolveImpactFallbackNames` symmetrically adds suffix-stripped aliases for C# class names ending in `Attribute`, so the BFS-less file-hint path can find `[Foo]` sites whose source symbol_name does not match the defining class name. The same canonicalization is propagated to the symbol-level reference reader: `DbReader.SearchReferences`, `CountSearchReferences`, and `CountSearchReferencesTotal` (backing `references`, `inspect`, MCP `analyze_symbol`) now compute a C#-only `queryAttributeAlias` (defined in the new `ComputeCSharpAttributeSuffixAlias` helper) and OR it into both exact (folded and NOCASE) and substring WHERE clauses, so `references MyAuditAttribute` (and `inspect MyAuditAttribute` / `analyze_symbol MyAuditAttribute`) surface the idiomatic `[MyAudit]` call site consistently with `deps` / `impact`. Substring mode uses an exact-OR for the alias (not a second LIKE) so user queries like `MyAuditAttribute` do not over-match unrelated names such as `FooAuditLog`. The alias is C# only, so Java / Kotlin / Scala / TypeScript annotation sites remain unaffected. `callers` / `callees` intentionally skip the alias because they filter to the shared call-graph kinds (`call`, `instantiate`, `subscribe`) and reject `--kind attribute|annotation` at the CLI / MCP boundary — those commands model the dynamic call graph, not the dependency graph. The alias branch is additionally scoped so it cannot produce false positives: `ComputeCSharpAttributeSuffixAlias` returns `null` when `referenceKind` is a non-`attribute` call-graph kind (so `references FooAttribute --kind call` stays strict and does not match `Foo()`), and the SQL alias disjunct in all three reader methods is clamped to `f.lang = 'csharp' AND r.reference_kind = 'attribute'` so an unscoped `references FooAttribute` (no `--lang` / no `--kind`) still cannot bleed into Java annotations or C# call rows. Suffix detection is also case-insensitive (`OrdinalIgnoreCase`) so `references myauditattribute` and `inspect MyAuditATTRIBUTE` still produce the `MyAudit` alias, consistent with the NOCASE / folded contract of the surrounding exact / substring query paths. Added regression coverage `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies` (runtime + metadata edges both surface), `DbReaderTests.GetFileDependencies_MatchesCSharpAttributeSuffixConvention` (`[MyAudit]` → `MyAuditAttribute` canonicalization through `deps`), `DbReaderTests.SearchReferences_MatchesCSharpAttributeSuffixConvention_Substring` / `_Exact` / `_CSharpAttributeSuffixAliasDoesNotBleedToOtherLanguages` (same canonicalization through `references` / `inspect` / `analyze_symbol`, plus C#-only scope guard), `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_NotAppliedToCallKind` (alias must not fire under `--kind call`), `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_UnscopedLangStillLimitsToCSharpAttributeRows` (unscoped query must not match Java annotations or C# call rows), and `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_CaseInsensitiveQuery` (lowercase / mixed-case queries still produce the alias). Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. - **`callers` / `callees` now reject `--kind attribute|annotation` at the CLI and MCP boundary, and Swift / Gradle `@` metadata is reclassified as `annotation` (#293 follow-up)** — Metadata rows (`attribute` / `annotation`) are attributed to their enclosing body-range symbol rather than the annotated target, so `callers Obsolete --kind attribute` would silently return `[Obsolete] void M()` under the enclosing class (not `M`), and file-level targets such as `[assembly: CLSCompliant]` or Kotlin `@file:JvmName` would drop entirely because `container_name` is `NULL`. `QueryCommandRunner.RunCallers` / `RunCallees` now reject `--kind attribute` and `--kind annotation` with `CommandExitCodes.UsageError`, and the MCP `callers` / `callees` tools return an `isError` tool response; both paths direct users to `references --kind attribute|annotation`, which IS a correct metadata enumeration path. Separately, `AnnotationLanguages` now includes `swift` and `gradle`, so Swift `@available(iOS 13.0, *)` / `@objc` / `@MainActor` and Gradle/Groovy `@CompileStatic` / `@TaskAction` are recorded as `annotation` instead of `call` (previously the `@Name(args)` forms leaked into `callers` / `callees` / `hotspots` / `impact` as phantom call edges, and the no-arg `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` forms dropped from the index entirely because no-arg annotation emission was gated to languages in this set). Added `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError` (theory, 4 cases), `McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError` (theory, 4 cases), `ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`, and `ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation`. Affected: `src/CodeIndex/Cli/QueryCommandRunner.cs`, `src/CodeIndex/Mcp/McpToolHandlers.cs`, `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. @@ -686,7 +686,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **DEVELOPER_GUIDE が SQLite の位置付けとリリース番号の例示を明確化** — cdidx が「設定ゼロ・単一ファイル・本番依存1個」の CLI であり、その唯一の本番依存が `Microsoft.Data.Sqlite` であることを明記した。あわせて、リリース手順のチェックリスト中に出てくる `1.9.0` は固定値ではなく例示であり、実際のリリース番号へ読み替える前提だと明示した。対象: `DEVELOPER_GUIDE.md`。 #### 修正 -- **`deps` の C# attribute サフィックス別名を class-like ターゲットに限定し、`impact` の metadata bypass も class-like ターゲット名が名前空間をまたいで曖昧な場合は抑止するよう修正 (#293 追加対応)** — 直前の #293 追加対応では、`deps` の logical-reference CTE に C# 限定で `symbol_name || 'Attribute'` のサフィックス別名を UNION することで `[Foo]` の attribute consumer が `FooAttribute` クラスファイルへ到達するようにし、`impact`(`GetFileDependencyHintsToResolvedType`)も寄与参照に `attribute` / `annotation` が含まれていれば structured-evidence guard をバイパスするようにしていたが、どちらも広すぎた。`deps` 側では別名が純粋に名前だけで join していたため、`[Foo]` が同名の関数・プロパティ・変数(例: 無関係なヘルパーの `void MyAuditAttribute()`)を定義するファイルにも依存を張ってしまっていた。`impact` 側の metadata bypass も、対象名が一意に class-like 定義を指すかを確認していなかったため、`MyAuditAttribute` のような短い名前が複数の namespace に存在すると rename / remove の blast-radius が過剰報告されていた(`attribute` / `annotation` 参照行は短い名前しか保持できず曖昧性解消ができない)。`DbReader.GetFileDependencies` は合成された別名行に `is_attribute_alias = 1`(本物の行は `0`)をタグ付けし、`source_name_counts` では `MIN` でフラグを伝播し、`edges` CTE の join で別名 edge をターゲット側 kind が `class` / `struct` / `interface` のときだけに制限するようにした。`target_files` CTE も `DISTINCT (path, lang, name, kind)` から `GROUP BY (path, lang, name)` + `MAX(...)` の `has_class_like_kind` フラグに切り替えたため、class と同名関数(例: C# コンストラクタ `Target()` と `class Target`)を同じファイルで定義しているケースでもターゲット行が 2 本に増えず `deps` の参照数を水増ししない。`DbReader.GetFileDependencyHintsToResolvedType` は新ヘルパー `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` を呼び、*active な impact スコープ内* で同名の class-like 定義数を数える(graph 対応言語に加えて、呼び出し側の `--lang` / `--path` / `--exclude-path` / `--exclude-tests` を同じフィルタで適用)。in-scope でその数が 1 以下のときに限り metadata evidence-guard bypass を適用する。スコープ内で曖昧なときは metadata 単独 edge も通常の `SourceFileHasStructuredTypeEvidence` に戻り、純 `[MyAudit]` consumer は誤帰属せずに落ちる。スコープ尊重が重要: 別言語 / 別 path / test ツリーにある同名クラスが C# の metadata bypass を潰してはならない(参照側が既に graph 対応 join で言語修飾されているため、曖昧性は scope 内で判定すべき)。README の `impact` 契約も「active な `--lang` / `--path` / `--exclude-path` / `--exclude-tests` スコープ」と明記している。既存の C# attribute deps テスト `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`、曖昧ではない単一定義の `impact` テスト、および既存の graph-queries dedupe テストは全てグリーンのまま。回帰テストとして `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets`(同名関数があっても虚偽 edge が出ないこと)、`DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget`(impact スコープ内で `MyAuditAttribute` が複数ある場合に metadata heuristic hint を抑止)、`DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous`(単一クラス定義のみのときは従来どおり `[MyAudit]` consumer を浮かせる)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope`(別言語の同名 annotation は `--lang csharp` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope`(スコープ外 `src/B/` の同名クラスは `src/A/` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests`(test ツリーの同名クラスは `--exclude-tests` クエリを潰さない)を追加。`ResolveImpactFallbackNames` も併せて引き締め、C# の `Attribute` サフィックス別名は *解決済み定義自身* の名前にのみ適用するようにした: 以前は同一ファイル内の fallback 名全てに別名を付けていたため、`FooAttribute` を定義するファイルに nested で `BarAttribute` が存在するだけで `impact FooAttribute` が `[Bar]` 利用サイトを `FooAttribute` の影響範囲として誤報告していた。`DbReaderTests.GetFileDependencyHints_CSharpAttributeSuffixAlias_DoesNotLeakToSameFileSiblings` を追加して固定。さらに `GetFileDependencies` を再構成し、metadata 行(`attribute` / `annotation`)が bare-name の call-graph ターゲットに漏れないようにした: `logical_references` CTE に `is_metadata` フラグを付与し、`source_name_counts` の GROUP BY を `(source_file_id, source_path, source_lang, symbol_name, is_attribute_alias, is_metadata)` に拡張して metadata と非 metadata で同じ symbol_name の行を別グループ化する。`edges` CTE は全 metadata edge(合成別名だけでなく実体行も)に `tf.has_class_like_kind = 1` を要求し、さらに 2 つの CTE で残る誤検知を潰す: `metadata_raw_suppression` は同じ source file で合成別名 `FooAttribute` が class-like ターゲットに解決できているとき、生の C# `[Foo]` 行を落とす(`[Foo]` が `class FooAttribute` と無関係な同名 `class Foo` に fan-out しない)。`target_ambiguity` はスコープ内で同名 class-like ターゲットが複数あるとき metadata edge を落とす(曖昧な `[MyAudit]` は deps で fan-out させず、`impact` / `references` に判断を委ねる)。既存の `GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies`(`[JsonConverter(...)]` → `class JsonConverter : Attribute` をターゲット名に `Attribute` サフィックスがなくても固定)は、class-like ターゲットが 1 つしかないので `target_ambiguity` で残り、`is_metadata` / class-like 制限も受理し、合成 `JsonConverterAttribute` 別名は class-like に解決できないので `metadata_raw_suppression` も発火せず、そのまま通る。回帰テストとして `DbReaderTests.GetFileDependencies_CSharpAttributeRawDoesNotLeakToBareNameClass_WhenSuffixTargetExists`(別名が `class MyAuditAttribute` に解決できたとき生 `[MyAudit]` 行は抑止され、無関係な plain `class MyAudit` ファイルに依存しない)、`DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotLeakToSameNameMethodOrProperty`(完全形 `[MyAuditAttribute]` は同名メソッド / プロパティしか持たないファイルに依存しない)、`DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotFanOutWhenMultipleSameNameAttributeClasses`(別名前空間に同名 attribute クラスが 2 つあると metadata edge はあいまいとして落とす)を追加。加えて `impact` の曖昧性ガード (`IsMetadataTargetUnambiguous`) と `deps` の曖昧性 CTE (`target_ambiguity`) も、path 単位ではなく symbol identity 単位で class-like 定義を数えるように引き締めた: `IsMetadataTargetUnambiguous` は `SELECT DISTINCT f.path, s.line, s.name` に変更し、C# でよくある 1 つの .cs ファイル内に別名前空間で同名 class-like が 2 つ定義されている形 (`namespace A { class FooAttribute { } } namespace B { class FooAttribute { } }`) でも ambiguity を 2 として検出する。`target_ambiguity` は `target_files` を `files` + `symbols` に JOIN し直して `COUNT(*)` を class-like symbol 行に対して取るため、同じ同名ファイル内重複ケースでも `[Foo]` metadata edge を正しく落とせる。path 単位カウントでは `SELECT DISTINCT f.path` も `target_files`(`dst.path` で GROUP BY)も同一ファイル内の 2 定義を 1 行に潰してしまうため、従来は count=1 として曖昧性を取りこぼし、`cdidx deps` と `cdidx impact` が同名 short-name metadata 参照を確定的な依存 / blast-radius edge として出していた。これは曖昧性に "duplicates in one file" を含める前提の設計契約に反していた。`deps` と `impact` 両方で symbol-identity 単位カウントを固定する `DbReaderTests.GetFileDependencies_CSharpAttributeAmbiguityCountsSameFileDuplicateClassDefinitions` と `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CountsSameFileDuplicateDefinitions` を追加。さらに `IsMetadataTargetUnambiguous` の `--path` / `--exclude-path` パラメータを、他の reader 経路(search / references / deps 等)と同じ LIKE セマンティクスに揃えるため `EscapeLikeQuery` でエスケープしたうえで `%...%` で包んでバインドするようにした。生値のままだと `--path src/A/` のような CLI 形では LIKE が一致せず、リクエストされたサブツリーに同名 attribute クラスが 2 件並んでいても in-scope class-like カウントが 1 に過小化され、本来抑止すべき metadata bypass が誤発火してしまっていた。`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CliPathPatternEscaping_SuppressesWhenInScopeIsAmbiguous` を追加して固定する。あわせて `CSharpNoArgAttributeRegex` を拡張し、属性名の直後に任意のジェネリック引数リスト `(?:\s*<[^\n]+?>)?` を許容するようにしたため、`[MyAudit]`・`[assembly: MyAttr]`・複数行の `[\n MyAttr\n]`・ネストジェネリックの `[MyAttr>]` や `[MyOther>>]` のような引数なしジェネリック属性も `attribute` 参照としてインデックスされるようになった。内側ジェネリックセグメントは非貪欲な `[^\n]+?` に変更し、内側の `>`(例: `Dictionary` を閉じる `>`)で match が打ち切られないようにした — 従来の `[^>\n]+` は `>` を除外していたため最初の内側 `>` で崩れ、ネストジェネリック属性の参照行が落ちていた。従来は `CallRegex`(`(` 必須)でも旧 no-arg regex(`]` / `,` / EOL で終端しなければならず、名前と `]` の間に `` があると拒否)でも拾えず、参照行が 0 件となり、`deps` / `impact` / `references` / `inspect` / `analyze_symbol` が実属性クラス `FooAttribute` への依存エッジを全て取りこぼしていた。`ReferenceExtractorTests.Extract_CsharpGenericNoArgAttribute_ClassifiedAsAttribute`、`ReferenceExtractorTests.Extract_CsharpNestedGenericNoArgAttribute_ClassifiedAsAttribute`、`DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_StillIndexedAndResolvesToAttributeClass`、`DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_AssemblyTarget_IsIndexed`、`DbReaderTests.GetFileDependencies_CSharpNestedGenericNoArgAttribute_ResolvesToAttributeClass` を追加して end-to-end の振る舞いを固定する。加えて、`impact` の曖昧性ガード (`IsMetadataTargetUnambiguous`) と `deps` の曖昧性判定 (`target_files` / `target_ambiguity`) は、C# クラスが有効な `[Attr]` metadata target になるのは「signature が `Attribute` サフィックス付き基底を継承している」場合 (`signature LIKE '%: %Attribute%'`) に限定するよう引き締めた。`: Attribute` 継承を書いていない plain `class MyAuditAttribute { }` はコンパイル時に `[MyAudit]` の target にならないため、曖昧性候補として数えると `impact` の影響範囲が過少報告され、`deps` 側でも同名 plain クラスが共存する scene で実 attribute クラスへの metadata edge が落ちてしまう。`target_files` に新しい `has_metadata_target_kind` MAX 列を追加して同じ C# signature フィルタを適用し、`metadata_raw_suppression` / `target_ambiguity` / `edges` の metadata 節はすべて `has_metadata_target_kind` に切り替えた。Java / Kotlin / Scala は portable な signature LIKE で metadata marker を表現しづらいため従来通り class-like 全体を候補に残す。`deps` 側の挙動を固定する `DbReaderTests.GetFileDependencies_CSharp_PlainClassWithAttributeSuffixName_DoesNotCountAsAmbiguity` を追加。対象: `src/CodeIndex/Database/DbReader.cs`、`src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 +- **`deps` の C# attribute サフィックス別名を class-like ターゲットに限定し、`impact` の metadata bypass も class-like ターゲット名が名前空間をまたいで曖昧な場合は抑止するよう修正 (#293 追加対応)** — 直前の #293 追加対応では、`deps` の logical-reference CTE に C# 限定で `symbol_name || 'Attribute'` のサフィックス別名を UNION することで `[Foo]` の attribute consumer が `FooAttribute` クラスファイルへ到達するようにし、`impact`(`GetFileDependencyHintsToResolvedType`)も寄与参照に `attribute` / `annotation` が含まれていれば structured-evidence guard をバイパスするようにしていたが、どちらも広すぎた。`deps` 側では別名が純粋に名前だけで join していたため、`[Foo]` が同名の関数・プロパティ・変数(例: 無関係なヘルパーの `void MyAuditAttribute()`)を定義するファイルにも依存を張ってしまっていた。`impact` 側の metadata bypass も、対象名が一意に class-like 定義を指すかを確認していなかったため、`MyAuditAttribute` のような短い名前が複数の namespace に存在すると rename / remove の blast-radius が過剰報告されていた(`attribute` / `annotation` 参照行は短い名前しか保持できず曖昧性解消ができない)。`DbReader.GetFileDependencies` は合成された別名行に `is_attribute_alias = 1`(本物の行は `0`)をタグ付けし、`source_name_counts` では `MIN` でフラグを伝播し、`edges` CTE の join で別名 edge をターゲット側 kind が `class` / `struct` / `interface` のときだけに制限するようにした。`target_files` CTE も `DISTINCT (path, lang, name, kind)` から `GROUP BY (path, lang, name)` + `MAX(...)` の `has_class_like_kind` フラグに切り替えたため、class と同名関数(例: C# コンストラクタ `Target()` と `class Target`)を同じファイルで定義しているケースでもターゲット行が 2 本に増えず `deps` の参照数を水増ししない。`DbReader.GetFileDependencyHintsToResolvedType` は新ヘルパー `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` を呼び、*active な impact スコープ内* で同名の class-like 定義数を数える(graph 対応言語に加えて、呼び出し側の `--lang` / `--path` / `--exclude-path` / `--exclude-tests` を同じフィルタで適用)。in-scope でその数が 1 以下のときに限り metadata evidence-guard bypass を適用する。スコープ内で曖昧なときは metadata 単独 edge も通常の `SourceFileHasStructuredTypeEvidence` に戻り、純 `[MyAudit]` consumer は誤帰属せずに落ちる。スコープ尊重が重要: 別言語 / 別 path / test ツリーにある同名クラスが C# の metadata bypass を潰してはならない(参照側が既に graph 対応 join で言語修飾されているため、曖昧性は scope 内で判定すべき)。README の `impact` 契約も「active な `--lang` / `--path` / `--exclude-path` / `--exclude-tests` スコープ」と明記している。既存の C# attribute deps テスト `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`、曖昧ではない単一定義の `impact` テスト、および既存の graph-queries dedupe テストは全てグリーンのまま。回帰テストとして `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets`(同名関数があっても虚偽 edge が出ないこと)、`DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget`(impact スコープ内で `MyAuditAttribute` が複数ある場合に metadata heuristic hint を抑止)、`DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous`(単一クラス定義のみのときは従来どおり `[MyAudit]` consumer を浮かせる)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope`(別言語の同名 annotation は `--lang csharp` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope`(スコープ外 `src/B/` の同名クラスは `src/A/` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests`(test ツリーの同名クラスは `--exclude-tests` クエリを潰さない)を追加。`ResolveImpactFallbackNames` も併せて引き締め、C# の `Attribute` サフィックス別名は *解決済み定義自身* の名前にのみ適用するようにした: 以前は同一ファイル内の fallback 名全てに別名を付けていたため、`FooAttribute` を定義するファイルに nested で `BarAttribute` が存在するだけで `impact FooAttribute` が `[Bar]` 利用サイトを `FooAttribute` の影響範囲として誤報告していた。`DbReaderTests.GetFileDependencyHints_CSharpAttributeSuffixAlias_DoesNotLeakToSameFileSiblings` を追加して固定。さらに `GetFileDependencies` を再構成し、metadata 行(`attribute` / `annotation`)が bare-name の call-graph ターゲットに漏れないようにした: `logical_references` CTE に `is_metadata` フラグを付与し、`source_name_counts` の GROUP BY を `(source_file_id, source_path, source_lang, symbol_name, is_attribute_alias, is_metadata)` に拡張して metadata と非 metadata で同じ symbol_name の行を別グループ化する。`edges` CTE は全 metadata edge(合成別名だけでなく実体行も)に `tf.has_class_like_kind = 1` を要求し、さらに 2 つの CTE で残る誤検知を潰す: `metadata_raw_suppression` は同じ source file で合成別名 `FooAttribute` が class-like ターゲットに解決できているとき、生の C# `[Foo]` 行を落とす(`[Foo]` が `class FooAttribute` と無関係な同名 `class Foo` に fan-out しない)。`target_ambiguity` はスコープ内で同名 class-like ターゲットが複数あるとき metadata edge を落とす(曖昧な `[MyAudit]` は deps で fan-out させず、`impact` / `references` に判断を委ねる)。既存の `GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies`(`[JsonConverter(...)]` → `class JsonConverter : Attribute` をターゲット名に `Attribute` サフィックスがなくても固定)は、class-like ターゲットが 1 つしかないので `target_ambiguity` で残り、`is_metadata` / class-like 制限も受理し、合成 `JsonConverterAttribute` 別名は class-like に解決できないので `metadata_raw_suppression` も発火せず、そのまま通る。回帰テストとして `DbReaderTests.GetFileDependencies_CSharpAttributeRawDoesNotLeakToBareNameClass_WhenSuffixTargetExists`(別名が `class MyAuditAttribute` に解決できたとき生 `[MyAudit]` 行は抑止され、無関係な plain `class MyAudit` ファイルに依存しない)、`DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotLeakToSameNameMethodOrProperty`(完全形 `[MyAuditAttribute]` は同名メソッド / プロパティしか持たないファイルに依存しない)、`DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotFanOutWhenMultipleSameNameAttributeClasses`(別名前空間に同名 attribute クラスが 2 つあると metadata edge はあいまいとして落とす)を追加。加えて `impact` の曖昧性ガード (`IsMetadataTargetUnambiguous`) と `deps` の曖昧性 CTE (`target_ambiguity`) も、path 単位ではなく symbol identity 単位で class-like 定義を数えるように引き締めた: `IsMetadataTargetUnambiguous` は `SELECT DISTINCT f.path, s.line, s.name` に変更し、C# でよくある 1 つの .cs ファイル内に別名前空間で同名 class-like が 2 つ定義されている形 (`namespace A { class FooAttribute { } } namespace B { class FooAttribute { } }`) でも ambiguity を 2 として検出する。`target_ambiguity` は `target_files` を `files` + `symbols` に JOIN し直して `COUNT(*)` を class-like symbol 行に対して取るため、同じ同名ファイル内重複ケースでも `[Foo]` metadata edge を正しく落とせる。path 単位カウントでは `SELECT DISTINCT f.path` も `target_files`(`dst.path` で GROUP BY)も同一ファイル内の 2 定義を 1 行に潰してしまうため、従来は count=1 として曖昧性を取りこぼし、`cdidx deps` と `cdidx impact` が同名 short-name metadata 参照を確定的な依存 / blast-radius edge として出していた。これは曖昧性に "duplicates in one file" を含める前提の設計契約に反していた。`deps` と `impact` 両方で symbol-identity 単位カウントを固定する `DbReaderTests.GetFileDependencies_CSharpAttributeAmbiguityCountsSameFileDuplicateClassDefinitions` と `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CountsSameFileDuplicateDefinitions` を追加。さらに `IsMetadataTargetUnambiguous` の `--path` / `--exclude-path` パラメータを、他の reader 経路(search / references / deps 等)と同じ LIKE セマンティクスに揃えるため `EscapeLikeQuery` でエスケープしたうえで `%...%` で包んでバインドするようにした。生値のままだと `--path src/A/` のような CLI 形では LIKE が一致せず、リクエストされたサブツリーに同名 attribute クラスが 2 件並んでいても in-scope class-like カウントが 1 に過小化され、本来抑止すべき metadata bypass が誤発火してしまっていた。`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CliPathPatternEscaping_SuppressesWhenInScopeIsAmbiguous` を追加して固定する。あわせて `CSharpNoArgAttributeRegex` を拡張し、属性名の直後に任意のジェネリック引数リスト `(?:\s*<[^\n]+?>)?` を許容するようにしたため、`[MyAudit]`・`[assembly: MyAttr]`・複数行の `[\n MyAttr\n]`・ネストジェネリックの `[MyAttr>]` や `[MyOther>>]` のような引数なしジェネリック属性も `attribute` 参照としてインデックスされるようになった。内側ジェネリックセグメントは非貪欲な `[^\n]+?` に変更し、内側の `>`(例: `Dictionary` を閉じる `>`)で match が打ち切られないようにした — 従来の `[^>\n]+` は `>` を除外していたため最初の内側 `>` で崩れ、ネストジェネリック属性の参照行が落ちていた。従来は `CallRegex`(`(` 必須)でも旧 no-arg regex(`]` / `,` / EOL で終端しなければならず、名前と `]` の間に `` があると拒否)でも拾えず、参照行が 0 件となり、`deps` / `impact` / `references` / `inspect` / `analyze_symbol` が実属性クラス `FooAttribute` への依存エッジを全て取りこぼしていた。`ReferenceExtractorTests.Extract_CsharpGenericNoArgAttribute_ClassifiedAsAttribute`、`ReferenceExtractorTests.Extract_CsharpNestedGenericNoArgAttribute_ClassifiedAsAttribute`、`DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_StillIndexedAndResolvesToAttributeClass`、`DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_AssemblyTarget_IsIndexed`、`DbReaderTests.GetFileDependencies_CSharpNestedGenericNoArgAttribute_ResolvesToAttributeClass` を追加して end-to-end の振る舞いを固定する。加えて、`impact` の曖昧性ガード (`IsMetadataTargetUnambiguous`) と `deps` の曖昧性判定 (`target_files` / `target_ambiguity`) は、C# クラスが有効な `[Attr]` metadata target になるのは「signature が `Attribute` サフィックス付き基底を継承している」場合 (`signature LIKE '%: %Attribute%'`) に限定するよう引き締めた。`: Attribute` 継承を書いていない plain `class MyAuditAttribute { }` はコンパイル時に `[MyAudit]` の target にならないため、曖昧性候補として数えると `impact` の影響範囲が過少報告され、`deps` 側でも同名 plain クラスが共存する scene で実 attribute クラスへの metadata edge が落ちてしまう。`target_files` に新しい `has_metadata_target_kind` MAX 列を追加して同じ C# signature フィルタを適用し、`metadata_raw_suppression` / `target_ambiguity` / `edges` の metadata 節はすべて `has_metadata_target_kind` に切り替えた。Java / Kotlin / Scala は portable な signature LIKE で metadata marker を表現しづらいため従来通り class-like 全体を候補に残す。`deps` 側の挙動を固定する `DbReaderTests.GetFileDependencies_CSharp_PlainClassWithAttributeSuffixName_DoesNotCountAsAmbiguity` を追加。Round-17 追加対応では、C# metadata target の signature フィルタを `'%: %Attribute%'`(直接基底が `Attribute` で終わることを要求)から `'%: %'`(何らかの継承節を持てば候補)に緩和した。`class MyAuditAttribute : BaseAudit` で `BaseAudit : Attribute` のような間接継承でも `[MyAudit]` の有効な target として認識される。SQL 時点で基底型を遡ることはできないため、継承節チェックは portable な近似として採用する — 非 attribute 由来のクラスは過剰に受理するが、無効な `[MyFoo]` をコンパイルすることはできず実参照にはならないので実害はない。共有ヘルパー `BuildCSharpMetadataTargetFilter` はさらに、`TryMigrateForRead` が追加できなかった legacy / read-only DB で `symbols.signature` 列が欠落しているときは `1 = 1`(metadata-eligibility 導入前の挙動)へ degrade するので、`deps` / `impact` クエリは `no such column: s.signature` でクラッシュせず結果を返す。間接継承パスを固定する `DbReaderTests.GetFileDependencies_CSharp_IndirectAttributeInheritance_ResolvesAsMetadataTarget` を追加。対象: `src/CodeIndex/Database/DbReader.cs`、`src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 - **MCP の `callers` / `callees` の `kind` schema・CLI help・README の `--kind` オプション行が metadata kinds を有効フィルタ値として広告しないよう修正 (#293 追加対応)** — `callers` / `callees` の handler は `kind: "attribute"` / `kind: "annotation"` を usage error(CLI)/ `isError` ツールレスポンス(MCP)で拒否するにもかかわらず、MCP ツール schema の `kind` プロパティ説明は 5 種すべてを列挙した `"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"` のままで、`cdidx --help` の `--kind` 行も `callers` / `callees` を `references` と同じ行にまとめていた。さらに README の `--kind` オプション表(英語 436 行目 / 日本語 1307 行目)も、`callers` / `callees` で `--kind attribute` / `--kind annotation` を渡せるかのように案内していた。schema と handler の契約を一致させ、`callers` / `callees` の `kind` description を `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."` に修正し、CLI の `--kind` help 行も `references`(5 種すべて受理)と `callers` / `callees`(call-graph 種別のみ、metadata kinds は拒否して `references` へ誘導)を分けるようにし、README の `--kind` 行(英語・日本語の両方)も `callers` / `callees` が `--kind attribute|annotation` を usage error で拒否すること、metadata 列挙の正規経路が `references --kind attribute|annotation` であることを明示する文言に直した。MCP の `references` ツール説明は全 reference kind を受け付ける契約のままなので変更なし。修正後の schema 文言を固定する `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` を追加。対象: `src/CodeIndex/Mcp/McpToolDefinitions.cs`、`src/CodeIndex/Cli/ConsoleUi.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`、`README.md`。 - **`deps`・`impact`・`references`・`inspect`・`analyze_symbol` が metadata 参照を compile-time 依存として保持し、C# の attribute サフィックス命名規約を正規化 (#293 追加対応)** — `cdidx deps` の実装である `DbReader.GetFileDependencies` と、`cdidx impact` / MCP `impact_analysis` の heuristic file-hint フォールバックである `DbReader.GetFileDependencyHintsToResolvedType` は、logical reference CTE に metadata 参照も含める。attribute クラスを rename / 削除すれば `[JsonConverterAttribute]` のような注釈サイトも compile-time で壊れるため、ファイル単位の blast-radius 分析はそれらも本物の依存 edge として表示する必要があり、`logical_reference_kind` では `attribute` / `annotation` として正しくラベル付けされる(分類修正前は同じサイトが phantom `call` として漏れていた)。さらに `GetFileDependencies` は C# の attribute 命名規約を正規化する: 慣用的な `[Foo]` は `symbol_name = "Foo"` で保存されるが、定義クラスは `FooAttribute` 命名となるため、logical-reference CTE に C# `attribute` 種別の行をサフィックス `'Attribute'` を付与した別名コピーとして UNION ALL で追加する(元の symbol_name が既に `Attribute` で終わっている場合は重複しないようスキップ)。これにより target ファイル join が attribute クラス側のファイルへ到達する。`GetFileDependencyHintsToResolvedType` はさらに candidate ごとに `has_metadata_ref` フラグを集約し、寄与する参照が metadata だけである edge については `SourceFileHasStructuredTypeEvidence` guard をバイパスするため、`[MyAudit] class Svc` のような純 attribute consumer ファイルも `impact MyAuditAttribute` で出るようになった(以前は filter で落ちていた)。`ResolveImpactFallbackNames` も対称的に、`Attribute` で終わる C# クラス名にはサフィックスを外した別名を追加するため、BFS を使わない file-hint 経路でも source symbol_name が定義クラス名と一致しない `[Foo]` サイトを発見できる。同じ正規化をシンボル単位の参照 reader にも伝播する: `references`・`inspect`・MCP `analyze_symbol` を支える `DbReader.SearchReferences`・`CountSearchReferences`・`CountSearchReferencesTotal` は、新しいヘルパー `ComputeCSharpAttributeSuffixAlias` で C# 限定の `queryAttributeAlias` を計算し、exact(folded と NOCASE)・substring の両方の WHERE 句に OR で追加する。これにより `references MyAuditAttribute`(および `inspect MyAuditAttribute` / `analyze_symbol MyAuditAttribute`)でも慣用的な `[MyAudit]` 参照サイトが `deps` / `impact` と一貫して surface する。substring モードでは alias を二つ目の LIKE にせず exact-OR で束ねているため、ユーザーが `MyAuditAttribute` と打った時に `FooAuditLog` のような無関係な名前に over-match しない。alias は C# 限定なので、Java / Kotlin / Scala / TypeScript の annotation サイトには影響しない。`callers` / `callees` は共有の call-graph 種別 (`call` / `instantiate` / `subscribe`) に絞って `--kind attribute|annotation` を CLI / MCP 境界で拒否する契約なので、alias は意図的に適用しない(dynamic call graph と dependency graph は別契約)。さらに alias 節が誤一致を生まないようスコープする: `ComputeCSharpAttributeSuffixAlias` は `referenceKind` が `attribute` 以外の call-graph 種別のときは `null` を返す(`references FooAttribute --kind call` は厳格一致のままで `Foo()` に一致しない)、3 つの reader メソッドの SQL alias 節は `f.lang = 'csharp' AND r.reference_kind = 'attribute'` に限定するため、`--lang` / `--kind` 無指定の `references FooAttribute` でも Java の annotation や C# の call 行を拾わない。suffix 検出も `OrdinalIgnoreCase` にして case-insensitive 化したため、`references myauditattribute` / `inspect MyAuditATTRIBUTE` のような小文字・混在ケースでも alias `MyAudit` を生成でき、周辺の NOCASE / folded 契約と整合する。回帰テスト `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies`(runtime と metadata の両方の edge が出ることを固定)、`DbReaderTests.GetFileDependencies_MatchesCSharpAttributeSuffixConvention`(`deps` 経由での `[MyAudit]` → `MyAuditAttribute` 正規化)、`DbReaderTests.SearchReferences_MatchesCSharpAttributeSuffixConvention_Substring` / `_Exact` / `_CSharpAttributeSuffixAliasDoesNotBleedToOtherLanguages`(`references` / `inspect` / `analyze_symbol` での同一正規化と C# 限定スコープ)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_NotAppliedToCallKind`(`--kind call` で alias が発火しないこと)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_UnscopedLangStillLimitsToCSharpAttributeRows`(無指定でも Java annotation や C# call 行に一致しないこと)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_CaseInsensitiveQuery`(小文字・混在ケースのクエリでも alias を生成)を追加。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 - **`callers` / `callees` が CLI と MCP 境界で `--kind attribute|annotation` を拒否し、Swift / Gradle の `@` metadata を `annotation` に再分類 (#293 追加対応)** — metadata 行 (`attribute` / `annotation`) は注釈対象そのものではなく body-range の外側シンボルに帰属するため、`callers Obsolete --kind attribute` は `[Obsolete] void M()` を `M` ではなく外側クラスの下に黙って返し、`[assembly: CLSCompliant]` や Kotlin の `@file:JvmName` のような file-level target は `container_name = NULL` で完全脱落していた。`QueryCommandRunner.RunCallers` / `RunCallees` は `--kind attribute` / `--kind annotation` を `CommandExitCodes.UsageError` で弾き、MCP の `callers` / `callees` ツールは `isError` ツールレスポンスを返すようにした。どちらの経路も、metadata 列挙の正しいパスである `references --kind attribute|annotation` へ誘導する。あわせて `AnnotationLanguages` に `swift` と `gradle` を追加し、Swift の `@available(iOS 13.0, *)` / `@objc` / `@MainActor` や Gradle / Groovy の `@CompileStatic` / `@TaskAction` を `call` ではなく `annotation` として記録するようにした(修正前は引数付きの `@Name(args)` 形は `callers` / `callees` / `hotspots` / `impact` に phantom call edge として漏れ、引数なしの `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` 形はこの集合で gate されていた no-arg annotation 出力から外れてインデックス自体に残らなかった)。回帰テストとして `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError`(theory 4 ケース)、`McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError`(theory 4 ケース)、`ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`、`ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation` を追加。対象: `src/CodeIndex/Cli/QueryCommandRunner.cs`、`src/CodeIndex/Mcp/McpToolHandlers.cs`、`src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 diff --git a/src/CodeIndex/Database/DbReader.cs b/src/CodeIndex/Database/DbReader.cs index 7f42ad140a..9ffa03710c 100644 --- a/src/CodeIndex/Database/DbReader.cs +++ b/src/CodeIndex/Database/DbReader.cs @@ -1945,22 +1945,31 @@ private bool IsMetadataTargetUnambiguous( // (例: `namespace A { class MyAuditAttribute { } } namespace B { class // MyAuditAttribute { } }`) でも ambiguity を 2 として検出できる。DISTINCT // f.path のままだと 1 に潰れ、metadata bypass が誤って有効化される。 - // For C# specifically, only count class-like definitions that are actually - // eligible as attribute metadata targets — i.e. signatures that contain - // `: ...Attribute` inheritance (e.g. `: Attribute`, `: System.Attribute`, - // `: BaseLogAttribute`). A plain `class MyAuditAttribute { }` without the - // Attribute-suffixed base class is not a valid target for `[MyAudit]` at - // compile time, so counting it as an ambiguity candidate would falsely - // suppress the metadata bypass and under-report `impact`. Other languages - // (Java `@interface`, Kotlin `annotation class`, etc.) still count every - // class-like definition because their metadata-target markers are not as - // easily captured by a single signature pattern. - // C# に限り、曖昧性候補は「Attribute を継承している」class-like 定義だけに絞る。 - // シグネチャに `: ...Attribute` を含むもの (`: Attribute`, `: System.Attribute`, - // `: BaseLogAttribute` 等) のみが `[MyAudit]` の実 target になり得るため、 - // `class MyAuditAttribute { }` のように継承を書いていない plain クラスを数える - // と `impact` が過少報告になる。他言語 (Java `@interface`, Kotlin `annotation - // class` 等) は単一 LIKE でマーカーを書きにくいので従来どおり class-like 全体を候補にする。 + // For C# specifically, only count class-like definitions that are + // plausible attribute metadata targets. We don't resolve base types + // transitively at SQL time, so the best portable approximation is + // "has an inheritance clause": any class declared with `: ...` is a + // potential attribute type (direct `: Attribute`, indirect + // `: BaseAudit` where BaseAudit itself derives from Attribute, or + // any other `: Base` chain). A plain `class MyAuditAttribute { }` + // with no `:` clause is not a valid `[MyAudit]` target at compile + // time, so excluding it prevents the metadata bypass from being + // falsely suppressed. We deliberately over-accept non-attribute + // derived classes rather than under-accept indirectly-derived + // attribute classes, because an invalid `[MyFoo]` against a + // non-attribute class would fail to compile and therefore not + // appear as a real reference. Other languages keep the broad + // class-like candidate set because their metadata-target markers + // don't match this signature shape. + // C# は SQL 時点で基底型を遡れないため、「何かを継承している + // class-like」を attribute 候補の近似として扱う。`: Attribute` の + // 直接継承も、`: BaseAudit` のような中間基底経由の間接継承も、 + // 何らかの `: Base` があれば候補に含める。継承節の無い plain + // `class MyAuditAttribute { }` だけを除外することで metadata + // bypass の誤抑止を防ぐ。非 attribute を過剰に含めるが、無効な + // `[MyFoo]` はコンパイルできないので実参照にはならず実害が無い。 + // 署名列が無い legacy DB では degrade して class-like 全体を候補にする。 + var csharpMetadataFilterForF = BuildCSharpMetadataTargetFilter("f"); var sql = $@" SELECT COUNT(*) FROM ( SELECT DISTINCT f.path, s.line, s.name @@ -1968,7 +1977,7 @@ FROM symbols s JOIN files f ON s.file_id = f.id WHERE s.name = @metadataAmbigName COLLATE NOCASE AND s.kind IN ('class','struct','interface') - AND (f.lang != 'csharp' OR (s.signature IS NOT NULL AND s.signature LIKE '%: %Attribute%')) + AND ({csharpMetadataFilterForF}) AND {supportedLangFilter}"; if (lang != null) { @@ -2565,6 +2574,35 @@ internal string GetFileColumnSql(string columnName, string? fallbackSql = null) return fallbackSql ?? "NULL"; } + // Build the C# metadata-target eligibility predicate used by `deps` + // (target_files / target_ambiguity) and `impact` (IsMetadataTargetUnambiguous). + // Returns a SQL fragment that evaluates to TRUE when a symbol row under the + // given files-table alias should be counted as a plausible `[Attribute]` + // target. Transitive base-type resolution isn't available at SQL time, so + // we approximate with "has an inheritance clause" — any C# class declared + // with a `: ...` base list qualifies (direct `: Attribute`, indirect + // `: BaseAudit` where BaseAudit derives from Attribute, or any other + // base). Non-inheriting plain classes are excluded. Legacy DBs without a + // `symbols.signature` column degrade to the broad class-like set so + // read-only / pre-migration indexes do not crash with `no such column`. + // `deps` と `impact` で共有する C# metadata-target 適格性判定。基底型を + // SQL 時点で遡れないため、「継承節を持つ」を近似とする(直接・間接の + // Attribute 継承を両方拾う)。継承のない plain class だけを除外する。 + // signature 列が無い legacy/read-only DB では判定を無効化し、従来の + // class-like 全体を候補集合にするので読み取り失敗しない。 + private string BuildCSharpMetadataTargetFilter(string fileAlias) + { + if (!_symbolColumns.Contains("signature")) + { + // Legacy schema — no signature column available. Fall back to the + // pre-metadata-eligibility behavior so `deps`/`impact` still return + // results rather than crashing. + // signature 列が無い legacy schema — filter を無効化して従来挙動に戻す。 + return "1 = 1"; + } + return $"{fileAlias}.lang != 'csharp' OR (s.signature IS NOT NULL AND s.signature LIKE '%: %')"; + } + /// /// Compute file-level dependency edges: which files reference symbols defined in which other files. /// ファイル間の依存関係エッジを算出: どのファイルがどのファイルで定義されたシンボルを参照しているか。 @@ -2685,23 +2723,24 @@ target_files AS ( -- が別行として残り、deps の参照カウントが膨らんでしまう。 -- has_metadata_target_kind further narrows the class-like set to targets -- that can legitimately be referenced as [Attribute] metadata. For C# - -- that additionally requires the class to inherit from something ending - -- in `Attribute` (signature LIKE '%: %Attribute%'); a plain - -- `class FooAttribute { }` without that inheritance is not a valid - -- [Foo] target at compile time and should not count as an ambiguity - -- candidate. Other languages keep the original class-like breadth. - -- has_metadata_target_kind は `[Attribute]` metadata target として - -- 有効な class-like のみを数える。C# に限り signature に - -- `: ...Attribute` を含むクラスだけを対象にする (`class FooAttribute { }` - -- のように Attribute 継承を書いていない plain クラスは `[Foo]` の target - -- にならないため)。Java/Kotlin 等は従来どおり class-like 全体を候補に残す。 + -- we cannot resolve base types transitively at SQL time, so the best + -- portable approximation is an inheritance-clause check: any class + -- declared with a base list is a potential attribute type (direct or + -- indirect Attribute derivation). A plain class FooAttribute with no + -- base clause is not a valid [Foo] target at compile time. + -- Other languages keep the original class-like breadth. Legacy DBs + -- without a signature column degrade to the broad class-like set. + -- has_metadata_target_kind は [Attribute] metadata target として妥当な + -- class-like のみに絞る。C# は SQL 時点で基底型を遡れないため、継承節を + -- 持つクラスを候補とする近似を採る(直接・間接の Attribute 継承を + -- 取りこぼさない)。他言語は class-like 全体を残す。signature 列が無い + -- legacy DB では filter を無効化し class-like 全体に戻る。 SELECT dst.path AS target_path, dst.lang AS target_lang, s.name AS symbol_name, MAX(CASE WHEN s.kind IN ('class','struct','interface') THEN 1 ELSE 0 END) AS has_class_like_kind, MAX(CASE WHEN s.kind IN ('class','struct','interface') - AND (dst.lang != 'csharp' - OR (s.signature IS NOT NULL AND s.signature LIKE '%: %Attribute%')) + AND (" + BuildCSharpMetadataTargetFilter("dst") + @") THEN 1 ELSE 0 END) AS has_metadata_target_kind FROM symbols s JOIN files dst ON s.file_id = dst.id @@ -2776,15 +2815,15 @@ JOIN symbols s ON s.file_id = dst.id AND s.name = tf.symbol_name AND s.kind IN ('class','struct','interface') - -- Same C# metadata-eligibility filter as target_files: only count - -- attribute-derived classes for C#. Otherwise two same-named plain - -- classes (only one of which inherits from Attribute) would still - -- look ambiguous and suppress the metadata bypass. - -- target_files と同じ metadata 適格性フィルタ。C# では Attribute を - -- 継承している class のみを数え、plain 同名クラスが混在していても - -- ambiguity と誤判定しない。 - AND (dst.lang != 'csharp' - OR (s.signature IS NOT NULL AND s.signature LIKE '%: %Attribute%')) + -- Same C# metadata-eligibility filter as target_files: a C# class + -- must have an inheritance clause to qualify as an attribute-target + -- candidate (covers direct : Attribute and indirect : BaseAudit + -- inheritance). Non-inheriting plain classes are not valid targets + -- and should not contribute to the ambiguity count. + -- target_files と同じ適格性フィルタ。C# クラスは継承節を持つときだけ + -- 候補になる(直接 : Attribute も、間接 : BaseAudit も救う)。 + -- 継承のない plain class は候補から外し ambiguity を誤判定しない。 + AND (" + BuildCSharpMetadataTargetFilter("dst") + @") WHERE tf.has_metadata_target_kind = 1 GROUP BY tf.target_lang, tf.symbol_name ), diff --git a/tests/CodeIndex.Tests/DbReaderTests.cs b/tests/CodeIndex.Tests/DbReaderTests.cs index 74cb3fda1e..25f51c6e7d 100644 --- a/tests/CodeIndex.Tests/DbReaderTests.cs +++ b/tests/CodeIndex.Tests/DbReaderTests.cs @@ -3271,6 +3271,59 @@ public class Svc d.TargetPath == "src/MyAttrAttribute.cs"); } + [Fact] + public void GetFileDependencies_CSharp_IndirectAttributeInheritance_ResolvesAsMetadataTarget() + { + // issue #293 round-17: the metadata-eligibility filter must not require + // the immediate base class to end in `Attribute`. Indirect inheritance + // like `class MyAuditAttribute : BaseAudit` where `BaseAudit : Attribute` + // is a valid `[MyAudit]` target at compile time. The previous strict + // pattern (`signature LIKE '%: %Attribute%'`) wrongly excluded the + // indirectly-derived class and dropped the deps edge. The loosened + // pattern (`signature LIKE '%: %'`) accepts any class with an + // inheritance clause, which is the best portable approximation since + // SQL cannot resolve base types transitively. + // issue #293 round-17: metadata 適格性フィルタは直接基底が + // `Attribute` で終わることを要求してはならない。 + // `class MyAuditAttribute : BaseAudit` で `BaseAudit : Attribute` の + // ような間接継承も `[MyAudit]` の有効な target である。以前の + // 厳格パターンは間接継承を弾いて deps エッジを落としていた。 + // 緩和パターンは「継承節を持つ class」を近似として採用する。 + InsertIndexedFile("src/BaseAudit.cs", "csharp", + """ + namespace App; + + public abstract class BaseAudit : System.Attribute + { + } + """); + InsertIndexedFile("src/MyAuditAttribute.cs", "csharp", + """ + namespace App; + + public sealed class MyAuditAttribute : BaseAudit + { + } + """); + InsertIndexedFile("src/Svc.cs", "csharp", + """ + namespace App; + + [MyAudit] + public class Svc + { + } + """); + + var deps = _reader.GetFileDependencies( + limit: 50, + lang: "csharp"); + + Assert.Contains(deps, d => + d.SourcePath == "src/Svc.cs" && + d.TargetPath == "src/MyAuditAttribute.cs"); + } + [Fact] public void GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests() { From 88f6fdfa727dbe802c58d18a0b37981e9422c387 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 18 Apr 2026 17:11:34 +0900 Subject: [PATCH 28/30] Fix #293 narrow C# metadata target to class-only and accept JS/TS function decorators Replace the boolean BuildCSharpMetadataTargetFilter helper with a unified BuildMetadataTargetKindExpr(fileAlias) expression that encodes per-language metadata-target kind rules: C# accepts only kind = 'class' with an inheritance clause (legacy fallback: class-only), JS/TS accepts class/struct/interface/ function (so function-decorator factories stay as valid deps edges), other graph-supported languages keep the broader class/struct/interface set. The previous filter left s.kind IN ('class','struct','interface') outside, which (a) wrongly counted struct/interface MyAuditAttribute as C# metadata ambiguity candidates even though only class can derive from System.Attribute, and (b) dropped JS/TS decorator deps edges to function-factory targets like `function sealed(target) { ... }` targeted by `@sealed`. Added DbReaderTests.GetFileDependencies_JavaScriptFunctionDecorator_ResolvesAsDependency and DbReaderTests.GetFileDependencies_CSharp_SameNameInterface_DoesNotBlockMetadataEdge. Co-Authored-By: Claude Opus 4 --- CHANGELOG.md | 4 +- src/CodeIndex/Database/DbReader.cs | 100 ++++++++++++++----------- tests/CodeIndex.Tests/DbReaderTests.cs | 74 ++++++++++++++++++ 3 files changed, 133 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c659161e72..d58a2fad0b 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/), - **Developer Guide now distinguishes SQLite's role from "zero dependencies" and labels release numbers as examples** — Clarified that cdidx remains a zero-configuration, single-file CLI with exactly one production dependency (`Microsoft.Data.Sqlite`), and made the release-workflow checklist explicitly state that `1.9.0` is only an example version to substitute during an actual release. Affected: `DEVELOPER_GUIDE.md`. #### Fixed -- **`deps` C# attribute suffix alias now only matches class-like targets, and `impact` metadata bypass is suppressed when the class-like target name is ambiguous across namespaces (#293 follow-up)** — The earlier `#293` follow-up made `deps` UNION a C#-only `symbol_name || 'Attribute'` suffix alias into the logical-reference CTE so `[Foo]` attribute consumers could reach `FooAttribute` class files, and made `impact` (`GetFileDependencyHintsToResolvedType`) bypass the structured-evidence guard whenever any contributing reference was `attribute` / `annotation`. Both changes were too broad. On `deps` the alias joined purely by name, so `[Foo]` would also depend on any file that merely defined a function / property / variable named `FooAttribute` (e.g. `void MyAuditAttribute()` in an unrelated helper). On `impact` the metadata bypass did not check whether the target name was uniquely a class-like definition, so when the same unqualified name (e.g. `MyAuditAttribute`) existed in multiple namespaces the rename/remove blast radius was over-reported — attribute / annotation reference rows only keep the short name and cannot be disambiguated. `DbReader.GetFileDependencies` now tags synthetic alias rows with `is_attribute_alias = 1` (real rows are `0`), propagates the flag through `source_name_counts` via `MIN`, and restricts alias edges to class-like target kinds (`class` / `struct` / `interface`) in the `edges` CTE join. The `target_files` CTE is also switched from `DISTINCT` over `(path, lang, name, kind)` to `GROUP BY (path, lang, name)` with a `MAX(...)` `has_class_like_kind` flag, so files that define both a class and a same-named function (e.g. the C# constructor `Target()` inside `class Target`) no longer fan out into two target rows and inflate the `deps` reference count. `DbReader.GetFileDependencyHintsToResolvedType` now calls a new `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` helper that counts class-like definitions with the same unqualified name *within the active impact scope* (graph-supported languages, plus the caller's `--lang` / `--path` / `--exclude-path` / `--exclude-tests` filters); the metadata evidence-guard bypass is only applied when that in-scope count is ≤ 1. If the name is ambiguous within scope, metadata-only edges fall through to the normal `SourceFileHasStructuredTypeEvidence` check, so pure `[MyAudit]` consumers are dropped instead of being mis-attributed. Scoping matters: a same-named class in an unrelated language / path / test tree must not suppress the C# metadata bypass, because attribute reference rows are already language-qualified through the graph-supported reference join, and the README contract for `impact` explicitly promises "active `--lang` / `--path` / `--exclude-path` / `--exclude-tests` scope". The C# attribute deps test `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`, the unambiguous single-definition `impact` test, and the pre-existing graph-queries dedupe test all still pass. Added `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets` (ambiguous same-name function must not produce a spurious edge), `DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget` (two `MyAuditAttribute` classes within the same impact scope suppress the metadata heuristic hint), `DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous` (single class definition still surfaces the `[MyAudit]` consumer), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope` (same-named Java annotation must not suppress a `--lang csharp` query), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope` (out-of-scope `src/B/` same-named class must not suppress a `src/A/`-scoped query), and `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests` (test-tree same-named class must not suppress an `--exclude-tests` query). `ResolveImpactFallbackNames` is additionally tightened so the C# `Attribute`-suffix alias is only applied to the resolved definition's own name: previously the alias was applied to every same-file fallback name, so a nested `BarAttribute` inside the file that defines `FooAttribute` caused `impact FooAttribute` to falsely attribute `[Bar]` use sites to `FooAttribute`'s blast radius. Added `DbReaderTests.GetFileDependencyHints_CSharpAttributeSuffixAlias_DoesNotLeakToSameFileSiblings` to pin this. `GetFileDependencies` is additionally restructured so metadata rows (`attribute` / `annotation`) cannot leak onto bare-name call-graph targets: the `logical_references` CTE now carries an `is_metadata` flag, `source_name_counts` groups by `(source_file_id, source_path, source_lang, symbol_name, is_attribute_alias, is_metadata)` so metadata and non-metadata rows with the same symbol_name stay separate, the `edges` CTE restricts ALL metadata edges (not just synthetic aliases) to `tf.has_class_like_kind = 1`, and two new CTEs resolve the remaining false-positive shapes: `metadata_raw_suppression` drops the raw C# `[Foo]` row when the synthetic `FooAttribute` alias already resolves to a class-like target in the same source file (so `[Foo]` does not fan out to both `class FooAttribute` and a plain `class Foo` that shares the bare name), and `target_ambiguity` drops metadata edges when the same-named symbol resolves to multiple class-like definitions in scope (so ambiguous `[MyAudit]` usage is left to `impact` / `references` to disambiguate rather than being fanned out to every same-named attribute class). The existing `GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies` test (which pins `[JsonConverter(...)]` → `class JsonConverter : Attribute` for a target class whose name does NOT end in `Attribute`) still passes because only one class-like target exists in scope, so `target_ambiguity` keeps it, the `is_metadata` / class-like restriction accepts it, and `metadata_raw_suppression` finds no synthetic `JsonConverterAttribute` alias target and therefore does not suppress the raw row. Added `DbReaderTests.GetFileDependencies_CSharpAttributeRawDoesNotLeakToBareNameClass_WhenSuffixTargetExists` (raw `[MyAudit]` row is suppressed when the alias resolves to `class MyAuditAttribute`, so `[MyAudit]` does NOT depend on an unrelated plain `class MyAudit` file), `DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotLeakToSameNameMethodOrProperty` (fully-qualified `[MyAuditAttribute]` metadata does NOT depend on a file that only defines a method / property named `MyAuditAttribute`), and `DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotFanOutWhenMultipleSameNameAttributeClasses` (two same-named attribute classes in different namespaces cause the metadata edge to be dropped as ambiguous). Both the `impact` ambiguity guard (`IsMetadataTargetUnambiguous`) and the `deps` ambiguity CTE (`target_ambiguity`) are additionally tightened to count class-like definitions at symbol-identity level instead of path level: `IsMetadataTargetUnambiguous` now uses `SELECT DISTINCT f.path, s.line, s.name` so two same-named class-like definitions in one source file (e.g. idiomatic C# `namespace A { class FooAttribute { } } namespace B { class FooAttribute { } }` in a single .cs file) register as ambiguous; `target_ambiguity` joins `target_files` back through `files` + `symbols` with `COUNT(*)` over class-like symbol rows so the same same-file duplicate case correctly drops the `[Foo]` metadata edge. Path-level counting previously treated these same-file duplicates as count=1 because `SELECT DISTINCT f.path` and `target_files` (grouped by `dst.path`) both collapse the two definitions into a single row, so `cdidx deps` and `cdidx impact` were confidently reporting an ambiguous short-name metadata reference as a definite dependency / blast-radius edge — contradicting the design intent that "ambiguity" includes duplicates in one file. Added `DbReaderTests.GetFileDependencies_CSharpAttributeAmbiguityCountsSameFileDuplicateClassDefinitions` and `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CountsSameFileDuplicateDefinitions` to pin the symbol-identity-level counting for both `deps` and `impact`. `IsMetadataTargetUnambiguous` is additionally tightened to wrap `--path` / `--exclude-path` parameters through `EscapeLikeQuery` and bind them as `%...%` patterns, matching the LIKE semantics used by the rest of the reader (search / references / deps etc.); without the wrap the in-scope class-like count would underflow to 1 even when two same-named attribute classes sit side-by-side in the requested CLI subtree such as `--path src/A/`, and the metadata bypass would falsely fire. Added `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CliPathPatternEscaping_SuppressesWhenInScopeIsAmbiguous` to pin this. `CSharpNoArgAttributeRegex` is additionally widened to accept an optional generic-argument list after the attribute name (`(?:\s*<[^\n]+?>)?`) so generic no-arg attribute forms such as `[MyAudit]`, `[assembly: MyAttr]`, the multi-line `[\n MyAttr\n]`, and nested-generic forms like `[MyAttr>]` / `[MyOther>>]` are indexed as `attribute` references instead of falling through both `CallRegex` (requires `(`) and the old no-arg regex (which ended at `]` / `,` / EOL and therefore rejected `` between the name and `]`). The inner generic segment now uses a non-greedy `[^\n]+?` so nested `>` tokens (e.g. the closing `>` of an inner `Dictionary`) no longer terminate the match prematurely — the old `[^>\n]+` excluded `>` and therefore broke on the first inner `>`, dropping the reference row for nested-generic attribute usages. Without the widening those sites produced zero reference rows, so `deps` / `impact` / `references` / `inspect` / `analyze_symbol` all missed the dependency edge to the real `FooAttribute` class file. Added `ReferenceExtractorTests.Extract_CsharpGenericNoArgAttribute_ClassifiedAsAttribute`, `ReferenceExtractorTests.Extract_CsharpNestedGenericNoArgAttribute_ClassifiedAsAttribute`, `DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_StillIndexedAndResolvesToAttributeClass`, `DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_AssemblyTarget_IsIndexed`, and `DbReaderTests.GetFileDependencies_CSharpNestedGenericNoArgAttribute_ResolvesToAttributeClass` to pin the end-to-end behavior. Both the `impact` ambiguity guard (`IsMetadataTargetUnambiguous`) and the `deps` ambiguity path (`target_files` / `target_ambiguity`) are additionally tightened to treat a C# class as a valid `[Attr]` metadata target ONLY when its signature inherits from an `Attribute`-suffixed base class (`signature LIKE '%: %Attribute%'`). Plain `class MyAuditAttribute { }` without `: Attribute` inheritance is not a valid `[MyAudit]` target at compile time, so counting it as an ambiguity candidate would under-report the `impact` blast radius and suppress a real `deps` metadata edge when a same-named plain class coexists with the real attribute class. `target_files` now exposes a new `has_metadata_target_kind` MAX column that applies the same C# signature filter, and `metadata_raw_suppression` / `target_ambiguity` / the metadata clause in `edges` are all switched to `has_metadata_target_kind`; Java / Kotlin / Scala targets remain unaffected because their metadata markers cannot be captured with a single portable signature LIKE, so they keep the broader class-like candidate set. Added `DbReaderTests.GetFileDependencies_CSharp_PlainClassWithAttributeSuffixName_DoesNotCountAsAmbiguity` to pin the `deps` path. Round-17 follow-up loosens the C# metadata-target signature filter from `'%: %Attribute%'` (requires the immediate base class to end in `Attribute`) to `'%: %'` (any inheritance clause qualifies) so indirect inheritance like `class MyAuditAttribute : BaseAudit` where `BaseAudit : Attribute` is also recognized as a valid `[MyAudit]` target; transitive base-type resolution is not available at SQL time, so the inheritance-clause check is the best portable approximation — non-attribute derivations are over-accepted, but an invalid `[MyFoo]` against a non-attribute class would fail to compile and therefore does not appear as a real reference. The shared `BuildCSharpMetadataTargetFilter` helper additionally degrades to `1 = 1` (the pre-metadata-eligibility behavior) when the `symbols.signature` column is missing on legacy / read-only DBs where `TryMigrateForRead` could not add it, so `deps` / `impact` queries return results instead of crashing with `no such column: s.signature`. Added `DbReaderTests.GetFileDependencies_CSharp_IndirectAttributeInheritance_ResolvesAsMetadataTarget` to pin the indirect-inheritance path. Affected: `src/CodeIndex/Database/DbReader.cs`, `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. +- **`deps` C# attribute suffix alias now only matches class-like targets, and `impact` metadata bypass is suppressed when the class-like target name is ambiguous across namespaces (#293 follow-up)** — The earlier `#293` follow-up made `deps` UNION a C#-only `symbol_name || 'Attribute'` suffix alias into the logical-reference CTE so `[Foo]` attribute consumers could reach `FooAttribute` class files, and made `impact` (`GetFileDependencyHintsToResolvedType`) bypass the structured-evidence guard whenever any contributing reference was `attribute` / `annotation`. Both changes were too broad. On `deps` the alias joined purely by name, so `[Foo]` would also depend on any file that merely defined a function / property / variable named `FooAttribute` (e.g. `void MyAuditAttribute()` in an unrelated helper). On `impact` the metadata bypass did not check whether the target name was uniquely a class-like definition, so when the same unqualified name (e.g. `MyAuditAttribute`) existed in multiple namespaces the rename/remove blast radius was over-reported — attribute / annotation reference rows only keep the short name and cannot be disambiguated. `DbReader.GetFileDependencies` now tags synthetic alias rows with `is_attribute_alias = 1` (real rows are `0`), propagates the flag through `source_name_counts` via `MIN`, and restricts alias edges to class-like target kinds (`class` / `struct` / `interface`) in the `edges` CTE join. The `target_files` CTE is also switched from `DISTINCT` over `(path, lang, name, kind)` to `GROUP BY (path, lang, name)` with a `MAX(...)` `has_class_like_kind` flag, so files that define both a class and a same-named function (e.g. the C# constructor `Target()` inside `class Target`) no longer fan out into two target rows and inflate the `deps` reference count. `DbReader.GetFileDependencyHintsToResolvedType` now calls a new `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` helper that counts class-like definitions with the same unqualified name *within the active impact scope* (graph-supported languages, plus the caller's `--lang` / `--path` / `--exclude-path` / `--exclude-tests` filters); the metadata evidence-guard bypass is only applied when that in-scope count is ≤ 1. If the name is ambiguous within scope, metadata-only edges fall through to the normal `SourceFileHasStructuredTypeEvidence` check, so pure `[MyAudit]` consumers are dropped instead of being mis-attributed. Scoping matters: a same-named class in an unrelated language / path / test tree must not suppress the C# metadata bypass, because attribute reference rows are already language-qualified through the graph-supported reference join, and the README contract for `impact` explicitly promises "active `--lang` / `--path` / `--exclude-path` / `--exclude-tests` scope". The C# attribute deps test `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`, the unambiguous single-definition `impact` test, and the pre-existing graph-queries dedupe test all still pass. Added `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets` (ambiguous same-name function must not produce a spurious edge), `DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget` (two `MyAuditAttribute` classes within the same impact scope suppress the metadata heuristic hint), `DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous` (single class definition still surfaces the `[MyAudit]` consumer), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope` (same-named Java annotation must not suppress a `--lang csharp` query), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope` (out-of-scope `src/B/` same-named class must not suppress a `src/A/`-scoped query), and `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests` (test-tree same-named class must not suppress an `--exclude-tests` query). `ResolveImpactFallbackNames` is additionally tightened so the C# `Attribute`-suffix alias is only applied to the resolved definition's own name: previously the alias was applied to every same-file fallback name, so a nested `BarAttribute` inside the file that defines `FooAttribute` caused `impact FooAttribute` to falsely attribute `[Bar]` use sites to `FooAttribute`'s blast radius. Added `DbReaderTests.GetFileDependencyHints_CSharpAttributeSuffixAlias_DoesNotLeakToSameFileSiblings` to pin this. `GetFileDependencies` is additionally restructured so metadata rows (`attribute` / `annotation`) cannot leak onto bare-name call-graph targets: the `logical_references` CTE now carries an `is_metadata` flag, `source_name_counts` groups by `(source_file_id, source_path, source_lang, symbol_name, is_attribute_alias, is_metadata)` so metadata and non-metadata rows with the same symbol_name stay separate, the `edges` CTE restricts ALL metadata edges (not just synthetic aliases) to `tf.has_class_like_kind = 1`, and two new CTEs resolve the remaining false-positive shapes: `metadata_raw_suppression` drops the raw C# `[Foo]` row when the synthetic `FooAttribute` alias already resolves to a class-like target in the same source file (so `[Foo]` does not fan out to both `class FooAttribute` and a plain `class Foo` that shares the bare name), and `target_ambiguity` drops metadata edges when the same-named symbol resolves to multiple class-like definitions in scope (so ambiguous `[MyAudit]` usage is left to `impact` / `references` to disambiguate rather than being fanned out to every same-named attribute class). The existing `GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies` test (which pins `[JsonConverter(...)]` → `class JsonConverter : Attribute` for a target class whose name does NOT end in `Attribute`) still passes because only one class-like target exists in scope, so `target_ambiguity` keeps it, the `is_metadata` / class-like restriction accepts it, and `metadata_raw_suppression` finds no synthetic `JsonConverterAttribute` alias target and therefore does not suppress the raw row. Added `DbReaderTests.GetFileDependencies_CSharpAttributeRawDoesNotLeakToBareNameClass_WhenSuffixTargetExists` (raw `[MyAudit]` row is suppressed when the alias resolves to `class MyAuditAttribute`, so `[MyAudit]` does NOT depend on an unrelated plain `class MyAudit` file), `DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotLeakToSameNameMethodOrProperty` (fully-qualified `[MyAuditAttribute]` metadata does NOT depend on a file that only defines a method / property named `MyAuditAttribute`), and `DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotFanOutWhenMultipleSameNameAttributeClasses` (two same-named attribute classes in different namespaces cause the metadata edge to be dropped as ambiguous). Both the `impact` ambiguity guard (`IsMetadataTargetUnambiguous`) and the `deps` ambiguity CTE (`target_ambiguity`) are additionally tightened to count class-like definitions at symbol-identity level instead of path level: `IsMetadataTargetUnambiguous` now uses `SELECT DISTINCT f.path, s.line, s.name` so two same-named class-like definitions in one source file (e.g. idiomatic C# `namespace A { class FooAttribute { } } namespace B { class FooAttribute { } }` in a single .cs file) register as ambiguous; `target_ambiguity` joins `target_files` back through `files` + `symbols` with `COUNT(*)` over class-like symbol rows so the same same-file duplicate case correctly drops the `[Foo]` metadata edge. Path-level counting previously treated these same-file duplicates as count=1 because `SELECT DISTINCT f.path` and `target_files` (grouped by `dst.path`) both collapse the two definitions into a single row, so `cdidx deps` and `cdidx impact` were confidently reporting an ambiguous short-name metadata reference as a definite dependency / blast-radius edge — contradicting the design intent that "ambiguity" includes duplicates in one file. Added `DbReaderTests.GetFileDependencies_CSharpAttributeAmbiguityCountsSameFileDuplicateClassDefinitions` and `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CountsSameFileDuplicateDefinitions` to pin the symbol-identity-level counting for both `deps` and `impact`. `IsMetadataTargetUnambiguous` is additionally tightened to wrap `--path` / `--exclude-path` parameters through `EscapeLikeQuery` and bind them as `%...%` patterns, matching the LIKE semantics used by the rest of the reader (search / references / deps etc.); without the wrap the in-scope class-like count would underflow to 1 even when two same-named attribute classes sit side-by-side in the requested CLI subtree such as `--path src/A/`, and the metadata bypass would falsely fire. Added `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CliPathPatternEscaping_SuppressesWhenInScopeIsAmbiguous` to pin this. `CSharpNoArgAttributeRegex` is additionally widened to accept an optional generic-argument list after the attribute name (`(?:\s*<[^\n]+?>)?`) so generic no-arg attribute forms such as `[MyAudit]`, `[assembly: MyAttr]`, the multi-line `[\n MyAttr\n]`, and nested-generic forms like `[MyAttr>]` / `[MyOther>>]` are indexed as `attribute` references instead of falling through both `CallRegex` (requires `(`) and the old no-arg regex (which ended at `]` / `,` / EOL and therefore rejected `` between the name and `]`). The inner generic segment now uses a non-greedy `[^\n]+?` so nested `>` tokens (e.g. the closing `>` of an inner `Dictionary`) no longer terminate the match prematurely — the old `[^>\n]+` excluded `>` and therefore broke on the first inner `>`, dropping the reference row for nested-generic attribute usages. Without the widening those sites produced zero reference rows, so `deps` / `impact` / `references` / `inspect` / `analyze_symbol` all missed the dependency edge to the real `FooAttribute` class file. Added `ReferenceExtractorTests.Extract_CsharpGenericNoArgAttribute_ClassifiedAsAttribute`, `ReferenceExtractorTests.Extract_CsharpNestedGenericNoArgAttribute_ClassifiedAsAttribute`, `DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_StillIndexedAndResolvesToAttributeClass`, `DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_AssemblyTarget_IsIndexed`, and `DbReaderTests.GetFileDependencies_CSharpNestedGenericNoArgAttribute_ResolvesToAttributeClass` to pin the end-to-end behavior. Both the `impact` ambiguity guard (`IsMetadataTargetUnambiguous`) and the `deps` ambiguity path (`target_files` / `target_ambiguity`) are additionally tightened to treat a C# class as a valid `[Attr]` metadata target ONLY when its signature inherits from an `Attribute`-suffixed base class (`signature LIKE '%: %Attribute%'`). Plain `class MyAuditAttribute { }` without `: Attribute` inheritance is not a valid `[MyAudit]` target at compile time, so counting it as an ambiguity candidate would under-report the `impact` blast radius and suppress a real `deps` metadata edge when a same-named plain class coexists with the real attribute class. `target_files` now exposes a new `has_metadata_target_kind` MAX column that applies the same C# signature filter, and `metadata_raw_suppression` / `target_ambiguity` / the metadata clause in `edges` are all switched to `has_metadata_target_kind`; Java / Kotlin / Scala targets remain unaffected because their metadata markers cannot be captured with a single portable signature LIKE, so they keep the broader class-like candidate set. Added `DbReaderTests.GetFileDependencies_CSharp_PlainClassWithAttributeSuffixName_DoesNotCountAsAmbiguity` to pin the `deps` path. Round-17 follow-up loosens the C# metadata-target signature filter from `'%: %Attribute%'` (requires the immediate base class to end in `Attribute`) to `'%: %'` (any inheritance clause qualifies) so indirect inheritance like `class MyAuditAttribute : BaseAudit` where `BaseAudit : Attribute` is also recognized as a valid `[MyAudit]` target; transitive base-type resolution is not available at SQL time, so the inheritance-clause check is the best portable approximation — non-attribute derivations are over-accepted, but an invalid `[MyFoo]` against a non-attribute class would fail to compile and therefore does not appear as a real reference. The shared `BuildCSharpMetadataTargetFilter` helper additionally degrades to `1 = 1` (the pre-metadata-eligibility behavior) when the `symbols.signature` column is missing on legacy / read-only DBs where `TryMigrateForRead` could not add it, so `deps` / `impact` queries return results instead of crashing with `no such column: s.signature`. Added `DbReaderTests.GetFileDependencies_CSharp_IndirectAttributeInheritance_ResolvesAsMetadataTarget` to pin the indirect-inheritance path. Round-18 follow-up replaces the boolean `BuildCSharpMetadataTargetFilter` helper with a unified `BuildMetadataTargetKindExpr(fileAlias)` expression that narrows the metadata-target candidate kind per language: C# accepts only `kind = 'class'` with an inheritance clause (with the same legacy `1 = 1` fallback to class-only when `symbols.signature` is missing), JavaScript/TypeScript accept `class` / `struct` / `interface` / `function` (so function-decorator factories like `function sealed(target) { ... }` targeted by `@sealed` stay as valid `deps` edges), and other graph-supported languages keep the broader class/struct/interface candidate set. The old filter applied only the C# signature clause and left `s.kind IN ('class', 'struct', 'interface')` outside, which (a) wrongly counted `struct MyAuditAttribute` / `interface MyAuditAttribute` as C# metadata-target ambiguity candidates even though neither can derive from `System.Attribute`, and (b) dropped JS/TS decorator edges to a `function sealed` factory because `has_metadata_target_kind` required a class-like kind. The new expression is applied at three call sites: `IsMetadataTargetUnambiguous` (via a precomputed `metadataTargetKindExprF` local), the `target_files` CTE `has_metadata_target_kind` MAX column, and the `target_ambiguity` CTE JOIN condition (where the outer `s.kind IN (...)` narrowing is removed since the new expression already encodes the per-language kind rule). Added `DbReaderTests.GetFileDependencies_JavaScriptFunctionDecorator_ResolvesAsDependency` (JS `@sealed` class → `function sealed(target)` factory file now produces a deps edge instead of being dropped) and `DbReaderTests.GetFileDependencies_CSharp_SameNameInterface_DoesNotBlockMetadataEdge` (same-named `interface MyAuditAttribute` no longer ambiguates `[MyAudit]` → `class MyAuditAttribute : Attribute`). Affected: `src/CodeIndex/Database/DbReader.cs`, `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. - **MCP `callers` / `callees` `kind` schema, CLI help, and README `--kind` option row no longer advertise metadata kinds as valid filter values (#293 follow-up)** — The `callers` / `callees` handlers reject `kind: "attribute"` / `kind: "annotation"` with a usage error (CLI) / `isError` tool response (MCP), but the MCP tool schema's `kind` property description still listed all five kinds as if they were valid filter values (`"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"`), `cdidx --help` similarly lumped `callers` / `callees` together with `references` for the `--kind` help row, and the README `--kind` option table (EN line 436 / JP line 1307) still directed readers to pass `--kind attribute` or `--kind annotation` on `callers` / `callees`. Schema and handler now agree: the `callers` / `callees` `kind` description now reads `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."`, the CLI `--kind` help row splits `references` (accepts all five kinds) from `callers` / `callees` (call-graph kinds only, metadata rejected, redirects to `references`), and the README `--kind` row (both English and Japanese) now explicitly states that `callers` / `callees` reject `--kind attribute|annotation` with a usage error and points to `references --kind attribute|annotation` as the correct metadata-enumeration path. The MCP `references` tool description is unchanged because it still accepts every indexed kind. Added `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` to pin the corrected schema wording. Affected: `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `src/CodeIndex/Cli/ConsoleUi.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `README.md`. - **`deps`, `impact`, `references`, `inspect`, and `analyze_symbol` retain metadata references as compile-time dependency edges and canonicalize the C# attribute-suffix convention (#293 follow-up)** — `DbReader.GetFileDependencies` (backing `cdidx deps`) and `DbReader.GetFileDependencyHintsToResolvedType` (backing `cdidx impact` / MCP `impact_analysis` heuristic file-hint fallback) keep metadata references in their logical reference CTE. Renaming or removing an attribute class still breaks annotated sites such as `[JsonConverterAttribute]` at compile time, so file-level blast-radius analysis must surface those edges as real dependencies — now labeled `attribute` / `annotation` in `logical_reference_kind` (previously these same sites leaked in as phantom `call` rows before classification was corrected). `GetFileDependencies` additionally canonicalizes the C# attribute naming convention: idiomatic `[Foo]` usage is stored with `symbol_name = "Foo"` but the defining class is `FooAttribute`, so the logical-reference CTE now UNION-ALLs a suffix-aliased copy of every C# `attribute`-kind row with `'Attribute'` appended (only when the original symbol_name does not already end in `Attribute`) so the target-file join reaches the attribute class file. `GetFileDependencyHintsToResolvedType` also aggregates a per-candidate `has_metadata_ref` flag and bypasses the `SourceFileHasStructuredTypeEvidence` guard for edges whose contributing references are purely metadata, so pure-attribute consumer files such as `[MyAudit] class Svc` now surface through `impact MyAuditAttribute` instead of being filtered out. `ResolveImpactFallbackNames` symmetrically adds suffix-stripped aliases for C# class names ending in `Attribute`, so the BFS-less file-hint path can find `[Foo]` sites whose source symbol_name does not match the defining class name. The same canonicalization is propagated to the symbol-level reference reader: `DbReader.SearchReferences`, `CountSearchReferences`, and `CountSearchReferencesTotal` (backing `references`, `inspect`, MCP `analyze_symbol`) now compute a C#-only `queryAttributeAlias` (defined in the new `ComputeCSharpAttributeSuffixAlias` helper) and OR it into both exact (folded and NOCASE) and substring WHERE clauses, so `references MyAuditAttribute` (and `inspect MyAuditAttribute` / `analyze_symbol MyAuditAttribute`) surface the idiomatic `[MyAudit]` call site consistently with `deps` / `impact`. Substring mode uses an exact-OR for the alias (not a second LIKE) so user queries like `MyAuditAttribute` do not over-match unrelated names such as `FooAuditLog`. The alias is C# only, so Java / Kotlin / Scala / TypeScript annotation sites remain unaffected. `callers` / `callees` intentionally skip the alias because they filter to the shared call-graph kinds (`call`, `instantiate`, `subscribe`) and reject `--kind attribute|annotation` at the CLI / MCP boundary — those commands model the dynamic call graph, not the dependency graph. The alias branch is additionally scoped so it cannot produce false positives: `ComputeCSharpAttributeSuffixAlias` returns `null` when `referenceKind` is a non-`attribute` call-graph kind (so `references FooAttribute --kind call` stays strict and does not match `Foo()`), and the SQL alias disjunct in all three reader methods is clamped to `f.lang = 'csharp' AND r.reference_kind = 'attribute'` so an unscoped `references FooAttribute` (no `--lang` / no `--kind`) still cannot bleed into Java annotations or C# call rows. Suffix detection is also case-insensitive (`OrdinalIgnoreCase`) so `references myauditattribute` and `inspect MyAuditATTRIBUTE` still produce the `MyAudit` alias, consistent with the NOCASE / folded contract of the surrounding exact / substring query paths. Added regression coverage `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies` (runtime + metadata edges both surface), `DbReaderTests.GetFileDependencies_MatchesCSharpAttributeSuffixConvention` (`[MyAudit]` → `MyAuditAttribute` canonicalization through `deps`), `DbReaderTests.SearchReferences_MatchesCSharpAttributeSuffixConvention_Substring` / `_Exact` / `_CSharpAttributeSuffixAliasDoesNotBleedToOtherLanguages` (same canonicalization through `references` / `inspect` / `analyze_symbol`, plus C#-only scope guard), `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_NotAppliedToCallKind` (alias must not fire under `--kind call`), `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_UnscopedLangStillLimitsToCSharpAttributeRows` (unscoped query must not match Java annotations or C# call rows), and `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_CaseInsensitiveQuery` (lowercase / mixed-case queries still produce the alias). Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. - **`callers` / `callees` now reject `--kind attribute|annotation` at the CLI and MCP boundary, and Swift / Gradle `@` metadata is reclassified as `annotation` (#293 follow-up)** — Metadata rows (`attribute` / `annotation`) are attributed to their enclosing body-range symbol rather than the annotated target, so `callers Obsolete --kind attribute` would silently return `[Obsolete] void M()` under the enclosing class (not `M`), and file-level targets such as `[assembly: CLSCompliant]` or Kotlin `@file:JvmName` would drop entirely because `container_name` is `NULL`. `QueryCommandRunner.RunCallers` / `RunCallees` now reject `--kind attribute` and `--kind annotation` with `CommandExitCodes.UsageError`, and the MCP `callers` / `callees` tools return an `isError` tool response; both paths direct users to `references --kind attribute|annotation`, which IS a correct metadata enumeration path. Separately, `AnnotationLanguages` now includes `swift` and `gradle`, so Swift `@available(iOS 13.0, *)` / `@objc` / `@MainActor` and Gradle/Groovy `@CompileStatic` / `@TaskAction` are recorded as `annotation` instead of `call` (previously the `@Name(args)` forms leaked into `callers` / `callees` / `hotspots` / `impact` as phantom call edges, and the no-arg `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` forms dropped from the index entirely because no-arg annotation emission was gated to languages in this set). Added `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError` (theory, 4 cases), `McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError` (theory, 4 cases), `ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`, and `ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation`. Affected: `src/CodeIndex/Cli/QueryCommandRunner.cs`, `src/CodeIndex/Mcp/McpToolHandlers.cs`, `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. @@ -686,7 +686,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **DEVELOPER_GUIDE が SQLite の位置付けとリリース番号の例示を明確化** — cdidx が「設定ゼロ・単一ファイル・本番依存1個」の CLI であり、その唯一の本番依存が `Microsoft.Data.Sqlite` であることを明記した。あわせて、リリース手順のチェックリスト中に出てくる `1.9.0` は固定値ではなく例示であり、実際のリリース番号へ読み替える前提だと明示した。対象: `DEVELOPER_GUIDE.md`。 #### 修正 -- **`deps` の C# attribute サフィックス別名を class-like ターゲットに限定し、`impact` の metadata bypass も class-like ターゲット名が名前空間をまたいで曖昧な場合は抑止するよう修正 (#293 追加対応)** — 直前の #293 追加対応では、`deps` の logical-reference CTE に C# 限定で `symbol_name || 'Attribute'` のサフィックス別名を UNION することで `[Foo]` の attribute consumer が `FooAttribute` クラスファイルへ到達するようにし、`impact`(`GetFileDependencyHintsToResolvedType`)も寄与参照に `attribute` / `annotation` が含まれていれば structured-evidence guard をバイパスするようにしていたが、どちらも広すぎた。`deps` 側では別名が純粋に名前だけで join していたため、`[Foo]` が同名の関数・プロパティ・変数(例: 無関係なヘルパーの `void MyAuditAttribute()`)を定義するファイルにも依存を張ってしまっていた。`impact` 側の metadata bypass も、対象名が一意に class-like 定義を指すかを確認していなかったため、`MyAuditAttribute` のような短い名前が複数の namespace に存在すると rename / remove の blast-radius が過剰報告されていた(`attribute` / `annotation` 参照行は短い名前しか保持できず曖昧性解消ができない)。`DbReader.GetFileDependencies` は合成された別名行に `is_attribute_alias = 1`(本物の行は `0`)をタグ付けし、`source_name_counts` では `MIN` でフラグを伝播し、`edges` CTE の join で別名 edge をターゲット側 kind が `class` / `struct` / `interface` のときだけに制限するようにした。`target_files` CTE も `DISTINCT (path, lang, name, kind)` から `GROUP BY (path, lang, name)` + `MAX(...)` の `has_class_like_kind` フラグに切り替えたため、class と同名関数(例: C# コンストラクタ `Target()` と `class Target`)を同じファイルで定義しているケースでもターゲット行が 2 本に増えず `deps` の参照数を水増ししない。`DbReader.GetFileDependencyHintsToResolvedType` は新ヘルパー `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` を呼び、*active な impact スコープ内* で同名の class-like 定義数を数える(graph 対応言語に加えて、呼び出し側の `--lang` / `--path` / `--exclude-path` / `--exclude-tests` を同じフィルタで適用)。in-scope でその数が 1 以下のときに限り metadata evidence-guard bypass を適用する。スコープ内で曖昧なときは metadata 単独 edge も通常の `SourceFileHasStructuredTypeEvidence` に戻り、純 `[MyAudit]` consumer は誤帰属せずに落ちる。スコープ尊重が重要: 別言語 / 別 path / test ツリーにある同名クラスが C# の metadata bypass を潰してはならない(参照側が既に graph 対応 join で言語修飾されているため、曖昧性は scope 内で判定すべき)。README の `impact` 契約も「active な `--lang` / `--path` / `--exclude-path` / `--exclude-tests` スコープ」と明記している。既存の C# attribute deps テスト `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`、曖昧ではない単一定義の `impact` テスト、および既存の graph-queries dedupe テストは全てグリーンのまま。回帰テストとして `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets`(同名関数があっても虚偽 edge が出ないこと)、`DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget`(impact スコープ内で `MyAuditAttribute` が複数ある場合に metadata heuristic hint を抑止)、`DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous`(単一クラス定義のみのときは従来どおり `[MyAudit]` consumer を浮かせる)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope`(別言語の同名 annotation は `--lang csharp` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope`(スコープ外 `src/B/` の同名クラスは `src/A/` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests`(test ツリーの同名クラスは `--exclude-tests` クエリを潰さない)を追加。`ResolveImpactFallbackNames` も併せて引き締め、C# の `Attribute` サフィックス別名は *解決済み定義自身* の名前にのみ適用するようにした: 以前は同一ファイル内の fallback 名全てに別名を付けていたため、`FooAttribute` を定義するファイルに nested で `BarAttribute` が存在するだけで `impact FooAttribute` が `[Bar]` 利用サイトを `FooAttribute` の影響範囲として誤報告していた。`DbReaderTests.GetFileDependencyHints_CSharpAttributeSuffixAlias_DoesNotLeakToSameFileSiblings` を追加して固定。さらに `GetFileDependencies` を再構成し、metadata 行(`attribute` / `annotation`)が bare-name の call-graph ターゲットに漏れないようにした: `logical_references` CTE に `is_metadata` フラグを付与し、`source_name_counts` の GROUP BY を `(source_file_id, source_path, source_lang, symbol_name, is_attribute_alias, is_metadata)` に拡張して metadata と非 metadata で同じ symbol_name の行を別グループ化する。`edges` CTE は全 metadata edge(合成別名だけでなく実体行も)に `tf.has_class_like_kind = 1` を要求し、さらに 2 つの CTE で残る誤検知を潰す: `metadata_raw_suppression` は同じ source file で合成別名 `FooAttribute` が class-like ターゲットに解決できているとき、生の C# `[Foo]` 行を落とす(`[Foo]` が `class FooAttribute` と無関係な同名 `class Foo` に fan-out しない)。`target_ambiguity` はスコープ内で同名 class-like ターゲットが複数あるとき metadata edge を落とす(曖昧な `[MyAudit]` は deps で fan-out させず、`impact` / `references` に判断を委ねる)。既存の `GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies`(`[JsonConverter(...)]` → `class JsonConverter : Attribute` をターゲット名に `Attribute` サフィックスがなくても固定)は、class-like ターゲットが 1 つしかないので `target_ambiguity` で残り、`is_metadata` / class-like 制限も受理し、合成 `JsonConverterAttribute` 別名は class-like に解決できないので `metadata_raw_suppression` も発火せず、そのまま通る。回帰テストとして `DbReaderTests.GetFileDependencies_CSharpAttributeRawDoesNotLeakToBareNameClass_WhenSuffixTargetExists`(別名が `class MyAuditAttribute` に解決できたとき生 `[MyAudit]` 行は抑止され、無関係な plain `class MyAudit` ファイルに依存しない)、`DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotLeakToSameNameMethodOrProperty`(完全形 `[MyAuditAttribute]` は同名メソッド / プロパティしか持たないファイルに依存しない)、`DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotFanOutWhenMultipleSameNameAttributeClasses`(別名前空間に同名 attribute クラスが 2 つあると metadata edge はあいまいとして落とす)を追加。加えて `impact` の曖昧性ガード (`IsMetadataTargetUnambiguous`) と `deps` の曖昧性 CTE (`target_ambiguity`) も、path 単位ではなく symbol identity 単位で class-like 定義を数えるように引き締めた: `IsMetadataTargetUnambiguous` は `SELECT DISTINCT f.path, s.line, s.name` に変更し、C# でよくある 1 つの .cs ファイル内に別名前空間で同名 class-like が 2 つ定義されている形 (`namespace A { class FooAttribute { } } namespace B { class FooAttribute { } }`) でも ambiguity を 2 として検出する。`target_ambiguity` は `target_files` を `files` + `symbols` に JOIN し直して `COUNT(*)` を class-like symbol 行に対して取るため、同じ同名ファイル内重複ケースでも `[Foo]` metadata edge を正しく落とせる。path 単位カウントでは `SELECT DISTINCT f.path` も `target_files`(`dst.path` で GROUP BY)も同一ファイル内の 2 定義を 1 行に潰してしまうため、従来は count=1 として曖昧性を取りこぼし、`cdidx deps` と `cdidx impact` が同名 short-name metadata 参照を確定的な依存 / blast-radius edge として出していた。これは曖昧性に "duplicates in one file" を含める前提の設計契約に反していた。`deps` と `impact` 両方で symbol-identity 単位カウントを固定する `DbReaderTests.GetFileDependencies_CSharpAttributeAmbiguityCountsSameFileDuplicateClassDefinitions` と `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CountsSameFileDuplicateDefinitions` を追加。さらに `IsMetadataTargetUnambiguous` の `--path` / `--exclude-path` パラメータを、他の reader 経路(search / references / deps 等)と同じ LIKE セマンティクスに揃えるため `EscapeLikeQuery` でエスケープしたうえで `%...%` で包んでバインドするようにした。生値のままだと `--path src/A/` のような CLI 形では LIKE が一致せず、リクエストされたサブツリーに同名 attribute クラスが 2 件並んでいても in-scope class-like カウントが 1 に過小化され、本来抑止すべき metadata bypass が誤発火してしまっていた。`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CliPathPatternEscaping_SuppressesWhenInScopeIsAmbiguous` を追加して固定する。あわせて `CSharpNoArgAttributeRegex` を拡張し、属性名の直後に任意のジェネリック引数リスト `(?:\s*<[^\n]+?>)?` を許容するようにしたため、`[MyAudit]`・`[assembly: MyAttr]`・複数行の `[\n MyAttr\n]`・ネストジェネリックの `[MyAttr>]` や `[MyOther>>]` のような引数なしジェネリック属性も `attribute` 参照としてインデックスされるようになった。内側ジェネリックセグメントは非貪欲な `[^\n]+?` に変更し、内側の `>`(例: `Dictionary` を閉じる `>`)で match が打ち切られないようにした — 従来の `[^>\n]+` は `>` を除外していたため最初の内側 `>` で崩れ、ネストジェネリック属性の参照行が落ちていた。従来は `CallRegex`(`(` 必須)でも旧 no-arg regex(`]` / `,` / EOL で終端しなければならず、名前と `]` の間に `` があると拒否)でも拾えず、参照行が 0 件となり、`deps` / `impact` / `references` / `inspect` / `analyze_symbol` が実属性クラス `FooAttribute` への依存エッジを全て取りこぼしていた。`ReferenceExtractorTests.Extract_CsharpGenericNoArgAttribute_ClassifiedAsAttribute`、`ReferenceExtractorTests.Extract_CsharpNestedGenericNoArgAttribute_ClassifiedAsAttribute`、`DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_StillIndexedAndResolvesToAttributeClass`、`DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_AssemblyTarget_IsIndexed`、`DbReaderTests.GetFileDependencies_CSharpNestedGenericNoArgAttribute_ResolvesToAttributeClass` を追加して end-to-end の振る舞いを固定する。加えて、`impact` の曖昧性ガード (`IsMetadataTargetUnambiguous`) と `deps` の曖昧性判定 (`target_files` / `target_ambiguity`) は、C# クラスが有効な `[Attr]` metadata target になるのは「signature が `Attribute` サフィックス付き基底を継承している」場合 (`signature LIKE '%: %Attribute%'`) に限定するよう引き締めた。`: Attribute` 継承を書いていない plain `class MyAuditAttribute { }` はコンパイル時に `[MyAudit]` の target にならないため、曖昧性候補として数えると `impact` の影響範囲が過少報告され、`deps` 側でも同名 plain クラスが共存する scene で実 attribute クラスへの metadata edge が落ちてしまう。`target_files` に新しい `has_metadata_target_kind` MAX 列を追加して同じ C# signature フィルタを適用し、`metadata_raw_suppression` / `target_ambiguity` / `edges` の metadata 節はすべて `has_metadata_target_kind` に切り替えた。Java / Kotlin / Scala は portable な signature LIKE で metadata marker を表現しづらいため従来通り class-like 全体を候補に残す。`deps` 側の挙動を固定する `DbReaderTests.GetFileDependencies_CSharp_PlainClassWithAttributeSuffixName_DoesNotCountAsAmbiguity` を追加。Round-17 追加対応では、C# metadata target の signature フィルタを `'%: %Attribute%'`(直接基底が `Attribute` で終わることを要求)から `'%: %'`(何らかの継承節を持てば候補)に緩和した。`class MyAuditAttribute : BaseAudit` で `BaseAudit : Attribute` のような間接継承でも `[MyAudit]` の有効な target として認識される。SQL 時点で基底型を遡ることはできないため、継承節チェックは portable な近似として採用する — 非 attribute 由来のクラスは過剰に受理するが、無効な `[MyFoo]` をコンパイルすることはできず実参照にはならないので実害はない。共有ヘルパー `BuildCSharpMetadataTargetFilter` はさらに、`TryMigrateForRead` が追加できなかった legacy / read-only DB で `symbols.signature` 列が欠落しているときは `1 = 1`(metadata-eligibility 導入前の挙動)へ degrade するので、`deps` / `impact` クエリは `no such column: s.signature` でクラッシュせず結果を返す。間接継承パスを固定する `DbReaderTests.GetFileDependencies_CSharp_IndirectAttributeInheritance_ResolvesAsMetadataTarget` を追加。対象: `src/CodeIndex/Database/DbReader.cs`、`src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 +- **`deps` の C# attribute サフィックス別名を class-like ターゲットに限定し、`impact` の metadata bypass も class-like ターゲット名が名前空間をまたいで曖昧な場合は抑止するよう修正 (#293 追加対応)** — 直前の #293 追加対応では、`deps` の logical-reference CTE に C# 限定で `symbol_name || 'Attribute'` のサフィックス別名を UNION することで `[Foo]` の attribute consumer が `FooAttribute` クラスファイルへ到達するようにし、`impact`(`GetFileDependencyHintsToResolvedType`)も寄与参照に `attribute` / `annotation` が含まれていれば structured-evidence guard をバイパスするようにしていたが、どちらも広すぎた。`deps` 側では別名が純粋に名前だけで join していたため、`[Foo]` が同名の関数・プロパティ・変数(例: 無関係なヘルパーの `void MyAuditAttribute()`)を定義するファイルにも依存を張ってしまっていた。`impact` 側の metadata bypass も、対象名が一意に class-like 定義を指すかを確認していなかったため、`MyAuditAttribute` のような短い名前が複数の namespace に存在すると rename / remove の blast-radius が過剰報告されていた(`attribute` / `annotation` 参照行は短い名前しか保持できず曖昧性解消ができない)。`DbReader.GetFileDependencies` は合成された別名行に `is_attribute_alias = 1`(本物の行は `0`)をタグ付けし、`source_name_counts` では `MIN` でフラグを伝播し、`edges` CTE の join で別名 edge をターゲット側 kind が `class` / `struct` / `interface` のときだけに制限するようにした。`target_files` CTE も `DISTINCT (path, lang, name, kind)` から `GROUP BY (path, lang, name)` + `MAX(...)` の `has_class_like_kind` フラグに切り替えたため、class と同名関数(例: C# コンストラクタ `Target()` と `class Target`)を同じファイルで定義しているケースでもターゲット行が 2 本に増えず `deps` の参照数を水増ししない。`DbReader.GetFileDependencyHintsToResolvedType` は新ヘルパー `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` を呼び、*active な impact スコープ内* で同名の class-like 定義数を数える(graph 対応言語に加えて、呼び出し側の `--lang` / `--path` / `--exclude-path` / `--exclude-tests` を同じフィルタで適用)。in-scope でその数が 1 以下のときに限り metadata evidence-guard bypass を適用する。スコープ内で曖昧なときは metadata 単独 edge も通常の `SourceFileHasStructuredTypeEvidence` に戻り、純 `[MyAudit]` consumer は誤帰属せずに落ちる。スコープ尊重が重要: 別言語 / 別 path / test ツリーにある同名クラスが C# の metadata bypass を潰してはならない(参照側が既に graph 対応 join で言語修飾されているため、曖昧性は scope 内で判定すべき)。README の `impact` 契約も「active な `--lang` / `--path` / `--exclude-path` / `--exclude-tests` スコープ」と明記している。既存の C# attribute deps テスト `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`、曖昧ではない単一定義の `impact` テスト、および既存の graph-queries dedupe テストは全てグリーンのまま。回帰テストとして `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets`(同名関数があっても虚偽 edge が出ないこと)、`DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget`(impact スコープ内で `MyAuditAttribute` が複数ある場合に metadata heuristic hint を抑止)、`DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous`(単一クラス定義のみのときは従来どおり `[MyAudit]` consumer を浮かせる)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope`(別言語の同名 annotation は `--lang csharp` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope`(スコープ外 `src/B/` の同名クラスは `src/A/` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests`(test ツリーの同名クラスは `--exclude-tests` クエリを潰さない)を追加。`ResolveImpactFallbackNames` も併せて引き締め、C# の `Attribute` サフィックス別名は *解決済み定義自身* の名前にのみ適用するようにした: 以前は同一ファイル内の fallback 名全てに別名を付けていたため、`FooAttribute` を定義するファイルに nested で `BarAttribute` が存在するだけで `impact FooAttribute` が `[Bar]` 利用サイトを `FooAttribute` の影響範囲として誤報告していた。`DbReaderTests.GetFileDependencyHints_CSharpAttributeSuffixAlias_DoesNotLeakToSameFileSiblings` を追加して固定。さらに `GetFileDependencies` を再構成し、metadata 行(`attribute` / `annotation`)が bare-name の call-graph ターゲットに漏れないようにした: `logical_references` CTE に `is_metadata` フラグを付与し、`source_name_counts` の GROUP BY を `(source_file_id, source_path, source_lang, symbol_name, is_attribute_alias, is_metadata)` に拡張して metadata と非 metadata で同じ symbol_name の行を別グループ化する。`edges` CTE は全 metadata edge(合成別名だけでなく実体行も)に `tf.has_class_like_kind = 1` を要求し、さらに 2 つの CTE で残る誤検知を潰す: `metadata_raw_suppression` は同じ source file で合成別名 `FooAttribute` が class-like ターゲットに解決できているとき、生の C# `[Foo]` 行を落とす(`[Foo]` が `class FooAttribute` と無関係な同名 `class Foo` に fan-out しない)。`target_ambiguity` はスコープ内で同名 class-like ターゲットが複数あるとき metadata edge を落とす(曖昧な `[MyAudit]` は deps で fan-out させず、`impact` / `references` に判断を委ねる)。既存の `GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies`(`[JsonConverter(...)]` → `class JsonConverter : Attribute` をターゲット名に `Attribute` サフィックスがなくても固定)は、class-like ターゲットが 1 つしかないので `target_ambiguity` で残り、`is_metadata` / class-like 制限も受理し、合成 `JsonConverterAttribute` 別名は class-like に解決できないので `metadata_raw_suppression` も発火せず、そのまま通る。回帰テストとして `DbReaderTests.GetFileDependencies_CSharpAttributeRawDoesNotLeakToBareNameClass_WhenSuffixTargetExists`(別名が `class MyAuditAttribute` に解決できたとき生 `[MyAudit]` 行は抑止され、無関係な plain `class MyAudit` ファイルに依存しない)、`DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotLeakToSameNameMethodOrProperty`(完全形 `[MyAuditAttribute]` は同名メソッド / プロパティしか持たないファイルに依存しない)、`DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotFanOutWhenMultipleSameNameAttributeClasses`(別名前空間に同名 attribute クラスが 2 つあると metadata edge はあいまいとして落とす)を追加。加えて `impact` の曖昧性ガード (`IsMetadataTargetUnambiguous`) と `deps` の曖昧性 CTE (`target_ambiguity`) も、path 単位ではなく symbol identity 単位で class-like 定義を数えるように引き締めた: `IsMetadataTargetUnambiguous` は `SELECT DISTINCT f.path, s.line, s.name` に変更し、C# でよくある 1 つの .cs ファイル内に別名前空間で同名 class-like が 2 つ定義されている形 (`namespace A { class FooAttribute { } } namespace B { class FooAttribute { } }`) でも ambiguity を 2 として検出する。`target_ambiguity` は `target_files` を `files` + `symbols` に JOIN し直して `COUNT(*)` を class-like symbol 行に対して取るため、同じ同名ファイル内重複ケースでも `[Foo]` metadata edge を正しく落とせる。path 単位カウントでは `SELECT DISTINCT f.path` も `target_files`(`dst.path` で GROUP BY)も同一ファイル内の 2 定義を 1 行に潰してしまうため、従来は count=1 として曖昧性を取りこぼし、`cdidx deps` と `cdidx impact` が同名 short-name metadata 参照を確定的な依存 / blast-radius edge として出していた。これは曖昧性に "duplicates in one file" を含める前提の設計契約に反していた。`deps` と `impact` 両方で symbol-identity 単位カウントを固定する `DbReaderTests.GetFileDependencies_CSharpAttributeAmbiguityCountsSameFileDuplicateClassDefinitions` と `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CountsSameFileDuplicateDefinitions` を追加。さらに `IsMetadataTargetUnambiguous` の `--path` / `--exclude-path` パラメータを、他の reader 経路(search / references / deps 等)と同じ LIKE セマンティクスに揃えるため `EscapeLikeQuery` でエスケープしたうえで `%...%` で包んでバインドするようにした。生値のままだと `--path src/A/` のような CLI 形では LIKE が一致せず、リクエストされたサブツリーに同名 attribute クラスが 2 件並んでいても in-scope class-like カウントが 1 に過小化され、本来抑止すべき metadata bypass が誤発火してしまっていた。`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CliPathPatternEscaping_SuppressesWhenInScopeIsAmbiguous` を追加して固定する。あわせて `CSharpNoArgAttributeRegex` を拡張し、属性名の直後に任意のジェネリック引数リスト `(?:\s*<[^\n]+?>)?` を許容するようにしたため、`[MyAudit]`・`[assembly: MyAttr]`・複数行の `[\n MyAttr\n]`・ネストジェネリックの `[MyAttr>]` や `[MyOther>>]` のような引数なしジェネリック属性も `attribute` 参照としてインデックスされるようになった。内側ジェネリックセグメントは非貪欲な `[^\n]+?` に変更し、内側の `>`(例: `Dictionary` を閉じる `>`)で match が打ち切られないようにした — 従来の `[^>\n]+` は `>` を除外していたため最初の内側 `>` で崩れ、ネストジェネリック属性の参照行が落ちていた。従来は `CallRegex`(`(` 必須)でも旧 no-arg regex(`]` / `,` / EOL で終端しなければならず、名前と `]` の間に `` があると拒否)でも拾えず、参照行が 0 件となり、`deps` / `impact` / `references` / `inspect` / `analyze_symbol` が実属性クラス `FooAttribute` への依存エッジを全て取りこぼしていた。`ReferenceExtractorTests.Extract_CsharpGenericNoArgAttribute_ClassifiedAsAttribute`、`ReferenceExtractorTests.Extract_CsharpNestedGenericNoArgAttribute_ClassifiedAsAttribute`、`DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_StillIndexedAndResolvesToAttributeClass`、`DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_AssemblyTarget_IsIndexed`、`DbReaderTests.GetFileDependencies_CSharpNestedGenericNoArgAttribute_ResolvesToAttributeClass` を追加して end-to-end の振る舞いを固定する。加えて、`impact` の曖昧性ガード (`IsMetadataTargetUnambiguous`) と `deps` の曖昧性判定 (`target_files` / `target_ambiguity`) は、C# クラスが有効な `[Attr]` metadata target になるのは「signature が `Attribute` サフィックス付き基底を継承している」場合 (`signature LIKE '%: %Attribute%'`) に限定するよう引き締めた。`: Attribute` 継承を書いていない plain `class MyAuditAttribute { }` はコンパイル時に `[MyAudit]` の target にならないため、曖昧性候補として数えると `impact` の影響範囲が過少報告され、`deps` 側でも同名 plain クラスが共存する scene で実 attribute クラスへの metadata edge が落ちてしまう。`target_files` に新しい `has_metadata_target_kind` MAX 列を追加して同じ C# signature フィルタを適用し、`metadata_raw_suppression` / `target_ambiguity` / `edges` の metadata 節はすべて `has_metadata_target_kind` に切り替えた。Java / Kotlin / Scala は portable な signature LIKE で metadata marker を表現しづらいため従来通り class-like 全体を候補に残す。`deps` 側の挙動を固定する `DbReaderTests.GetFileDependencies_CSharp_PlainClassWithAttributeSuffixName_DoesNotCountAsAmbiguity` を追加。Round-17 追加対応では、C# metadata target の signature フィルタを `'%: %Attribute%'`(直接基底が `Attribute` で終わることを要求)から `'%: %'`(何らかの継承節を持てば候補)に緩和した。`class MyAuditAttribute : BaseAudit` で `BaseAudit : Attribute` のような間接継承でも `[MyAudit]` の有効な target として認識される。SQL 時点で基底型を遡ることはできないため、継承節チェックは portable な近似として採用する — 非 attribute 由来のクラスは過剰に受理するが、無効な `[MyFoo]` をコンパイルすることはできず実参照にはならないので実害はない。共有ヘルパー `BuildCSharpMetadataTargetFilter` はさらに、`TryMigrateForRead` が追加できなかった legacy / read-only DB で `symbols.signature` 列が欠落しているときは `1 = 1`(metadata-eligibility 導入前の挙動)へ degrade するので、`deps` / `impact` クエリは `no such column: s.signature` でクラッシュせず結果を返す。間接継承パスを固定する `DbReaderTests.GetFileDependencies_CSharp_IndirectAttributeInheritance_ResolvesAsMetadataTarget` を追加。Round-18 では boolean の `BuildCSharpMetadataTargetFilter` ヘルパーを、ファイル別エイリアスを取り言語ごとに metadata-target 候補 kind を絞る `BuildMetadataTargetKindExpr(fileAlias)` 式へ置き換える。C# は継承節を持つ `kind = 'class'` のみ許可(`symbols.signature` が欠落する legacy DB では従来通り class-only にフォールバック)、JavaScript / TypeScript は `class` / `struct` / `interface` / `function` を許可(`function sealed(target) { ... }` のような factory 関数を `@sealed` が target とする decorator edge が `deps` に残る)、その他の graph 対応言語は従来通り `class` / `struct` / `interface` を受ける。旧フィルタは C# の signature 句だけを適用し、`s.kind IN ('class', 'struct', 'interface')` が外側に残っていたため、(a) C# で `struct MyAuditAttribute` / `interface MyAuditAttribute` が `System.Attribute` を継承できないにも関わらず metadata-target の ambiguity 候補として数えられ、(b) JS/TS で decorator の対象が `function sealed` factory の場合に `has_metadata_target_kind` が class-like を要求するために deps edge が落ちていた。新しい式は 3 箇所で使う: `IsMetadataTargetUnambiguous`(事前計算の `metadataTargetKindExprF` 経由)、`target_files` CTE の `has_metadata_target_kind` MAX 列、`target_ambiguity` CTE の JOIN 条件(言語ごとの kind narrowing が式に内包されたため、外側の `s.kind IN (...)` 絞り込みは削除)。`DbReaderTests.GetFileDependencies_JavaScriptFunctionDecorator_ResolvesAsDependency`(JS の `@sealed` class → `function sealed(target)` factory ファイルが落ちずに deps edge を生成)と `DbReaderTests.GetFileDependencies_CSharp_SameNameInterface_DoesNotBlockMetadataEdge`(同名の `interface MyAuditAttribute` が `[MyAudit]` → `class MyAuditAttribute : Attribute` を ambiguity 扱いにして抑止しない)を追加。対象: `src/CodeIndex/Database/DbReader.cs`、`src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 - **MCP の `callers` / `callees` の `kind` schema・CLI help・README の `--kind` オプション行が metadata kinds を有効フィルタ値として広告しないよう修正 (#293 追加対応)** — `callers` / `callees` の handler は `kind: "attribute"` / `kind: "annotation"` を usage error(CLI)/ `isError` ツールレスポンス(MCP)で拒否するにもかかわらず、MCP ツール schema の `kind` プロパティ説明は 5 種すべてを列挙した `"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"` のままで、`cdidx --help` の `--kind` 行も `callers` / `callees` を `references` と同じ行にまとめていた。さらに README の `--kind` オプション表(英語 436 行目 / 日本語 1307 行目)も、`callers` / `callees` で `--kind attribute` / `--kind annotation` を渡せるかのように案内していた。schema と handler の契約を一致させ、`callers` / `callees` の `kind` description を `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."` に修正し、CLI の `--kind` help 行も `references`(5 種すべて受理)と `callers` / `callees`(call-graph 種別のみ、metadata kinds は拒否して `references` へ誘導)を分けるようにし、README の `--kind` 行(英語・日本語の両方)も `callers` / `callees` が `--kind attribute|annotation` を usage error で拒否すること、metadata 列挙の正規経路が `references --kind attribute|annotation` であることを明示する文言に直した。MCP の `references` ツール説明は全 reference kind を受け付ける契約のままなので変更なし。修正後の schema 文言を固定する `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` を追加。対象: `src/CodeIndex/Mcp/McpToolDefinitions.cs`、`src/CodeIndex/Cli/ConsoleUi.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`、`README.md`。 - **`deps`・`impact`・`references`・`inspect`・`analyze_symbol` が metadata 参照を compile-time 依存として保持し、C# の attribute サフィックス命名規約を正規化 (#293 追加対応)** — `cdidx deps` の実装である `DbReader.GetFileDependencies` と、`cdidx impact` / MCP `impact_analysis` の heuristic file-hint フォールバックである `DbReader.GetFileDependencyHintsToResolvedType` は、logical reference CTE に metadata 参照も含める。attribute クラスを rename / 削除すれば `[JsonConverterAttribute]` のような注釈サイトも compile-time で壊れるため、ファイル単位の blast-radius 分析はそれらも本物の依存 edge として表示する必要があり、`logical_reference_kind` では `attribute` / `annotation` として正しくラベル付けされる(分類修正前は同じサイトが phantom `call` として漏れていた)。さらに `GetFileDependencies` は C# の attribute 命名規約を正規化する: 慣用的な `[Foo]` は `symbol_name = "Foo"` で保存されるが、定義クラスは `FooAttribute` 命名となるため、logical-reference CTE に C# `attribute` 種別の行をサフィックス `'Attribute'` を付与した別名コピーとして UNION ALL で追加する(元の symbol_name が既に `Attribute` で終わっている場合は重複しないようスキップ)。これにより target ファイル join が attribute クラス側のファイルへ到達する。`GetFileDependencyHintsToResolvedType` はさらに candidate ごとに `has_metadata_ref` フラグを集約し、寄与する参照が metadata だけである edge については `SourceFileHasStructuredTypeEvidence` guard をバイパスするため、`[MyAudit] class Svc` のような純 attribute consumer ファイルも `impact MyAuditAttribute` で出るようになった(以前は filter で落ちていた)。`ResolveImpactFallbackNames` も対称的に、`Attribute` で終わる C# クラス名にはサフィックスを外した別名を追加するため、BFS を使わない file-hint 経路でも source symbol_name が定義クラス名と一致しない `[Foo]` サイトを発見できる。同じ正規化をシンボル単位の参照 reader にも伝播する: `references`・`inspect`・MCP `analyze_symbol` を支える `DbReader.SearchReferences`・`CountSearchReferences`・`CountSearchReferencesTotal` は、新しいヘルパー `ComputeCSharpAttributeSuffixAlias` で C# 限定の `queryAttributeAlias` を計算し、exact(folded と NOCASE)・substring の両方の WHERE 句に OR で追加する。これにより `references MyAuditAttribute`(および `inspect MyAuditAttribute` / `analyze_symbol MyAuditAttribute`)でも慣用的な `[MyAudit]` 参照サイトが `deps` / `impact` と一貫して surface する。substring モードでは alias を二つ目の LIKE にせず exact-OR で束ねているため、ユーザーが `MyAuditAttribute` と打った時に `FooAuditLog` のような無関係な名前に over-match しない。alias は C# 限定なので、Java / Kotlin / Scala / TypeScript の annotation サイトには影響しない。`callers` / `callees` は共有の call-graph 種別 (`call` / `instantiate` / `subscribe`) に絞って `--kind attribute|annotation` を CLI / MCP 境界で拒否する契約なので、alias は意図的に適用しない(dynamic call graph と dependency graph は別契約)。さらに alias 節が誤一致を生まないようスコープする: `ComputeCSharpAttributeSuffixAlias` は `referenceKind` が `attribute` 以外の call-graph 種別のときは `null` を返す(`references FooAttribute --kind call` は厳格一致のままで `Foo()` に一致しない)、3 つの reader メソッドの SQL alias 節は `f.lang = 'csharp' AND r.reference_kind = 'attribute'` に限定するため、`--lang` / `--kind` 無指定の `references FooAttribute` でも Java の annotation や C# の call 行を拾わない。suffix 検出も `OrdinalIgnoreCase` にして case-insensitive 化したため、`references myauditattribute` / `inspect MyAuditATTRIBUTE` のような小文字・混在ケースでも alias `MyAudit` を生成でき、周辺の NOCASE / folded 契約と整合する。回帰テスト `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies`(runtime と metadata の両方の edge が出ることを固定)、`DbReaderTests.GetFileDependencies_MatchesCSharpAttributeSuffixConvention`(`deps` 経由での `[MyAudit]` → `MyAuditAttribute` 正規化)、`DbReaderTests.SearchReferences_MatchesCSharpAttributeSuffixConvention_Substring` / `_Exact` / `_CSharpAttributeSuffixAliasDoesNotBleedToOtherLanguages`(`references` / `inspect` / `analyze_symbol` での同一正規化と C# 限定スコープ)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_NotAppliedToCallKind`(`--kind call` で alias が発火しないこと)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_UnscopedLangStillLimitsToCSharpAttributeRows`(無指定でも Java annotation や C# call 行に一致しないこと)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_CaseInsensitiveQuery`(小文字・混在ケースのクエリでも alias を生成)を追加。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 - **`callers` / `callees` が CLI と MCP 境界で `--kind attribute|annotation` を拒否し、Swift / Gradle の `@` metadata を `annotation` に再分類 (#293 追加対応)** — metadata 行 (`attribute` / `annotation`) は注釈対象そのものではなく body-range の外側シンボルに帰属するため、`callers Obsolete --kind attribute` は `[Obsolete] void M()` を `M` ではなく外側クラスの下に黙って返し、`[assembly: CLSCompliant]` や Kotlin の `@file:JvmName` のような file-level target は `container_name = NULL` で完全脱落していた。`QueryCommandRunner.RunCallers` / `RunCallees` は `--kind attribute` / `--kind annotation` を `CommandExitCodes.UsageError` で弾き、MCP の `callers` / `callees` ツールは `isError` ツールレスポンスを返すようにした。どちらの経路も、metadata 列挙の正しいパスである `references --kind attribute|annotation` へ誘導する。あわせて `AnnotationLanguages` に `swift` と `gradle` を追加し、Swift の `@available(iOS 13.0, *)` / `@objc` / `@MainActor` や Gradle / Groovy の `@CompileStatic` / `@TaskAction` を `call` ではなく `annotation` として記録するようにした(修正前は引数付きの `@Name(args)` 形は `callers` / `callees` / `hotspots` / `impact` に phantom call edge として漏れ、引数なしの `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` 形はこの集合で gate されていた no-arg annotation 出力から外れてインデックス自体に残らなかった)。回帰テストとして `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError`(theory 4 ケース)、`McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError`(theory 4 ケース)、`ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`、`ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation` を追加。対象: `src/CodeIndex/Cli/QueryCommandRunner.cs`、`src/CodeIndex/Mcp/McpToolHandlers.cs`、`src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 diff --git a/src/CodeIndex/Database/DbReader.cs b/src/CodeIndex/Database/DbReader.cs index 9ffa03710c..99c8a0ddc8 100644 --- a/src/CodeIndex/Database/DbReader.cs +++ b/src/CodeIndex/Database/DbReader.cs @@ -1968,16 +1968,15 @@ private bool IsMetadataTargetUnambiguous( // `class MyAuditAttribute { }` だけを除外することで metadata // bypass の誤抑止を防ぐ。非 attribute を過剰に含めるが、無効な // `[MyFoo]` はコンパイルできないので実参照にはならず実害が無い。 - // 署名列が無い legacy DB では degrade して class-like 全体を候補にする。 - var csharpMetadataFilterForF = BuildCSharpMetadataTargetFilter("f"); + // 署名列が無い legacy DB では degrade して class 限定のみ使う。 + var metadataTargetKindExprF = BuildMetadataTargetKindExpr("f"); var sql = $@" SELECT COUNT(*) FROM ( SELECT DISTINCT f.path, s.line, s.name FROM symbols s JOIN files f ON s.file_id = f.id WHERE s.name = @metadataAmbigName COLLATE NOCASE - AND s.kind IN ('class','struct','interface') - AND ({csharpMetadataFilterForF}) + AND {metadataTargetKindExprF} AND {supportedLangFilter}"; if (lang != null) { @@ -2574,33 +2573,49 @@ internal string GetFileColumnSql(string columnName, string? fallbackSql = null) return fallbackSql ?? "NULL"; } - // Build the C# metadata-target eligibility predicate used by `deps` - // (target_files / target_ambiguity) and `impact` (IsMetadataTargetUnambiguous). - // Returns a SQL fragment that evaluates to TRUE when a symbol row under the - // given files-table alias should be counted as a plausible `[Attribute]` - // target. Transitive base-type resolution isn't available at SQL time, so - // we approximate with "has an inheritance clause" — any C# class declared - // with a `: ...` base list qualifies (direct `: Attribute`, indirect - // `: BaseAudit` where BaseAudit derives from Attribute, or any other - // base). Non-inheriting plain classes are excluded. Legacy DBs without a - // `symbols.signature` column degrade to the broad class-like set so - // read-only / pre-migration indexes do not crash with `no such column`. - // `deps` と `impact` で共有する C# metadata-target 適格性判定。基底型を - // SQL 時点で遡れないため、「継承節を持つ」を近似とする(直接・間接の - // Attribute 継承を両方拾う)。継承のない plain class だけを除外する。 - // signature 列が無い legacy/read-only DB では判定を無効化し、従来の - // class-like 全体を候補集合にするので読み取り失敗しない。 - private string BuildCSharpMetadataTargetFilter(string fileAlias) - { - if (!_symbolColumns.Contains("signature")) - { - // Legacy schema — no signature column available. Fall back to the - // pre-metadata-eligibility behavior so `deps`/`impact` still return - // results rather than crashing. - // signature 列が無い legacy schema — filter を無効化して従来挙動に戻す。 - return "1 = 1"; - } - return $"{fileAlias}.lang != 'csharp' OR (s.signature IS NOT NULL AND s.signature LIKE '%: %')"; + // Build the language-aware metadata-target eligibility predicate used by + // `deps` (target_files / target_ambiguity) and `impact` + // (IsMetadataTargetUnambiguous). Returns a SQL fragment that evaluates to + // TRUE when a `(symbols s, files )` row should be counted as a + // plausible metadata target (`[Attribute]` / `@Annotation` / `@decorator`). + // Rules by language: + // - C# (`csharp`): only `kind = 'class'` with an inheritance clause + // (signature LIKE '%: %'). `struct` / `interface` cannot be attribute + // targets even when they share the `MyAuditAttribute` name, so they + // must not count toward the ambiguity guard or produce metadata + // edges. Transitive base-type resolution is not available at SQL + // time, so "has an inheritance clause" is the portable approximation + // for direct `: Attribute` plus indirect `: BaseAudit` where + // BaseAudit itself derives from Attribute. Legacy DBs without a + // `symbols.signature` column degrade to `kind = 'class'` only so + // read-only / pre-migration indexes still return a result instead of + // crashing with `no such column`. + // - JS / TS (`javascript` / `typescript`): decorators legitimately + // target both class-like constructs AND factory `function` + // definitions (e.g. `function sealed(target) {}` used as + // `@sealed class Foo {}`), so `function` must be accepted too. + // - Everything else (Java `@interface`, Kotlin `annotation class`, + // Scala annotation classes, etc.): the annotation target is a + // class-like declaration, so keep the original class-like candidate + // set (`class` / `struct` / `interface`). + // `deps` と `impact` で共有する言語別 metadata-target 適格性判定。 + // C# は class 限定 + 継承節チェック(signature 列欠落時は class 限定のみ)。 + // JS / TS は decorator が function factory も対象にするため function を許容。 + // それ以外は従来どおり class-like を候補にする。 + private string BuildMetadataTargetKindExpr(string fileAlias) + { + // C# clause — class only (interface/struct cannot be attribute targets). + // C# は class のみ(interface/struct は attribute target にできない)。 + var csharpClause = _symbolColumns.Contains("signature") + ? $"({fileAlias}.lang = 'csharp' AND s.kind = 'class' AND s.signature IS NOT NULL AND s.signature LIKE '%: %')" + : $"({fileAlias}.lang = 'csharp' AND s.kind = 'class')"; + // JS / TS clause — decorators target classes and factory functions. + // JS / TS は decorator が class と factory function を対象にする。 + var jsClause = $"({fileAlias}.lang IN ('javascript','typescript') AND s.kind IN ('class','struct','interface','function'))"; + // All other graph-supported languages keep the original class-like set. + // その他の graph 対応言語は従来どおり class-like を対象にする。 + var otherClause = $"({fileAlias}.lang NOT IN ('csharp','javascript','typescript') AND s.kind IN ('class','struct','interface'))"; + return $"({csharpClause} OR {jsClause} OR {otherClause})"; } /// @@ -2739,8 +2754,7 @@ target_files AS ( dst.lang AS target_lang, s.name AS symbol_name, MAX(CASE WHEN s.kind IN ('class','struct','interface') THEN 1 ELSE 0 END) AS has_class_like_kind, - MAX(CASE WHEN s.kind IN ('class','struct','interface') - AND (" + BuildCSharpMetadataTargetFilter("dst") + @") + MAX(CASE WHEN " + BuildMetadataTargetKindExpr("dst") + @" THEN 1 ELSE 0 END) AS has_metadata_target_kind FROM symbols s JOIN files dst ON s.file_id = dst.id @@ -2814,16 +2828,16 @@ JOIN files dst JOIN symbols s ON s.file_id = dst.id AND s.name = tf.symbol_name - AND s.kind IN ('class','struct','interface') - -- Same C# metadata-eligibility filter as target_files: a C# class - -- must have an inheritance clause to qualify as an attribute-target - -- candidate (covers direct : Attribute and indirect : BaseAudit - -- inheritance). Non-inheriting plain classes are not valid targets - -- and should not contribute to the ambiguity count. - -- target_files と同じ適格性フィルタ。C# クラスは継承節を持つときだけ - -- 候補になる(直接 : Attribute も、間接 : BaseAudit も救う)。 - -- 継承のない plain class は候補から外し ambiguity を誤判定しない。 - AND (" + BuildCSharpMetadataTargetFilter("dst") + @") + -- Same language-aware metadata-eligibility filter as + -- target_files: C# restricts to `class` with inheritance + -- clause (interface/struct cannot be attribute targets); + -- JS/TS additionally accepts `function` (decorator + -- factory); others keep the class-like candidate set. + -- target_files と同じ言語別 metadata 適格性フィルタ。 + -- C# は class 限定 + 継承節 (interface/struct は除外)。 + -- JS/TS は decorator factory 用に function も許容。 + -- それ以外は class-like 全体を候補にする。 + AND " + BuildMetadataTargetKindExpr("dst") + @" WHERE tf.has_metadata_target_kind = 1 GROUP BY tf.target_lang, tf.symbol_name ), diff --git a/tests/CodeIndex.Tests/DbReaderTests.cs b/tests/CodeIndex.Tests/DbReaderTests.cs index 25f51c6e7d..93839f897c 100644 --- a/tests/CodeIndex.Tests/DbReaderTests.cs +++ b/tests/CodeIndex.Tests/DbReaderTests.cs @@ -3324,6 +3324,80 @@ public class Svc d.TargetPath == "src/MyAuditAttribute.cs"); } + [Fact] + public void GetFileDependencies_JavaScriptFunctionDecorator_ResolvesAsDependency() + { + // issue #293 round-18: JavaScript/TypeScript decorators legitimately target + // factory `function`s (e.g. `function sealed(target) { ... }`), not only + // class-like definitions. The metadata-target predicate must accept + // `function` for JS/TS or decorator edges to a function target are dropped + // from `deps`. + // issue #293 round-18: JS/TS decorator は `function sealed(target){...}` のような + // factory 関数も正当な target となる。JS/TS では `function` を metadata target の + // 対象 kind として許可しないと、function を対象とする decorator edge が deps から欠落する。 + InsertIndexedFile("src/decorators.js", "javascript", + """ + export function sealed(target) { + Object.freeze(target); + } + """); + InsertIndexedFile("src/model.js", "javascript", + """ + import { sealed } from './decorators.js'; + + @sealed + class Foo { + } + """); + + var deps = _reader.GetFileDependencies( + limit: 50, + lang: "javascript"); + + Assert.Contains(deps, d => + d.SourcePath == "src/model.js" && + d.TargetPath == "src/decorators.js"); + } + + [Fact] + public void GetFileDependencies_CSharp_SameNameInterface_DoesNotBlockMetadataEdge() + { + // issue #293 round-18: ambiguity should only count truly attribute-eligible + // duplicates. In C#, only `class` can inherit from `System.Attribute` — + // a same-named `interface` or `struct` cannot be an attribute target, so it + // must not suppress the metadata deps edge to the legitimate attribute class. + // issue #293 round-18: ambiguity 判定は attribute 適格な重複だけを数えるべき。 + // C# では `class` のみが `System.Attribute` を継承できるため、同名の + // `interface` や `struct` が存在しても metadata deps edge を抑止してはならない。 + InsertIndexedFile("src/MyAuditAttribute.cs", "csharp", + """ + public sealed class MyAuditAttribute : System.Attribute + { + } + """); + InsertIndexedFile("src/IMyAudit.cs", "csharp", + """ + public interface MyAuditAttribute + { + } + """); + InsertIndexedFile("src/Svc.cs", "csharp", + """ + [MyAudit] + public class Svc + { + } + """); + + var deps = _reader.GetFileDependencies( + limit: 50, + lang: "csharp"); + + Assert.Contains(deps, d => + d.SourcePath == "src/Svc.cs" && + d.TargetPath == "src/MyAuditAttribute.cs"); + } + [Fact] public void GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests() { From d4f858dfd4847a259817ed2503f3ae2ee4d12952 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 18 Apr 2026 17:24:09 +0900 Subject: [PATCH 29/30] Fix #293 treat NULL signature as metadata-eligible on legacy-migration DBs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-19 follow-up to #293: BuildMetadataTargetKindExpr previously required `s.signature IS NOT NULL AND s.signature LIKE '%: %'` when the `signature` column existed. On a DB whose schema was migrated in place via TryMigrateForRead without reindexing, existing rows keep NULL signatures and this predicate rejected every C# class — silently dropping real `[MyAudit]` → `class MyAuditAttribute : System.Attribute` edges from `deps` and `impact` until the user re-ran `cdidx index`. Matching the spirit of the column-missing `1 = 1` fallback, NULL signatures are now treated as eligible rather than rejected outright: (s.signature IS NULL OR s.signature LIKE '%: %') Added DbReaderTests.GetFileDependencies_CSharp_LegacyDbWithNullSignature_StillResolvesAttributeEdge. The remaining edge case — a non-attribute same-named class silently suppressing the real edge through over-broad `LIKE '%: %'` matching — is deferred to #435 since it needs a schema-level `is_metadata_target` column to resolve cleanly. Co-Authored-By: Claude Opus 4 --- CHANGELOG.md | 4 +-- src/CodeIndex/Database/DbReader.cs | 16 +++++++-- tests/CodeIndex.Tests/DbReaderTests.cs | 49 ++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d58a2fad0b..9160addc60 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/), - **Developer Guide now distinguishes SQLite's role from "zero dependencies" and labels release numbers as examples** — Clarified that cdidx remains a zero-configuration, single-file CLI with exactly one production dependency (`Microsoft.Data.Sqlite`), and made the release-workflow checklist explicitly state that `1.9.0` is only an example version to substitute during an actual release. Affected: `DEVELOPER_GUIDE.md`. #### Fixed -- **`deps` C# attribute suffix alias now only matches class-like targets, and `impact` metadata bypass is suppressed when the class-like target name is ambiguous across namespaces (#293 follow-up)** — The earlier `#293` follow-up made `deps` UNION a C#-only `symbol_name || 'Attribute'` suffix alias into the logical-reference CTE so `[Foo]` attribute consumers could reach `FooAttribute` class files, and made `impact` (`GetFileDependencyHintsToResolvedType`) bypass the structured-evidence guard whenever any contributing reference was `attribute` / `annotation`. Both changes were too broad. On `deps` the alias joined purely by name, so `[Foo]` would also depend on any file that merely defined a function / property / variable named `FooAttribute` (e.g. `void MyAuditAttribute()` in an unrelated helper). On `impact` the metadata bypass did not check whether the target name was uniquely a class-like definition, so when the same unqualified name (e.g. `MyAuditAttribute`) existed in multiple namespaces the rename/remove blast radius was over-reported — attribute / annotation reference rows only keep the short name and cannot be disambiguated. `DbReader.GetFileDependencies` now tags synthetic alias rows with `is_attribute_alias = 1` (real rows are `0`), propagates the flag through `source_name_counts` via `MIN`, and restricts alias edges to class-like target kinds (`class` / `struct` / `interface`) in the `edges` CTE join. The `target_files` CTE is also switched from `DISTINCT` over `(path, lang, name, kind)` to `GROUP BY (path, lang, name)` with a `MAX(...)` `has_class_like_kind` flag, so files that define both a class and a same-named function (e.g. the C# constructor `Target()` inside `class Target`) no longer fan out into two target rows and inflate the `deps` reference count. `DbReader.GetFileDependencyHintsToResolvedType` now calls a new `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` helper that counts class-like definitions with the same unqualified name *within the active impact scope* (graph-supported languages, plus the caller's `--lang` / `--path` / `--exclude-path` / `--exclude-tests` filters); the metadata evidence-guard bypass is only applied when that in-scope count is ≤ 1. If the name is ambiguous within scope, metadata-only edges fall through to the normal `SourceFileHasStructuredTypeEvidence` check, so pure `[MyAudit]` consumers are dropped instead of being mis-attributed. Scoping matters: a same-named class in an unrelated language / path / test tree must not suppress the C# metadata bypass, because attribute reference rows are already language-qualified through the graph-supported reference join, and the README contract for `impact` explicitly promises "active `--lang` / `--path` / `--exclude-path` / `--exclude-tests` scope". The C# attribute deps test `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`, the unambiguous single-definition `impact` test, and the pre-existing graph-queries dedupe test all still pass. Added `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets` (ambiguous same-name function must not produce a spurious edge), `DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget` (two `MyAuditAttribute` classes within the same impact scope suppress the metadata heuristic hint), `DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous` (single class definition still surfaces the `[MyAudit]` consumer), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope` (same-named Java annotation must not suppress a `--lang csharp` query), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope` (out-of-scope `src/B/` same-named class must not suppress a `src/A/`-scoped query), and `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests` (test-tree same-named class must not suppress an `--exclude-tests` query). `ResolveImpactFallbackNames` is additionally tightened so the C# `Attribute`-suffix alias is only applied to the resolved definition's own name: previously the alias was applied to every same-file fallback name, so a nested `BarAttribute` inside the file that defines `FooAttribute` caused `impact FooAttribute` to falsely attribute `[Bar]` use sites to `FooAttribute`'s blast radius. Added `DbReaderTests.GetFileDependencyHints_CSharpAttributeSuffixAlias_DoesNotLeakToSameFileSiblings` to pin this. `GetFileDependencies` is additionally restructured so metadata rows (`attribute` / `annotation`) cannot leak onto bare-name call-graph targets: the `logical_references` CTE now carries an `is_metadata` flag, `source_name_counts` groups by `(source_file_id, source_path, source_lang, symbol_name, is_attribute_alias, is_metadata)` so metadata and non-metadata rows with the same symbol_name stay separate, the `edges` CTE restricts ALL metadata edges (not just synthetic aliases) to `tf.has_class_like_kind = 1`, and two new CTEs resolve the remaining false-positive shapes: `metadata_raw_suppression` drops the raw C# `[Foo]` row when the synthetic `FooAttribute` alias already resolves to a class-like target in the same source file (so `[Foo]` does not fan out to both `class FooAttribute` and a plain `class Foo` that shares the bare name), and `target_ambiguity` drops metadata edges when the same-named symbol resolves to multiple class-like definitions in scope (so ambiguous `[MyAudit]` usage is left to `impact` / `references` to disambiguate rather than being fanned out to every same-named attribute class). The existing `GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies` test (which pins `[JsonConverter(...)]` → `class JsonConverter : Attribute` for a target class whose name does NOT end in `Attribute`) still passes because only one class-like target exists in scope, so `target_ambiguity` keeps it, the `is_metadata` / class-like restriction accepts it, and `metadata_raw_suppression` finds no synthetic `JsonConverterAttribute` alias target and therefore does not suppress the raw row. Added `DbReaderTests.GetFileDependencies_CSharpAttributeRawDoesNotLeakToBareNameClass_WhenSuffixTargetExists` (raw `[MyAudit]` row is suppressed when the alias resolves to `class MyAuditAttribute`, so `[MyAudit]` does NOT depend on an unrelated plain `class MyAudit` file), `DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotLeakToSameNameMethodOrProperty` (fully-qualified `[MyAuditAttribute]` metadata does NOT depend on a file that only defines a method / property named `MyAuditAttribute`), and `DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotFanOutWhenMultipleSameNameAttributeClasses` (two same-named attribute classes in different namespaces cause the metadata edge to be dropped as ambiguous). Both the `impact` ambiguity guard (`IsMetadataTargetUnambiguous`) and the `deps` ambiguity CTE (`target_ambiguity`) are additionally tightened to count class-like definitions at symbol-identity level instead of path level: `IsMetadataTargetUnambiguous` now uses `SELECT DISTINCT f.path, s.line, s.name` so two same-named class-like definitions in one source file (e.g. idiomatic C# `namespace A { class FooAttribute { } } namespace B { class FooAttribute { } }` in a single .cs file) register as ambiguous; `target_ambiguity` joins `target_files` back through `files` + `symbols` with `COUNT(*)` over class-like symbol rows so the same same-file duplicate case correctly drops the `[Foo]` metadata edge. Path-level counting previously treated these same-file duplicates as count=1 because `SELECT DISTINCT f.path` and `target_files` (grouped by `dst.path`) both collapse the two definitions into a single row, so `cdidx deps` and `cdidx impact` were confidently reporting an ambiguous short-name metadata reference as a definite dependency / blast-radius edge — contradicting the design intent that "ambiguity" includes duplicates in one file. Added `DbReaderTests.GetFileDependencies_CSharpAttributeAmbiguityCountsSameFileDuplicateClassDefinitions` and `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CountsSameFileDuplicateDefinitions` to pin the symbol-identity-level counting for both `deps` and `impact`. `IsMetadataTargetUnambiguous` is additionally tightened to wrap `--path` / `--exclude-path` parameters through `EscapeLikeQuery` and bind them as `%...%` patterns, matching the LIKE semantics used by the rest of the reader (search / references / deps etc.); without the wrap the in-scope class-like count would underflow to 1 even when two same-named attribute classes sit side-by-side in the requested CLI subtree such as `--path src/A/`, and the metadata bypass would falsely fire. Added `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CliPathPatternEscaping_SuppressesWhenInScopeIsAmbiguous` to pin this. `CSharpNoArgAttributeRegex` is additionally widened to accept an optional generic-argument list after the attribute name (`(?:\s*<[^\n]+?>)?`) so generic no-arg attribute forms such as `[MyAudit]`, `[assembly: MyAttr]`, the multi-line `[\n MyAttr\n]`, and nested-generic forms like `[MyAttr>]` / `[MyOther>>]` are indexed as `attribute` references instead of falling through both `CallRegex` (requires `(`) and the old no-arg regex (which ended at `]` / `,` / EOL and therefore rejected `` between the name and `]`). The inner generic segment now uses a non-greedy `[^\n]+?` so nested `>` tokens (e.g. the closing `>` of an inner `Dictionary`) no longer terminate the match prematurely — the old `[^>\n]+` excluded `>` and therefore broke on the first inner `>`, dropping the reference row for nested-generic attribute usages. Without the widening those sites produced zero reference rows, so `deps` / `impact` / `references` / `inspect` / `analyze_symbol` all missed the dependency edge to the real `FooAttribute` class file. Added `ReferenceExtractorTests.Extract_CsharpGenericNoArgAttribute_ClassifiedAsAttribute`, `ReferenceExtractorTests.Extract_CsharpNestedGenericNoArgAttribute_ClassifiedAsAttribute`, `DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_StillIndexedAndResolvesToAttributeClass`, `DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_AssemblyTarget_IsIndexed`, and `DbReaderTests.GetFileDependencies_CSharpNestedGenericNoArgAttribute_ResolvesToAttributeClass` to pin the end-to-end behavior. Both the `impact` ambiguity guard (`IsMetadataTargetUnambiguous`) and the `deps` ambiguity path (`target_files` / `target_ambiguity`) are additionally tightened to treat a C# class as a valid `[Attr]` metadata target ONLY when its signature inherits from an `Attribute`-suffixed base class (`signature LIKE '%: %Attribute%'`). Plain `class MyAuditAttribute { }` without `: Attribute` inheritance is not a valid `[MyAudit]` target at compile time, so counting it as an ambiguity candidate would under-report the `impact` blast radius and suppress a real `deps` metadata edge when a same-named plain class coexists with the real attribute class. `target_files` now exposes a new `has_metadata_target_kind` MAX column that applies the same C# signature filter, and `metadata_raw_suppression` / `target_ambiguity` / the metadata clause in `edges` are all switched to `has_metadata_target_kind`; Java / Kotlin / Scala targets remain unaffected because their metadata markers cannot be captured with a single portable signature LIKE, so they keep the broader class-like candidate set. Added `DbReaderTests.GetFileDependencies_CSharp_PlainClassWithAttributeSuffixName_DoesNotCountAsAmbiguity` to pin the `deps` path. Round-17 follow-up loosens the C# metadata-target signature filter from `'%: %Attribute%'` (requires the immediate base class to end in `Attribute`) to `'%: %'` (any inheritance clause qualifies) so indirect inheritance like `class MyAuditAttribute : BaseAudit` where `BaseAudit : Attribute` is also recognized as a valid `[MyAudit]` target; transitive base-type resolution is not available at SQL time, so the inheritance-clause check is the best portable approximation — non-attribute derivations are over-accepted, but an invalid `[MyFoo]` against a non-attribute class would fail to compile and therefore does not appear as a real reference. The shared `BuildCSharpMetadataTargetFilter` helper additionally degrades to `1 = 1` (the pre-metadata-eligibility behavior) when the `symbols.signature` column is missing on legacy / read-only DBs where `TryMigrateForRead` could not add it, so `deps` / `impact` queries return results instead of crashing with `no such column: s.signature`. Added `DbReaderTests.GetFileDependencies_CSharp_IndirectAttributeInheritance_ResolvesAsMetadataTarget` to pin the indirect-inheritance path. Round-18 follow-up replaces the boolean `BuildCSharpMetadataTargetFilter` helper with a unified `BuildMetadataTargetKindExpr(fileAlias)` expression that narrows the metadata-target candidate kind per language: C# accepts only `kind = 'class'` with an inheritance clause (with the same legacy `1 = 1` fallback to class-only when `symbols.signature` is missing), JavaScript/TypeScript accept `class` / `struct` / `interface` / `function` (so function-decorator factories like `function sealed(target) { ... }` targeted by `@sealed` stay as valid `deps` edges), and other graph-supported languages keep the broader class/struct/interface candidate set. The old filter applied only the C# signature clause and left `s.kind IN ('class', 'struct', 'interface')` outside, which (a) wrongly counted `struct MyAuditAttribute` / `interface MyAuditAttribute` as C# metadata-target ambiguity candidates even though neither can derive from `System.Attribute`, and (b) dropped JS/TS decorator edges to a `function sealed` factory because `has_metadata_target_kind` required a class-like kind. The new expression is applied at three call sites: `IsMetadataTargetUnambiguous` (via a precomputed `metadataTargetKindExprF` local), the `target_files` CTE `has_metadata_target_kind` MAX column, and the `target_ambiguity` CTE JOIN condition (where the outer `s.kind IN (...)` narrowing is removed since the new expression already encodes the per-language kind rule). Added `DbReaderTests.GetFileDependencies_JavaScriptFunctionDecorator_ResolvesAsDependency` (JS `@sealed` class → `function sealed(target)` factory file now produces a deps edge instead of being dropped) and `DbReaderTests.GetFileDependencies_CSharp_SameNameInterface_DoesNotBlockMetadataEdge` (same-named `interface MyAuditAttribute` no longer ambiguates `[MyAudit]` → `class MyAuditAttribute : Attribute`). Affected: `src/CodeIndex/Database/DbReader.cs`, `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. +- **`deps` C# attribute suffix alias now only matches class-like targets, and `impact` metadata bypass is suppressed when the class-like target name is ambiguous across namespaces (#293 follow-up)** — The earlier `#293` follow-up made `deps` UNION a C#-only `symbol_name || 'Attribute'` suffix alias into the logical-reference CTE so `[Foo]` attribute consumers could reach `FooAttribute` class files, and made `impact` (`GetFileDependencyHintsToResolvedType`) bypass the structured-evidence guard whenever any contributing reference was `attribute` / `annotation`. Both changes were too broad. On `deps` the alias joined purely by name, so `[Foo]` would also depend on any file that merely defined a function / property / variable named `FooAttribute` (e.g. `void MyAuditAttribute()` in an unrelated helper). On `impact` the metadata bypass did not check whether the target name was uniquely a class-like definition, so when the same unqualified name (e.g. `MyAuditAttribute`) existed in multiple namespaces the rename/remove blast radius was over-reported — attribute / annotation reference rows only keep the short name and cannot be disambiguated. `DbReader.GetFileDependencies` now tags synthetic alias rows with `is_attribute_alias = 1` (real rows are `0`), propagates the flag through `source_name_counts` via `MIN`, and restricts alias edges to class-like target kinds (`class` / `struct` / `interface`) in the `edges` CTE join. The `target_files` CTE is also switched from `DISTINCT` over `(path, lang, name, kind)` to `GROUP BY (path, lang, name)` with a `MAX(...)` `has_class_like_kind` flag, so files that define both a class and a same-named function (e.g. the C# constructor `Target()` inside `class Target`) no longer fan out into two target rows and inflate the `deps` reference count. `DbReader.GetFileDependencyHintsToResolvedType` now calls a new `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` helper that counts class-like definitions with the same unqualified name *within the active impact scope* (graph-supported languages, plus the caller's `--lang` / `--path` / `--exclude-path` / `--exclude-tests` filters); the metadata evidence-guard bypass is only applied when that in-scope count is ≤ 1. If the name is ambiguous within scope, metadata-only edges fall through to the normal `SourceFileHasStructuredTypeEvidence` check, so pure `[MyAudit]` consumers are dropped instead of being mis-attributed. Scoping matters: a same-named class in an unrelated language / path / test tree must not suppress the C# metadata bypass, because attribute reference rows are already language-qualified through the graph-supported reference join, and the README contract for `impact` explicitly promises "active `--lang` / `--path` / `--exclude-path` / `--exclude-tests` scope". The C# attribute deps test `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`, the unambiguous single-definition `impact` test, and the pre-existing graph-queries dedupe test all still pass. Added `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets` (ambiguous same-name function must not produce a spurious edge), `DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget` (two `MyAuditAttribute` classes within the same impact scope suppress the metadata heuristic hint), `DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous` (single class definition still surfaces the `[MyAudit]` consumer), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope` (same-named Java annotation must not suppress a `--lang csharp` query), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope` (out-of-scope `src/B/` same-named class must not suppress a `src/A/`-scoped query), and `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests` (test-tree same-named class must not suppress an `--exclude-tests` query). `ResolveImpactFallbackNames` is additionally tightened so the C# `Attribute`-suffix alias is only applied to the resolved definition's own name: previously the alias was applied to every same-file fallback name, so a nested `BarAttribute` inside the file that defines `FooAttribute` caused `impact FooAttribute` to falsely attribute `[Bar]` use sites to `FooAttribute`'s blast radius. Added `DbReaderTests.GetFileDependencyHints_CSharpAttributeSuffixAlias_DoesNotLeakToSameFileSiblings` to pin this. `GetFileDependencies` is additionally restructured so metadata rows (`attribute` / `annotation`) cannot leak onto bare-name call-graph targets: the `logical_references` CTE now carries an `is_metadata` flag, `source_name_counts` groups by `(source_file_id, source_path, source_lang, symbol_name, is_attribute_alias, is_metadata)` so metadata and non-metadata rows with the same symbol_name stay separate, the `edges` CTE restricts ALL metadata edges (not just synthetic aliases) to `tf.has_class_like_kind = 1`, and two new CTEs resolve the remaining false-positive shapes: `metadata_raw_suppression` drops the raw C# `[Foo]` row when the synthetic `FooAttribute` alias already resolves to a class-like target in the same source file (so `[Foo]` does not fan out to both `class FooAttribute` and a plain `class Foo` that shares the bare name), and `target_ambiguity` drops metadata edges when the same-named symbol resolves to multiple class-like definitions in scope (so ambiguous `[MyAudit]` usage is left to `impact` / `references` to disambiguate rather than being fanned out to every same-named attribute class). The existing `GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies` test (which pins `[JsonConverter(...)]` → `class JsonConverter : Attribute` for a target class whose name does NOT end in `Attribute`) still passes because only one class-like target exists in scope, so `target_ambiguity` keeps it, the `is_metadata` / class-like restriction accepts it, and `metadata_raw_suppression` finds no synthetic `JsonConverterAttribute` alias target and therefore does not suppress the raw row. Added `DbReaderTests.GetFileDependencies_CSharpAttributeRawDoesNotLeakToBareNameClass_WhenSuffixTargetExists` (raw `[MyAudit]` row is suppressed when the alias resolves to `class MyAuditAttribute`, so `[MyAudit]` does NOT depend on an unrelated plain `class MyAudit` file), `DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotLeakToSameNameMethodOrProperty` (fully-qualified `[MyAuditAttribute]` metadata does NOT depend on a file that only defines a method / property named `MyAuditAttribute`), and `DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotFanOutWhenMultipleSameNameAttributeClasses` (two same-named attribute classes in different namespaces cause the metadata edge to be dropped as ambiguous). Both the `impact` ambiguity guard (`IsMetadataTargetUnambiguous`) and the `deps` ambiguity CTE (`target_ambiguity`) are additionally tightened to count class-like definitions at symbol-identity level instead of path level: `IsMetadataTargetUnambiguous` now uses `SELECT DISTINCT f.path, s.line, s.name` so two same-named class-like definitions in one source file (e.g. idiomatic C# `namespace A { class FooAttribute { } } namespace B { class FooAttribute { } }` in a single .cs file) register as ambiguous; `target_ambiguity` joins `target_files` back through `files` + `symbols` with `COUNT(*)` over class-like symbol rows so the same same-file duplicate case correctly drops the `[Foo]` metadata edge. Path-level counting previously treated these same-file duplicates as count=1 because `SELECT DISTINCT f.path` and `target_files` (grouped by `dst.path`) both collapse the two definitions into a single row, so `cdidx deps` and `cdidx impact` were confidently reporting an ambiguous short-name metadata reference as a definite dependency / blast-radius edge — contradicting the design intent that "ambiguity" includes duplicates in one file. Added `DbReaderTests.GetFileDependencies_CSharpAttributeAmbiguityCountsSameFileDuplicateClassDefinitions` and `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CountsSameFileDuplicateDefinitions` to pin the symbol-identity-level counting for both `deps` and `impact`. `IsMetadataTargetUnambiguous` is additionally tightened to wrap `--path` / `--exclude-path` parameters through `EscapeLikeQuery` and bind them as `%...%` patterns, matching the LIKE semantics used by the rest of the reader (search / references / deps etc.); without the wrap the in-scope class-like count would underflow to 1 even when two same-named attribute classes sit side-by-side in the requested CLI subtree such as `--path src/A/`, and the metadata bypass would falsely fire. Added `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CliPathPatternEscaping_SuppressesWhenInScopeIsAmbiguous` to pin this. `CSharpNoArgAttributeRegex` is additionally widened to accept an optional generic-argument list after the attribute name (`(?:\s*<[^\n]+?>)?`) so generic no-arg attribute forms such as `[MyAudit]`, `[assembly: MyAttr]`, the multi-line `[\n MyAttr\n]`, and nested-generic forms like `[MyAttr>]` / `[MyOther>>]` are indexed as `attribute` references instead of falling through both `CallRegex` (requires `(`) and the old no-arg regex (which ended at `]` / `,` / EOL and therefore rejected `` between the name and `]`). The inner generic segment now uses a non-greedy `[^\n]+?` so nested `>` tokens (e.g. the closing `>` of an inner `Dictionary`) no longer terminate the match prematurely — the old `[^>\n]+` excluded `>` and therefore broke on the first inner `>`, dropping the reference row for nested-generic attribute usages. Without the widening those sites produced zero reference rows, so `deps` / `impact` / `references` / `inspect` / `analyze_symbol` all missed the dependency edge to the real `FooAttribute` class file. Added `ReferenceExtractorTests.Extract_CsharpGenericNoArgAttribute_ClassifiedAsAttribute`, `ReferenceExtractorTests.Extract_CsharpNestedGenericNoArgAttribute_ClassifiedAsAttribute`, `DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_StillIndexedAndResolvesToAttributeClass`, `DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_AssemblyTarget_IsIndexed`, and `DbReaderTests.GetFileDependencies_CSharpNestedGenericNoArgAttribute_ResolvesToAttributeClass` to pin the end-to-end behavior. Both the `impact` ambiguity guard (`IsMetadataTargetUnambiguous`) and the `deps` ambiguity path (`target_files` / `target_ambiguity`) are additionally tightened to treat a C# class as a valid `[Attr]` metadata target ONLY when its signature inherits from an `Attribute`-suffixed base class (`signature LIKE '%: %Attribute%'`). Plain `class MyAuditAttribute { }` without `: Attribute` inheritance is not a valid `[MyAudit]` target at compile time, so counting it as an ambiguity candidate would under-report the `impact` blast radius and suppress a real `deps` metadata edge when a same-named plain class coexists with the real attribute class. `target_files` now exposes a new `has_metadata_target_kind` MAX column that applies the same C# signature filter, and `metadata_raw_suppression` / `target_ambiguity` / the metadata clause in `edges` are all switched to `has_metadata_target_kind`; Java / Kotlin / Scala targets remain unaffected because their metadata markers cannot be captured with a single portable signature LIKE, so they keep the broader class-like candidate set. Added `DbReaderTests.GetFileDependencies_CSharp_PlainClassWithAttributeSuffixName_DoesNotCountAsAmbiguity` to pin the `deps` path. Round-17 follow-up loosens the C# metadata-target signature filter from `'%: %Attribute%'` (requires the immediate base class to end in `Attribute`) to `'%: %'` (any inheritance clause qualifies) so indirect inheritance like `class MyAuditAttribute : BaseAudit` where `BaseAudit : Attribute` is also recognized as a valid `[MyAudit]` target; transitive base-type resolution is not available at SQL time, so the inheritance-clause check is the best portable approximation — non-attribute derivations are over-accepted, but an invalid `[MyFoo]` against a non-attribute class would fail to compile and therefore does not appear as a real reference. The shared `BuildCSharpMetadataTargetFilter` helper additionally degrades to `1 = 1` (the pre-metadata-eligibility behavior) when the `symbols.signature` column is missing on legacy / read-only DBs where `TryMigrateForRead` could not add it, so `deps` / `impact` queries return results instead of crashing with `no such column: s.signature`. Added `DbReaderTests.GetFileDependencies_CSharp_IndirectAttributeInheritance_ResolvesAsMetadataTarget` to pin the indirect-inheritance path. Round-18 follow-up replaces the boolean `BuildCSharpMetadataTargetFilter` helper with a unified `BuildMetadataTargetKindExpr(fileAlias)` expression that narrows the metadata-target candidate kind per language: C# accepts only `kind = 'class'` with an inheritance clause (with the same legacy `1 = 1` fallback to class-only when `symbols.signature` is missing), JavaScript/TypeScript accept `class` / `struct` / `interface` / `function` (so function-decorator factories like `function sealed(target) { ... }` targeted by `@sealed` stay as valid `deps` edges), and other graph-supported languages keep the broader class/struct/interface candidate set. The old filter applied only the C# signature clause and left `s.kind IN ('class', 'struct', 'interface')` outside, which (a) wrongly counted `struct MyAuditAttribute` / `interface MyAuditAttribute` as C# metadata-target ambiguity candidates even though neither can derive from `System.Attribute`, and (b) dropped JS/TS decorator edges to a `function sealed` factory because `has_metadata_target_kind` required a class-like kind. The new expression is applied at three call sites: `IsMetadataTargetUnambiguous` (via a precomputed `metadataTargetKindExprF` local), the `target_files` CTE `has_metadata_target_kind` MAX column, and the `target_ambiguity` CTE JOIN condition (where the outer `s.kind IN (...)` narrowing is removed since the new expression already encodes the per-language kind rule). Added `DbReaderTests.GetFileDependencies_JavaScriptFunctionDecorator_ResolvesAsDependency` (JS `@sealed` class → `function sealed(target)` factory file now produces a deps edge instead of being dropped) and `DbReaderTests.GetFileDependencies_CSharp_SameNameInterface_DoesNotBlockMetadataEdge` (same-named `interface MyAuditAttribute` no longer ambiguates `[MyAudit]` → `class MyAuditAttribute : Attribute`). Round-19 follow-up additionally tightens the C# clause to accept rows whose `signature` is NULL when the column itself exists (`s.signature IS NULL OR s.signature LIKE '%: %'`): the previous `s.signature IS NOT NULL AND s.signature LIKE '%: %'` rejected every row on a DB whose schema was migrated in place by `TryMigrateForRead` without reindexing (the column is added but existing rows keep NULL signatures), so real `[MyAudit]` → `class MyAuditAttribute : System.Attribute` edges silently disappeared from `deps` / `impact` until the user re-ran `cdidx index`. Matching the spirit of the column-missing `1 = 1` fallback, NULL signatures are now treated as eligible instead of being rejected outright. Added `DbReaderTests.GetFileDependencies_CSharp_LegacyDbWithNullSignature_StillResolvesAttributeEdge` (NULL-signature attribute class still resolves the `deps` edge). The remaining edge case — a non-attribute class with the same name as a real attribute class silently suppressing the real edge through over-broad `LIKE '%: %'` matching — is tracked as #435 and requires a schema-level `is_metadata_target` column to resolve cleanly, so it is deliberately deferred out of this PR to keep the #293 scope focused on the already-landed attribute / annotation classification work. Affected: `src/CodeIndex/Database/DbReader.cs`, `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. - **MCP `callers` / `callees` `kind` schema, CLI help, and README `--kind` option row no longer advertise metadata kinds as valid filter values (#293 follow-up)** — The `callers` / `callees` handlers reject `kind: "attribute"` / `kind: "annotation"` with a usage error (CLI) / `isError` tool response (MCP), but the MCP tool schema's `kind` property description still listed all five kinds as if they were valid filter values (`"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"`), `cdidx --help` similarly lumped `callers` / `callees` together with `references` for the `--kind` help row, and the README `--kind` option table (EN line 436 / JP line 1307) still directed readers to pass `--kind attribute` or `--kind annotation` on `callers` / `callees`. Schema and handler now agree: the `callers` / `callees` `kind` description now reads `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."`, the CLI `--kind` help row splits `references` (accepts all five kinds) from `callers` / `callees` (call-graph kinds only, metadata rejected, redirects to `references`), and the README `--kind` row (both English and Japanese) now explicitly states that `callers` / `callees` reject `--kind attribute|annotation` with a usage error and points to `references --kind attribute|annotation` as the correct metadata-enumeration path. The MCP `references` tool description is unchanged because it still accepts every indexed kind. Added `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` to pin the corrected schema wording. Affected: `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `src/CodeIndex/Cli/ConsoleUi.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `README.md`. - **`deps`, `impact`, `references`, `inspect`, and `analyze_symbol` retain metadata references as compile-time dependency edges and canonicalize the C# attribute-suffix convention (#293 follow-up)** — `DbReader.GetFileDependencies` (backing `cdidx deps`) and `DbReader.GetFileDependencyHintsToResolvedType` (backing `cdidx impact` / MCP `impact_analysis` heuristic file-hint fallback) keep metadata references in their logical reference CTE. Renaming or removing an attribute class still breaks annotated sites such as `[JsonConverterAttribute]` at compile time, so file-level blast-radius analysis must surface those edges as real dependencies — now labeled `attribute` / `annotation` in `logical_reference_kind` (previously these same sites leaked in as phantom `call` rows before classification was corrected). `GetFileDependencies` additionally canonicalizes the C# attribute naming convention: idiomatic `[Foo]` usage is stored with `symbol_name = "Foo"` but the defining class is `FooAttribute`, so the logical-reference CTE now UNION-ALLs a suffix-aliased copy of every C# `attribute`-kind row with `'Attribute'` appended (only when the original symbol_name does not already end in `Attribute`) so the target-file join reaches the attribute class file. `GetFileDependencyHintsToResolvedType` also aggregates a per-candidate `has_metadata_ref` flag and bypasses the `SourceFileHasStructuredTypeEvidence` guard for edges whose contributing references are purely metadata, so pure-attribute consumer files such as `[MyAudit] class Svc` now surface through `impact MyAuditAttribute` instead of being filtered out. `ResolveImpactFallbackNames` symmetrically adds suffix-stripped aliases for C# class names ending in `Attribute`, so the BFS-less file-hint path can find `[Foo]` sites whose source symbol_name does not match the defining class name. The same canonicalization is propagated to the symbol-level reference reader: `DbReader.SearchReferences`, `CountSearchReferences`, and `CountSearchReferencesTotal` (backing `references`, `inspect`, MCP `analyze_symbol`) now compute a C#-only `queryAttributeAlias` (defined in the new `ComputeCSharpAttributeSuffixAlias` helper) and OR it into both exact (folded and NOCASE) and substring WHERE clauses, so `references MyAuditAttribute` (and `inspect MyAuditAttribute` / `analyze_symbol MyAuditAttribute`) surface the idiomatic `[MyAudit]` call site consistently with `deps` / `impact`. Substring mode uses an exact-OR for the alias (not a second LIKE) so user queries like `MyAuditAttribute` do not over-match unrelated names such as `FooAuditLog`. The alias is C# only, so Java / Kotlin / Scala / TypeScript annotation sites remain unaffected. `callers` / `callees` intentionally skip the alias because they filter to the shared call-graph kinds (`call`, `instantiate`, `subscribe`) and reject `--kind attribute|annotation` at the CLI / MCP boundary — those commands model the dynamic call graph, not the dependency graph. The alias branch is additionally scoped so it cannot produce false positives: `ComputeCSharpAttributeSuffixAlias` returns `null` when `referenceKind` is a non-`attribute` call-graph kind (so `references FooAttribute --kind call` stays strict and does not match `Foo()`), and the SQL alias disjunct in all three reader methods is clamped to `f.lang = 'csharp' AND r.reference_kind = 'attribute'` so an unscoped `references FooAttribute` (no `--lang` / no `--kind`) still cannot bleed into Java annotations or C# call rows. Suffix detection is also case-insensitive (`OrdinalIgnoreCase`) so `references myauditattribute` and `inspect MyAuditATTRIBUTE` still produce the `MyAudit` alias, consistent with the NOCASE / folded contract of the surrounding exact / substring query paths. Added regression coverage `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies` (runtime + metadata edges both surface), `DbReaderTests.GetFileDependencies_MatchesCSharpAttributeSuffixConvention` (`[MyAudit]` → `MyAuditAttribute` canonicalization through `deps`), `DbReaderTests.SearchReferences_MatchesCSharpAttributeSuffixConvention_Substring` / `_Exact` / `_CSharpAttributeSuffixAliasDoesNotBleedToOtherLanguages` (same canonicalization through `references` / `inspect` / `analyze_symbol`, plus C#-only scope guard), `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_NotAppliedToCallKind` (alias must not fire under `--kind call`), `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_UnscopedLangStillLimitsToCSharpAttributeRows` (unscoped query must not match Java annotations or C# call rows), and `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_CaseInsensitiveQuery` (lowercase / mixed-case queries still produce the alias). Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. - **`callers` / `callees` now reject `--kind attribute|annotation` at the CLI and MCP boundary, and Swift / Gradle `@` metadata is reclassified as `annotation` (#293 follow-up)** — Metadata rows (`attribute` / `annotation`) are attributed to their enclosing body-range symbol rather than the annotated target, so `callers Obsolete --kind attribute` would silently return `[Obsolete] void M()` under the enclosing class (not `M`), and file-level targets such as `[assembly: CLSCompliant]` or Kotlin `@file:JvmName` would drop entirely because `container_name` is `NULL`. `QueryCommandRunner.RunCallers` / `RunCallees` now reject `--kind attribute` and `--kind annotation` with `CommandExitCodes.UsageError`, and the MCP `callers` / `callees` tools return an `isError` tool response; both paths direct users to `references --kind attribute|annotation`, which IS a correct metadata enumeration path. Separately, `AnnotationLanguages` now includes `swift` and `gradle`, so Swift `@available(iOS 13.0, *)` / `@objc` / `@MainActor` and Gradle/Groovy `@CompileStatic` / `@TaskAction` are recorded as `annotation` instead of `call` (previously the `@Name(args)` forms leaked into `callers` / `callees` / `hotspots` / `impact` as phantom call edges, and the no-arg `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` forms dropped from the index entirely because no-arg annotation emission was gated to languages in this set). Added `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError` (theory, 4 cases), `McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError` (theory, 4 cases), `ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`, and `ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation`. Affected: `src/CodeIndex/Cli/QueryCommandRunner.cs`, `src/CodeIndex/Mcp/McpToolHandlers.cs`, `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. @@ -686,7 +686,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **DEVELOPER_GUIDE が SQLite の位置付けとリリース番号の例示を明確化** — cdidx が「設定ゼロ・単一ファイル・本番依存1個」の CLI であり、その唯一の本番依存が `Microsoft.Data.Sqlite` であることを明記した。あわせて、リリース手順のチェックリスト中に出てくる `1.9.0` は固定値ではなく例示であり、実際のリリース番号へ読み替える前提だと明示した。対象: `DEVELOPER_GUIDE.md`。 #### 修正 -- **`deps` の C# attribute サフィックス別名を class-like ターゲットに限定し、`impact` の metadata bypass も class-like ターゲット名が名前空間をまたいで曖昧な場合は抑止するよう修正 (#293 追加対応)** — 直前の #293 追加対応では、`deps` の logical-reference CTE に C# 限定で `symbol_name || 'Attribute'` のサフィックス別名を UNION することで `[Foo]` の attribute consumer が `FooAttribute` クラスファイルへ到達するようにし、`impact`(`GetFileDependencyHintsToResolvedType`)も寄与参照に `attribute` / `annotation` が含まれていれば structured-evidence guard をバイパスするようにしていたが、どちらも広すぎた。`deps` 側では別名が純粋に名前だけで join していたため、`[Foo]` が同名の関数・プロパティ・変数(例: 無関係なヘルパーの `void MyAuditAttribute()`)を定義するファイルにも依存を張ってしまっていた。`impact` 側の metadata bypass も、対象名が一意に class-like 定義を指すかを確認していなかったため、`MyAuditAttribute` のような短い名前が複数の namespace に存在すると rename / remove の blast-radius が過剰報告されていた(`attribute` / `annotation` 参照行は短い名前しか保持できず曖昧性解消ができない)。`DbReader.GetFileDependencies` は合成された別名行に `is_attribute_alias = 1`(本物の行は `0`)をタグ付けし、`source_name_counts` では `MIN` でフラグを伝播し、`edges` CTE の join で別名 edge をターゲット側 kind が `class` / `struct` / `interface` のときだけに制限するようにした。`target_files` CTE も `DISTINCT (path, lang, name, kind)` から `GROUP BY (path, lang, name)` + `MAX(...)` の `has_class_like_kind` フラグに切り替えたため、class と同名関数(例: C# コンストラクタ `Target()` と `class Target`)を同じファイルで定義しているケースでもターゲット行が 2 本に増えず `deps` の参照数を水増ししない。`DbReader.GetFileDependencyHintsToResolvedType` は新ヘルパー `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` を呼び、*active な impact スコープ内* で同名の class-like 定義数を数える(graph 対応言語に加えて、呼び出し側の `--lang` / `--path` / `--exclude-path` / `--exclude-tests` を同じフィルタで適用)。in-scope でその数が 1 以下のときに限り metadata evidence-guard bypass を適用する。スコープ内で曖昧なときは metadata 単独 edge も通常の `SourceFileHasStructuredTypeEvidence` に戻り、純 `[MyAudit]` consumer は誤帰属せずに落ちる。スコープ尊重が重要: 別言語 / 別 path / test ツリーにある同名クラスが C# の metadata bypass を潰してはならない(参照側が既に graph 対応 join で言語修飾されているため、曖昧性は scope 内で判定すべき)。README の `impact` 契約も「active な `--lang` / `--path` / `--exclude-path` / `--exclude-tests` スコープ」と明記している。既存の C# attribute deps テスト `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`、曖昧ではない単一定義の `impact` テスト、および既存の graph-queries dedupe テストは全てグリーンのまま。回帰テストとして `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets`(同名関数があっても虚偽 edge が出ないこと)、`DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget`(impact スコープ内で `MyAuditAttribute` が複数ある場合に metadata heuristic hint を抑止)、`DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous`(単一クラス定義のみのときは従来どおり `[MyAudit]` consumer を浮かせる)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope`(別言語の同名 annotation は `--lang csharp` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope`(スコープ外 `src/B/` の同名クラスは `src/A/` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests`(test ツリーの同名クラスは `--exclude-tests` クエリを潰さない)を追加。`ResolveImpactFallbackNames` も併せて引き締め、C# の `Attribute` サフィックス別名は *解決済み定義自身* の名前にのみ適用するようにした: 以前は同一ファイル内の fallback 名全てに別名を付けていたため、`FooAttribute` を定義するファイルに nested で `BarAttribute` が存在するだけで `impact FooAttribute` が `[Bar]` 利用サイトを `FooAttribute` の影響範囲として誤報告していた。`DbReaderTests.GetFileDependencyHints_CSharpAttributeSuffixAlias_DoesNotLeakToSameFileSiblings` を追加して固定。さらに `GetFileDependencies` を再構成し、metadata 行(`attribute` / `annotation`)が bare-name の call-graph ターゲットに漏れないようにした: `logical_references` CTE に `is_metadata` フラグを付与し、`source_name_counts` の GROUP BY を `(source_file_id, source_path, source_lang, symbol_name, is_attribute_alias, is_metadata)` に拡張して metadata と非 metadata で同じ symbol_name の行を別グループ化する。`edges` CTE は全 metadata edge(合成別名だけでなく実体行も)に `tf.has_class_like_kind = 1` を要求し、さらに 2 つの CTE で残る誤検知を潰す: `metadata_raw_suppression` は同じ source file で合成別名 `FooAttribute` が class-like ターゲットに解決できているとき、生の C# `[Foo]` 行を落とす(`[Foo]` が `class FooAttribute` と無関係な同名 `class Foo` に fan-out しない)。`target_ambiguity` はスコープ内で同名 class-like ターゲットが複数あるとき metadata edge を落とす(曖昧な `[MyAudit]` は deps で fan-out させず、`impact` / `references` に判断を委ねる)。既存の `GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies`(`[JsonConverter(...)]` → `class JsonConverter : Attribute` をターゲット名に `Attribute` サフィックスがなくても固定)は、class-like ターゲットが 1 つしかないので `target_ambiguity` で残り、`is_metadata` / class-like 制限も受理し、合成 `JsonConverterAttribute` 別名は class-like に解決できないので `metadata_raw_suppression` も発火せず、そのまま通る。回帰テストとして `DbReaderTests.GetFileDependencies_CSharpAttributeRawDoesNotLeakToBareNameClass_WhenSuffixTargetExists`(別名が `class MyAuditAttribute` に解決できたとき生 `[MyAudit]` 行は抑止され、無関係な plain `class MyAudit` ファイルに依存しない)、`DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotLeakToSameNameMethodOrProperty`(完全形 `[MyAuditAttribute]` は同名メソッド / プロパティしか持たないファイルに依存しない)、`DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotFanOutWhenMultipleSameNameAttributeClasses`(別名前空間に同名 attribute クラスが 2 つあると metadata edge はあいまいとして落とす)を追加。加えて `impact` の曖昧性ガード (`IsMetadataTargetUnambiguous`) と `deps` の曖昧性 CTE (`target_ambiguity`) も、path 単位ではなく symbol identity 単位で class-like 定義を数えるように引き締めた: `IsMetadataTargetUnambiguous` は `SELECT DISTINCT f.path, s.line, s.name` に変更し、C# でよくある 1 つの .cs ファイル内に別名前空間で同名 class-like が 2 つ定義されている形 (`namespace A { class FooAttribute { } } namespace B { class FooAttribute { } }`) でも ambiguity を 2 として検出する。`target_ambiguity` は `target_files` を `files` + `symbols` に JOIN し直して `COUNT(*)` を class-like symbol 行に対して取るため、同じ同名ファイル内重複ケースでも `[Foo]` metadata edge を正しく落とせる。path 単位カウントでは `SELECT DISTINCT f.path` も `target_files`(`dst.path` で GROUP BY)も同一ファイル内の 2 定義を 1 行に潰してしまうため、従来は count=1 として曖昧性を取りこぼし、`cdidx deps` と `cdidx impact` が同名 short-name metadata 参照を確定的な依存 / blast-radius edge として出していた。これは曖昧性に "duplicates in one file" を含める前提の設計契約に反していた。`deps` と `impact` 両方で symbol-identity 単位カウントを固定する `DbReaderTests.GetFileDependencies_CSharpAttributeAmbiguityCountsSameFileDuplicateClassDefinitions` と `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CountsSameFileDuplicateDefinitions` を追加。さらに `IsMetadataTargetUnambiguous` の `--path` / `--exclude-path` パラメータを、他の reader 経路(search / references / deps 等)と同じ LIKE セマンティクスに揃えるため `EscapeLikeQuery` でエスケープしたうえで `%...%` で包んでバインドするようにした。生値のままだと `--path src/A/` のような CLI 形では LIKE が一致せず、リクエストされたサブツリーに同名 attribute クラスが 2 件並んでいても in-scope class-like カウントが 1 に過小化され、本来抑止すべき metadata bypass が誤発火してしまっていた。`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CliPathPatternEscaping_SuppressesWhenInScopeIsAmbiguous` を追加して固定する。あわせて `CSharpNoArgAttributeRegex` を拡張し、属性名の直後に任意のジェネリック引数リスト `(?:\s*<[^\n]+?>)?` を許容するようにしたため、`[MyAudit]`・`[assembly: MyAttr]`・複数行の `[\n MyAttr\n]`・ネストジェネリックの `[MyAttr>]` や `[MyOther>>]` のような引数なしジェネリック属性も `attribute` 参照としてインデックスされるようになった。内側ジェネリックセグメントは非貪欲な `[^\n]+?` に変更し、内側の `>`(例: `Dictionary` を閉じる `>`)で match が打ち切られないようにした — 従来の `[^>\n]+` は `>` を除外していたため最初の内側 `>` で崩れ、ネストジェネリック属性の参照行が落ちていた。従来は `CallRegex`(`(` 必須)でも旧 no-arg regex(`]` / `,` / EOL で終端しなければならず、名前と `]` の間に `` があると拒否)でも拾えず、参照行が 0 件となり、`deps` / `impact` / `references` / `inspect` / `analyze_symbol` が実属性クラス `FooAttribute` への依存エッジを全て取りこぼしていた。`ReferenceExtractorTests.Extract_CsharpGenericNoArgAttribute_ClassifiedAsAttribute`、`ReferenceExtractorTests.Extract_CsharpNestedGenericNoArgAttribute_ClassifiedAsAttribute`、`DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_StillIndexedAndResolvesToAttributeClass`、`DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_AssemblyTarget_IsIndexed`、`DbReaderTests.GetFileDependencies_CSharpNestedGenericNoArgAttribute_ResolvesToAttributeClass` を追加して end-to-end の振る舞いを固定する。加えて、`impact` の曖昧性ガード (`IsMetadataTargetUnambiguous`) と `deps` の曖昧性判定 (`target_files` / `target_ambiguity`) は、C# クラスが有効な `[Attr]` metadata target になるのは「signature が `Attribute` サフィックス付き基底を継承している」場合 (`signature LIKE '%: %Attribute%'`) に限定するよう引き締めた。`: Attribute` 継承を書いていない plain `class MyAuditAttribute { }` はコンパイル時に `[MyAudit]` の target にならないため、曖昧性候補として数えると `impact` の影響範囲が過少報告され、`deps` 側でも同名 plain クラスが共存する scene で実 attribute クラスへの metadata edge が落ちてしまう。`target_files` に新しい `has_metadata_target_kind` MAX 列を追加して同じ C# signature フィルタを適用し、`metadata_raw_suppression` / `target_ambiguity` / `edges` の metadata 節はすべて `has_metadata_target_kind` に切り替えた。Java / Kotlin / Scala は portable な signature LIKE で metadata marker を表現しづらいため従来通り class-like 全体を候補に残す。`deps` 側の挙動を固定する `DbReaderTests.GetFileDependencies_CSharp_PlainClassWithAttributeSuffixName_DoesNotCountAsAmbiguity` を追加。Round-17 追加対応では、C# metadata target の signature フィルタを `'%: %Attribute%'`(直接基底が `Attribute` で終わることを要求)から `'%: %'`(何らかの継承節を持てば候補)に緩和した。`class MyAuditAttribute : BaseAudit` で `BaseAudit : Attribute` のような間接継承でも `[MyAudit]` の有効な target として認識される。SQL 時点で基底型を遡ることはできないため、継承節チェックは portable な近似として採用する — 非 attribute 由来のクラスは過剰に受理するが、無効な `[MyFoo]` をコンパイルすることはできず実参照にはならないので実害はない。共有ヘルパー `BuildCSharpMetadataTargetFilter` はさらに、`TryMigrateForRead` が追加できなかった legacy / read-only DB で `symbols.signature` 列が欠落しているときは `1 = 1`(metadata-eligibility 導入前の挙動)へ degrade するので、`deps` / `impact` クエリは `no such column: s.signature` でクラッシュせず結果を返す。間接継承パスを固定する `DbReaderTests.GetFileDependencies_CSharp_IndirectAttributeInheritance_ResolvesAsMetadataTarget` を追加。Round-18 では boolean の `BuildCSharpMetadataTargetFilter` ヘルパーを、ファイル別エイリアスを取り言語ごとに metadata-target 候補 kind を絞る `BuildMetadataTargetKindExpr(fileAlias)` 式へ置き換える。C# は継承節を持つ `kind = 'class'` のみ許可(`symbols.signature` が欠落する legacy DB では従来通り class-only にフォールバック)、JavaScript / TypeScript は `class` / `struct` / `interface` / `function` を許可(`function sealed(target) { ... }` のような factory 関数を `@sealed` が target とする decorator edge が `deps` に残る)、その他の graph 対応言語は従来通り `class` / `struct` / `interface` を受ける。旧フィルタは C# の signature 句だけを適用し、`s.kind IN ('class', 'struct', 'interface')` が外側に残っていたため、(a) C# で `struct MyAuditAttribute` / `interface MyAuditAttribute` が `System.Attribute` を継承できないにも関わらず metadata-target の ambiguity 候補として数えられ、(b) JS/TS で decorator の対象が `function sealed` factory の場合に `has_metadata_target_kind` が class-like を要求するために deps edge が落ちていた。新しい式は 3 箇所で使う: `IsMetadataTargetUnambiguous`(事前計算の `metadataTargetKindExprF` 経由)、`target_files` CTE の `has_metadata_target_kind` MAX 列、`target_ambiguity` CTE の JOIN 条件(言語ごとの kind narrowing が式に内包されたため、外側の `s.kind IN (...)` 絞り込みは削除)。`DbReaderTests.GetFileDependencies_JavaScriptFunctionDecorator_ResolvesAsDependency`(JS の `@sealed` class → `function sealed(target)` factory ファイルが落ちずに deps edge を生成)と `DbReaderTests.GetFileDependencies_CSharp_SameNameInterface_DoesNotBlockMetadataEdge`(同名の `interface MyAuditAttribute` が `[MyAudit]` → `class MyAuditAttribute : Attribute` を ambiguity 扱いにして抑止しない)を追加。対象: `src/CodeIndex/Database/DbReader.cs`、`src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 +- **`deps` の C# attribute サフィックス別名を class-like ターゲットに限定し、`impact` の metadata bypass も class-like ターゲット名が名前空間をまたいで曖昧な場合は抑止するよう修正 (#293 追加対応)** — 直前の #293 追加対応では、`deps` の logical-reference CTE に C# 限定で `symbol_name || 'Attribute'` のサフィックス別名を UNION することで `[Foo]` の attribute consumer が `FooAttribute` クラスファイルへ到達するようにし、`impact`(`GetFileDependencyHintsToResolvedType`)も寄与参照に `attribute` / `annotation` が含まれていれば structured-evidence guard をバイパスするようにしていたが、どちらも広すぎた。`deps` 側では別名が純粋に名前だけで join していたため、`[Foo]` が同名の関数・プロパティ・変数(例: 無関係なヘルパーの `void MyAuditAttribute()`)を定義するファイルにも依存を張ってしまっていた。`impact` 側の metadata bypass も、対象名が一意に class-like 定義を指すかを確認していなかったため、`MyAuditAttribute` のような短い名前が複数の namespace に存在すると rename / remove の blast-radius が過剰報告されていた(`attribute` / `annotation` 参照行は短い名前しか保持できず曖昧性解消ができない)。`DbReader.GetFileDependencies` は合成された別名行に `is_attribute_alias = 1`(本物の行は `0`)をタグ付けし、`source_name_counts` では `MIN` でフラグを伝播し、`edges` CTE の join で別名 edge をターゲット側 kind が `class` / `struct` / `interface` のときだけに制限するようにした。`target_files` CTE も `DISTINCT (path, lang, name, kind)` から `GROUP BY (path, lang, name)` + `MAX(...)` の `has_class_like_kind` フラグに切り替えたため、class と同名関数(例: C# コンストラクタ `Target()` と `class Target`)を同じファイルで定義しているケースでもターゲット行が 2 本に増えず `deps` の参照数を水増ししない。`DbReader.GetFileDependencyHintsToResolvedType` は新ヘルパー `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` を呼び、*active な impact スコープ内* で同名の class-like 定義数を数える(graph 対応言語に加えて、呼び出し側の `--lang` / `--path` / `--exclude-path` / `--exclude-tests` を同じフィルタで適用)。in-scope でその数が 1 以下のときに限り metadata evidence-guard bypass を適用する。スコープ内で曖昧なときは metadata 単独 edge も通常の `SourceFileHasStructuredTypeEvidence` に戻り、純 `[MyAudit]` consumer は誤帰属せずに落ちる。スコープ尊重が重要: 別言語 / 別 path / test ツリーにある同名クラスが C# の metadata bypass を潰してはならない(参照側が既に graph 対応 join で言語修飾されているため、曖昧性は scope 内で判定すべき)。README の `impact` 契約も「active な `--lang` / `--path` / `--exclude-path` / `--exclude-tests` スコープ」と明記している。既存の C# attribute deps テスト `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`、曖昧ではない単一定義の `impact` テスト、および既存の graph-queries dedupe テストは全てグリーンのまま。回帰テストとして `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets`(同名関数があっても虚偽 edge が出ないこと)、`DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget`(impact スコープ内で `MyAuditAttribute` が複数ある場合に metadata heuristic hint を抑止)、`DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous`(単一クラス定義のみのときは従来どおり `[MyAudit]` consumer を浮かせる)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope`(別言語の同名 annotation は `--lang csharp` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope`(スコープ外 `src/B/` の同名クラスは `src/A/` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests`(test ツリーの同名クラスは `--exclude-tests` クエリを潰さない)を追加。`ResolveImpactFallbackNames` も併せて引き締め、C# の `Attribute` サフィックス別名は *解決済み定義自身* の名前にのみ適用するようにした: 以前は同一ファイル内の fallback 名全てに別名を付けていたため、`FooAttribute` を定義するファイルに nested で `BarAttribute` が存在するだけで `impact FooAttribute` が `[Bar]` 利用サイトを `FooAttribute` の影響範囲として誤報告していた。`DbReaderTests.GetFileDependencyHints_CSharpAttributeSuffixAlias_DoesNotLeakToSameFileSiblings` を追加して固定。さらに `GetFileDependencies` を再構成し、metadata 行(`attribute` / `annotation`)が bare-name の call-graph ターゲットに漏れないようにした: `logical_references` CTE に `is_metadata` フラグを付与し、`source_name_counts` の GROUP BY を `(source_file_id, source_path, source_lang, symbol_name, is_attribute_alias, is_metadata)` に拡張して metadata と非 metadata で同じ symbol_name の行を別グループ化する。`edges` CTE は全 metadata edge(合成別名だけでなく実体行も)に `tf.has_class_like_kind = 1` を要求し、さらに 2 つの CTE で残る誤検知を潰す: `metadata_raw_suppression` は同じ source file で合成別名 `FooAttribute` が class-like ターゲットに解決できているとき、生の C# `[Foo]` 行を落とす(`[Foo]` が `class FooAttribute` と無関係な同名 `class Foo` に fan-out しない)。`target_ambiguity` はスコープ内で同名 class-like ターゲットが複数あるとき metadata edge を落とす(曖昧な `[MyAudit]` は deps で fan-out させず、`impact` / `references` に判断を委ねる)。既存の `GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies`(`[JsonConverter(...)]` → `class JsonConverter : Attribute` をターゲット名に `Attribute` サフィックスがなくても固定)は、class-like ターゲットが 1 つしかないので `target_ambiguity` で残り、`is_metadata` / class-like 制限も受理し、合成 `JsonConverterAttribute` 別名は class-like に解決できないので `metadata_raw_suppression` も発火せず、そのまま通る。回帰テストとして `DbReaderTests.GetFileDependencies_CSharpAttributeRawDoesNotLeakToBareNameClass_WhenSuffixTargetExists`(別名が `class MyAuditAttribute` に解決できたとき生 `[MyAudit]` 行は抑止され、無関係な plain `class MyAudit` ファイルに依存しない)、`DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotLeakToSameNameMethodOrProperty`(完全形 `[MyAuditAttribute]` は同名メソッド / プロパティしか持たないファイルに依存しない)、`DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotFanOutWhenMultipleSameNameAttributeClasses`(別名前空間に同名 attribute クラスが 2 つあると metadata edge はあいまいとして落とす)を追加。加えて `impact` の曖昧性ガード (`IsMetadataTargetUnambiguous`) と `deps` の曖昧性 CTE (`target_ambiguity`) も、path 単位ではなく symbol identity 単位で class-like 定義を数えるように引き締めた: `IsMetadataTargetUnambiguous` は `SELECT DISTINCT f.path, s.line, s.name` に変更し、C# でよくある 1 つの .cs ファイル内に別名前空間で同名 class-like が 2 つ定義されている形 (`namespace A { class FooAttribute { } } namespace B { class FooAttribute { } }`) でも ambiguity を 2 として検出する。`target_ambiguity` は `target_files` を `files` + `symbols` に JOIN し直して `COUNT(*)` を class-like symbol 行に対して取るため、同じ同名ファイル内重複ケースでも `[Foo]` metadata edge を正しく落とせる。path 単位カウントでは `SELECT DISTINCT f.path` も `target_files`(`dst.path` で GROUP BY)も同一ファイル内の 2 定義を 1 行に潰してしまうため、従来は count=1 として曖昧性を取りこぼし、`cdidx deps` と `cdidx impact` が同名 short-name metadata 参照を確定的な依存 / blast-radius edge として出していた。これは曖昧性に "duplicates in one file" を含める前提の設計契約に反していた。`deps` と `impact` 両方で symbol-identity 単位カウントを固定する `DbReaderTests.GetFileDependencies_CSharpAttributeAmbiguityCountsSameFileDuplicateClassDefinitions` と `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CountsSameFileDuplicateDefinitions` を追加。さらに `IsMetadataTargetUnambiguous` の `--path` / `--exclude-path` パラメータを、他の reader 経路(search / references / deps 等)と同じ LIKE セマンティクスに揃えるため `EscapeLikeQuery` でエスケープしたうえで `%...%` で包んでバインドするようにした。生値のままだと `--path src/A/` のような CLI 形では LIKE が一致せず、リクエストされたサブツリーに同名 attribute クラスが 2 件並んでいても in-scope class-like カウントが 1 に過小化され、本来抑止すべき metadata bypass が誤発火してしまっていた。`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CliPathPatternEscaping_SuppressesWhenInScopeIsAmbiguous` を追加して固定する。あわせて `CSharpNoArgAttributeRegex` を拡張し、属性名の直後に任意のジェネリック引数リスト `(?:\s*<[^\n]+?>)?` を許容するようにしたため、`[MyAudit]`・`[assembly: MyAttr]`・複数行の `[\n MyAttr\n]`・ネストジェネリックの `[MyAttr>]` や `[MyOther>>]` のような引数なしジェネリック属性も `attribute` 参照としてインデックスされるようになった。内側ジェネリックセグメントは非貪欲な `[^\n]+?` に変更し、内側の `>`(例: `Dictionary` を閉じる `>`)で match が打ち切られないようにした — 従来の `[^>\n]+` は `>` を除外していたため最初の内側 `>` で崩れ、ネストジェネリック属性の参照行が落ちていた。従来は `CallRegex`(`(` 必須)でも旧 no-arg regex(`]` / `,` / EOL で終端しなければならず、名前と `]` の間に `` があると拒否)でも拾えず、参照行が 0 件となり、`deps` / `impact` / `references` / `inspect` / `analyze_symbol` が実属性クラス `FooAttribute` への依存エッジを全て取りこぼしていた。`ReferenceExtractorTests.Extract_CsharpGenericNoArgAttribute_ClassifiedAsAttribute`、`ReferenceExtractorTests.Extract_CsharpNestedGenericNoArgAttribute_ClassifiedAsAttribute`、`DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_StillIndexedAndResolvesToAttributeClass`、`DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_AssemblyTarget_IsIndexed`、`DbReaderTests.GetFileDependencies_CSharpNestedGenericNoArgAttribute_ResolvesToAttributeClass` を追加して end-to-end の振る舞いを固定する。加えて、`impact` の曖昧性ガード (`IsMetadataTargetUnambiguous`) と `deps` の曖昧性判定 (`target_files` / `target_ambiguity`) は、C# クラスが有効な `[Attr]` metadata target になるのは「signature が `Attribute` サフィックス付き基底を継承している」場合 (`signature LIKE '%: %Attribute%'`) に限定するよう引き締めた。`: Attribute` 継承を書いていない plain `class MyAuditAttribute { }` はコンパイル時に `[MyAudit]` の target にならないため、曖昧性候補として数えると `impact` の影響範囲が過少報告され、`deps` 側でも同名 plain クラスが共存する scene で実 attribute クラスへの metadata edge が落ちてしまう。`target_files` に新しい `has_metadata_target_kind` MAX 列を追加して同じ C# signature フィルタを適用し、`metadata_raw_suppression` / `target_ambiguity` / `edges` の metadata 節はすべて `has_metadata_target_kind` に切り替えた。Java / Kotlin / Scala は portable な signature LIKE で metadata marker を表現しづらいため従来通り class-like 全体を候補に残す。`deps` 側の挙動を固定する `DbReaderTests.GetFileDependencies_CSharp_PlainClassWithAttributeSuffixName_DoesNotCountAsAmbiguity` を追加。Round-17 追加対応では、C# metadata target の signature フィルタを `'%: %Attribute%'`(直接基底が `Attribute` で終わることを要求)から `'%: %'`(何らかの継承節を持てば候補)に緩和した。`class MyAuditAttribute : BaseAudit` で `BaseAudit : Attribute` のような間接継承でも `[MyAudit]` の有効な target として認識される。SQL 時点で基底型を遡ることはできないため、継承節チェックは portable な近似として採用する — 非 attribute 由来のクラスは過剰に受理するが、無効な `[MyFoo]` をコンパイルすることはできず実参照にはならないので実害はない。共有ヘルパー `BuildCSharpMetadataTargetFilter` はさらに、`TryMigrateForRead` が追加できなかった legacy / read-only DB で `symbols.signature` 列が欠落しているときは `1 = 1`(metadata-eligibility 導入前の挙動)へ degrade するので、`deps` / `impact` クエリは `no such column: s.signature` でクラッシュせず結果を返す。間接継承パスを固定する `DbReaderTests.GetFileDependencies_CSharp_IndirectAttributeInheritance_ResolvesAsMetadataTarget` を追加。Round-18 では boolean の `BuildCSharpMetadataTargetFilter` ヘルパーを、ファイル別エイリアスを取り言語ごとに metadata-target 候補 kind を絞る `BuildMetadataTargetKindExpr(fileAlias)` 式へ置き換える。C# は継承節を持つ `kind = 'class'` のみ許可(`symbols.signature` が欠落する legacy DB では従来通り class-only にフォールバック)、JavaScript / TypeScript は `class` / `struct` / `interface` / `function` を許可(`function sealed(target) { ... }` のような factory 関数を `@sealed` が target とする decorator edge が `deps` に残る)、その他の graph 対応言語は従来通り `class` / `struct` / `interface` を受ける。旧フィルタは C# の signature 句だけを適用し、`s.kind IN ('class', 'struct', 'interface')` が外側に残っていたため、(a) C# で `struct MyAuditAttribute` / `interface MyAuditAttribute` が `System.Attribute` を継承できないにも関わらず metadata-target の ambiguity 候補として数えられ、(b) JS/TS で decorator の対象が `function sealed` factory の場合に `has_metadata_target_kind` が class-like を要求するために deps edge が落ちていた。新しい式は 3 箇所で使う: `IsMetadataTargetUnambiguous`(事前計算の `metadataTargetKindExprF` 経由)、`target_files` CTE の `has_metadata_target_kind` MAX 列、`target_ambiguity` CTE の JOIN 条件(言語ごとの kind narrowing が式に内包されたため、外側の `s.kind IN (...)` 絞り込みは削除)。`DbReaderTests.GetFileDependencies_JavaScriptFunctionDecorator_ResolvesAsDependency`(JS の `@sealed` class → `function sealed(target)` factory ファイルが落ちずに deps edge を生成)と `DbReaderTests.GetFileDependencies_CSharp_SameNameInterface_DoesNotBlockMetadataEdge`(同名の `interface MyAuditAttribute` が `[MyAudit]` → `class MyAuditAttribute : Attribute` を ambiguity 扱いにして抑止しない)を追加。Round-19 の追加では、C# 句を `s.signature IS NULL OR s.signature LIKE '%: %'` に緩め、`signature` 列は存在するが row の値が NULL の DB(`TryMigrateForRead` でその場 migration された列を持つが再インデックスを行っていない DB。既存行は NULL のまま)でも degrade するようにする。旧 `s.signature IS NOT NULL AND s.signature LIKE '%: %'` ではそうした DB で全行が reject され、本物の `[MyAudit]` → `class MyAuditAttribute : System.Attribute` edge がユーザーが `cdidx index` を再実行するまで silent に `deps` / `impact` から消えていた。列欠落時の `1 = 1` fallback と同じ精神で NULL signature を eligible 扱いにする。`DbReaderTests.GetFileDependencies_CSharp_LegacyDbWithNullSignature_StillResolvesAttributeEdge`(NULL signature の attribute class でも deps edge が解決する)を追加。残る edge case — 真の attribute class と同名の non-attribute class が `LIKE '%: %'` の過剰マッチで実 edge を silent に抑止する問題 — は schema レベルの `is_metadata_target` 列が必要になるため、#293 PR の scope を広げないために #435 で追跡する別 issue として意図的に deferred。対象: `src/CodeIndex/Database/DbReader.cs`、`src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 - **MCP の `callers` / `callees` の `kind` schema・CLI help・README の `--kind` オプション行が metadata kinds を有効フィルタ値として広告しないよう修正 (#293 追加対応)** — `callers` / `callees` の handler は `kind: "attribute"` / `kind: "annotation"` を usage error(CLI)/ `isError` ツールレスポンス(MCP)で拒否するにもかかわらず、MCP ツール schema の `kind` プロパティ説明は 5 種すべてを列挙した `"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"` のままで、`cdidx --help` の `--kind` 行も `callers` / `callees` を `references` と同じ行にまとめていた。さらに README の `--kind` オプション表(英語 436 行目 / 日本語 1307 行目)も、`callers` / `callees` で `--kind attribute` / `--kind annotation` を渡せるかのように案内していた。schema と handler の契約を一致させ、`callers` / `callees` の `kind` description を `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."` に修正し、CLI の `--kind` help 行も `references`(5 種すべて受理)と `callers` / `callees`(call-graph 種別のみ、metadata kinds は拒否して `references` へ誘導)を分けるようにし、README の `--kind` 行(英語・日本語の両方)も `callers` / `callees` が `--kind attribute|annotation` を usage error で拒否すること、metadata 列挙の正規経路が `references --kind attribute|annotation` であることを明示する文言に直した。MCP の `references` ツール説明は全 reference kind を受け付ける契約のままなので変更なし。修正後の schema 文言を固定する `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` を追加。対象: `src/CodeIndex/Mcp/McpToolDefinitions.cs`、`src/CodeIndex/Cli/ConsoleUi.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`、`README.md`。 - **`deps`・`impact`・`references`・`inspect`・`analyze_symbol` が metadata 参照を compile-time 依存として保持し、C# の attribute サフィックス命名規約を正規化 (#293 追加対応)** — `cdidx deps` の実装である `DbReader.GetFileDependencies` と、`cdidx impact` / MCP `impact_analysis` の heuristic file-hint フォールバックである `DbReader.GetFileDependencyHintsToResolvedType` は、logical reference CTE に metadata 参照も含める。attribute クラスを rename / 削除すれば `[JsonConverterAttribute]` のような注釈サイトも compile-time で壊れるため、ファイル単位の blast-radius 分析はそれらも本物の依存 edge として表示する必要があり、`logical_reference_kind` では `attribute` / `annotation` として正しくラベル付けされる(分類修正前は同じサイトが phantom `call` として漏れていた)。さらに `GetFileDependencies` は C# の attribute 命名規約を正規化する: 慣用的な `[Foo]` は `symbol_name = "Foo"` で保存されるが、定義クラスは `FooAttribute` 命名となるため、logical-reference CTE に C# `attribute` 種別の行をサフィックス `'Attribute'` を付与した別名コピーとして UNION ALL で追加する(元の symbol_name が既に `Attribute` で終わっている場合は重複しないようスキップ)。これにより target ファイル join が attribute クラス側のファイルへ到達する。`GetFileDependencyHintsToResolvedType` はさらに candidate ごとに `has_metadata_ref` フラグを集約し、寄与する参照が metadata だけである edge については `SourceFileHasStructuredTypeEvidence` guard をバイパスするため、`[MyAudit] class Svc` のような純 attribute consumer ファイルも `impact MyAuditAttribute` で出るようになった(以前は filter で落ちていた)。`ResolveImpactFallbackNames` も対称的に、`Attribute` で終わる C# クラス名にはサフィックスを外した別名を追加するため、BFS を使わない file-hint 経路でも source symbol_name が定義クラス名と一致しない `[Foo]` サイトを発見できる。同じ正規化をシンボル単位の参照 reader にも伝播する: `references`・`inspect`・MCP `analyze_symbol` を支える `DbReader.SearchReferences`・`CountSearchReferences`・`CountSearchReferencesTotal` は、新しいヘルパー `ComputeCSharpAttributeSuffixAlias` で C# 限定の `queryAttributeAlias` を計算し、exact(folded と NOCASE)・substring の両方の WHERE 句に OR で追加する。これにより `references MyAuditAttribute`(および `inspect MyAuditAttribute` / `analyze_symbol MyAuditAttribute`)でも慣用的な `[MyAudit]` 参照サイトが `deps` / `impact` と一貫して surface する。substring モードでは alias を二つ目の LIKE にせず exact-OR で束ねているため、ユーザーが `MyAuditAttribute` と打った時に `FooAuditLog` のような無関係な名前に over-match しない。alias は C# 限定なので、Java / Kotlin / Scala / TypeScript の annotation サイトには影響しない。`callers` / `callees` は共有の call-graph 種別 (`call` / `instantiate` / `subscribe`) に絞って `--kind attribute|annotation` を CLI / MCP 境界で拒否する契約なので、alias は意図的に適用しない(dynamic call graph と dependency graph は別契約)。さらに alias 節が誤一致を生まないようスコープする: `ComputeCSharpAttributeSuffixAlias` は `referenceKind` が `attribute` 以外の call-graph 種別のときは `null` を返す(`references FooAttribute --kind call` は厳格一致のままで `Foo()` に一致しない)、3 つの reader メソッドの SQL alias 節は `f.lang = 'csharp' AND r.reference_kind = 'attribute'` に限定するため、`--lang` / `--kind` 無指定の `references FooAttribute` でも Java の annotation や C# の call 行を拾わない。suffix 検出も `OrdinalIgnoreCase` にして case-insensitive 化したため、`references myauditattribute` / `inspect MyAuditATTRIBUTE` のような小文字・混在ケースでも alias `MyAudit` を生成でき、周辺の NOCASE / folded 契約と整合する。回帰テスト `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies`(runtime と metadata の両方の edge が出ることを固定)、`DbReaderTests.GetFileDependencies_MatchesCSharpAttributeSuffixConvention`(`deps` 経由での `[MyAudit]` → `MyAuditAttribute` 正規化)、`DbReaderTests.SearchReferences_MatchesCSharpAttributeSuffixConvention_Substring` / `_Exact` / `_CSharpAttributeSuffixAliasDoesNotBleedToOtherLanguages`(`references` / `inspect` / `analyze_symbol` での同一正規化と C# 限定スコープ)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_NotAppliedToCallKind`(`--kind call` で alias が発火しないこと)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_UnscopedLangStillLimitsToCSharpAttributeRows`(無指定でも Java annotation や C# call 行に一致しないこと)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_CaseInsensitiveQuery`(小文字・混在ケースのクエリでも alias を生成)を追加。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 - **`callers` / `callees` が CLI と MCP 境界で `--kind attribute|annotation` を拒否し、Swift / Gradle の `@` metadata を `annotation` に再分類 (#293 追加対応)** — metadata 行 (`attribute` / `annotation`) は注釈対象そのものではなく body-range の外側シンボルに帰属するため、`callers Obsolete --kind attribute` は `[Obsolete] void M()` を `M` ではなく外側クラスの下に黙って返し、`[assembly: CLSCompliant]` や Kotlin の `@file:JvmName` のような file-level target は `container_name = NULL` で完全脱落していた。`QueryCommandRunner.RunCallers` / `RunCallees` は `--kind attribute` / `--kind annotation` を `CommandExitCodes.UsageError` で弾き、MCP の `callers` / `callees` ツールは `isError` ツールレスポンスを返すようにした。どちらの経路も、metadata 列挙の正しいパスである `references --kind attribute|annotation` へ誘導する。あわせて `AnnotationLanguages` に `swift` と `gradle` を追加し、Swift の `@available(iOS 13.0, *)` / `@objc` / `@MainActor` や Gradle / Groovy の `@CompileStatic` / `@TaskAction` を `call` ではなく `annotation` として記録するようにした(修正前は引数付きの `@Name(args)` 形は `callers` / `callees` / `hotspots` / `impact` に phantom call edge として漏れ、引数なしの `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` 形はこの集合で gate されていた no-arg annotation 出力から外れてインデックス自体に残らなかった)。回帰テストとして `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError`(theory 4 ケース)、`McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError`(theory 4 ケース)、`ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`、`ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation` を追加。対象: `src/CodeIndex/Cli/QueryCommandRunner.cs`、`src/CodeIndex/Mcp/McpToolHandlers.cs`、`src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 diff --git a/src/CodeIndex/Database/DbReader.cs b/src/CodeIndex/Database/DbReader.cs index 99c8a0ddc8..b183737b35 100644 --- a/src/CodeIndex/Database/DbReader.cs +++ b/src/CodeIndex/Database/DbReader.cs @@ -2599,15 +2599,27 @@ internal string GetFileColumnSql(string columnName, string? fallbackSql = null) // class-like declaration, so keep the original class-like candidate // set (`class` / `struct` / `interface`). // `deps` と `impact` で共有する言語別 metadata-target 適格性判定。 - // C# は class 限定 + 継承節チェック(signature 列欠落時は class 限定のみ)。 + // C# は class 限定 + 継承節チェック(signature 列欠落時、または row 側 signature が + // NULL の legacy-migration DB では class 限定のみへ縮退)。 // JS / TS は decorator が function factory も対象にするため function を許容。 // それ以外は従来どおり class-like を候補にする。 private string BuildMetadataTargetKindExpr(string fileAlias) { // C# clause — class only (interface/struct cannot be attribute targets). + // When the `signature` column exists but an individual row's signature is + // NULL (e.g. a DB whose schema was migrated in-place without reindexing), + // we cannot verify the inheritance clause, so we conservatively keep such + // rows eligible just like the column-missing `1 = 1` fallback. Otherwise a + // real `[MyAudit]` → `class MyAuditAttribute : System.Attribute` edge would + // silently disappear from `deps` / `impact` on legacy-migration DBs. // C# は class のみ(interface/struct は attribute target にできない)。 + // signature 列は存在するが row の signature が NULL のケース(その場 migration で列だけ + // 追加された legacy DB)では inheritance clause を検証できないため、列欠落時の + // `1 = 1` fallback と同じ扱いで eligibility を保守的に維持する。そうしないと + // 本物の `[MyAudit]` → `class MyAuditAttribute : System.Attribute` edge が + // legacy-migration DB で silent に落ちる。 var csharpClause = _symbolColumns.Contains("signature") - ? $"({fileAlias}.lang = 'csharp' AND s.kind = 'class' AND s.signature IS NOT NULL AND s.signature LIKE '%: %')" + ? $"({fileAlias}.lang = 'csharp' AND s.kind = 'class' AND (s.signature IS NULL OR s.signature LIKE '%: %'))" : $"({fileAlias}.lang = 'csharp' AND s.kind = 'class')"; // JS / TS clause — decorators target classes and factory functions. // JS / TS は decorator が class と factory function を対象にする。 diff --git a/tests/CodeIndex.Tests/DbReaderTests.cs b/tests/CodeIndex.Tests/DbReaderTests.cs index 93839f897c..978dae6f06 100644 --- a/tests/CodeIndex.Tests/DbReaderTests.cs +++ b/tests/CodeIndex.Tests/DbReaderTests.cs @@ -3359,6 +3359,55 @@ class Foo { d.TargetPath == "src/decorators.js"); } + [Fact] + public void GetFileDependencies_CSharp_LegacyDbWithNullSignature_StillResolvesAttributeEdge() + { + // issue #293 round-19: the metadata-target signature clause must degrade + // gracefully when the `symbols.signature` column exists but individual + // rows carry NULL values — the common shape of a DB whose schema was + // migrated in place (`TryMigrateForRead`) without reindexing. Requiring + // `signature LIKE '%: %'` would silently drop the real + // `[MyAudit]` → `class MyAuditAttribute : System.Attribute` edge there, + // so the clause must treat NULL signature as eligible (equivalent to the + // column-missing `1 = 1` fallback). + // issue #293 round-19: metadata-target の signature 句は、列は存在するが + // row の値が NULL の legacy-migration DB でも degrade する必要がある。 + // LIKE を強要すると本物の `[MyAudit]` edge が silent に落ちる。 + // 列欠落時の `1 = 1` fallback と同じく NULL も eligible にする。 + InsertIndexedFile("src/MyAuditAttribute.cs", "csharp", + """ + public sealed class MyAuditAttribute : System.Attribute + { + } + """); + InsertIndexedFile("src/Svc.cs", "csharp", + """ + [MyAudit] + public class Svc + { + } + """); + + // Simulate the partial-migration shape: signature column is present but the + // C# class row has a NULL signature, as if the schema were upgraded in place + // without re-running extraction. + // partial-migration の形を再現: signature 列はあるが C# class 行の signature が + // NULL の状態 — その場 schema 移行後に再抽出していない DB と同じ。 + using (var cmd = _db.Connection.CreateCommand()) + { + cmd.CommandText = "UPDATE symbols SET signature = NULL WHERE name = 'MyAuditAttribute' AND kind = 'class'"; + cmd.ExecuteNonQuery(); + } + + var deps = _reader.GetFileDependencies( + limit: 50, + lang: "csharp"); + + Assert.Contains(deps, d => + d.SourcePath == "src/Svc.cs" && + d.TargetPath == "src/MyAuditAttribute.cs"); + } + [Fact] public void GetFileDependencies_CSharp_SameNameInterface_DoesNotBlockMetadataEdge() { From 536f914627d4a4a0cff83f800a2229d3b82f7ffe Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 18 Apr 2026 14:34:58 +0000 Subject: [PATCH 30/30] Fix #293 narrow C# NULL-signature metadata candidate to Attribute naming and drop JS/TS interface Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 4 +- src/CodeIndex/Database/DbReader.cs | 88 +++++++++++++-------- tests/CodeIndex.Tests/DbReaderTests.cs | 101 +++++++++++++++++++++++++ 3 files changed, 158 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9160addc60..f498e9654d 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/), - **Developer Guide now distinguishes SQLite's role from "zero dependencies" and labels release numbers as examples** — Clarified that cdidx remains a zero-configuration, single-file CLI with exactly one production dependency (`Microsoft.Data.Sqlite`), and made the release-workflow checklist explicitly state that `1.9.0` is only an example version to substitute during an actual release. Affected: `DEVELOPER_GUIDE.md`. #### Fixed -- **`deps` C# attribute suffix alias now only matches class-like targets, and `impact` metadata bypass is suppressed when the class-like target name is ambiguous across namespaces (#293 follow-up)** — The earlier `#293` follow-up made `deps` UNION a C#-only `symbol_name || 'Attribute'` suffix alias into the logical-reference CTE so `[Foo]` attribute consumers could reach `FooAttribute` class files, and made `impact` (`GetFileDependencyHintsToResolvedType`) bypass the structured-evidence guard whenever any contributing reference was `attribute` / `annotation`. Both changes were too broad. On `deps` the alias joined purely by name, so `[Foo]` would also depend on any file that merely defined a function / property / variable named `FooAttribute` (e.g. `void MyAuditAttribute()` in an unrelated helper). On `impact` the metadata bypass did not check whether the target name was uniquely a class-like definition, so when the same unqualified name (e.g. `MyAuditAttribute`) existed in multiple namespaces the rename/remove blast radius was over-reported — attribute / annotation reference rows only keep the short name and cannot be disambiguated. `DbReader.GetFileDependencies` now tags synthetic alias rows with `is_attribute_alias = 1` (real rows are `0`), propagates the flag through `source_name_counts` via `MIN`, and restricts alias edges to class-like target kinds (`class` / `struct` / `interface`) in the `edges` CTE join. The `target_files` CTE is also switched from `DISTINCT` over `(path, lang, name, kind)` to `GROUP BY (path, lang, name)` with a `MAX(...)` `has_class_like_kind` flag, so files that define both a class and a same-named function (e.g. the C# constructor `Target()` inside `class Target`) no longer fan out into two target rows and inflate the `deps` reference count. `DbReader.GetFileDependencyHintsToResolvedType` now calls a new `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` helper that counts class-like definitions with the same unqualified name *within the active impact scope* (graph-supported languages, plus the caller's `--lang` / `--path` / `--exclude-path` / `--exclude-tests` filters); the metadata evidence-guard bypass is only applied when that in-scope count is ≤ 1. If the name is ambiguous within scope, metadata-only edges fall through to the normal `SourceFileHasStructuredTypeEvidence` check, so pure `[MyAudit]` consumers are dropped instead of being mis-attributed. Scoping matters: a same-named class in an unrelated language / path / test tree must not suppress the C# metadata bypass, because attribute reference rows are already language-qualified through the graph-supported reference join, and the README contract for `impact` explicitly promises "active `--lang` / `--path` / `--exclude-path` / `--exclude-tests` scope". The C# attribute deps test `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`, the unambiguous single-definition `impact` test, and the pre-existing graph-queries dedupe test all still pass. Added `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets` (ambiguous same-name function must not produce a spurious edge), `DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget` (two `MyAuditAttribute` classes within the same impact scope suppress the metadata heuristic hint), `DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous` (single class definition still surfaces the `[MyAudit]` consumer), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope` (same-named Java annotation must not suppress a `--lang csharp` query), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope` (out-of-scope `src/B/` same-named class must not suppress a `src/A/`-scoped query), and `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests` (test-tree same-named class must not suppress an `--exclude-tests` query). `ResolveImpactFallbackNames` is additionally tightened so the C# `Attribute`-suffix alias is only applied to the resolved definition's own name: previously the alias was applied to every same-file fallback name, so a nested `BarAttribute` inside the file that defines `FooAttribute` caused `impact FooAttribute` to falsely attribute `[Bar]` use sites to `FooAttribute`'s blast radius. Added `DbReaderTests.GetFileDependencyHints_CSharpAttributeSuffixAlias_DoesNotLeakToSameFileSiblings` to pin this. `GetFileDependencies` is additionally restructured so metadata rows (`attribute` / `annotation`) cannot leak onto bare-name call-graph targets: the `logical_references` CTE now carries an `is_metadata` flag, `source_name_counts` groups by `(source_file_id, source_path, source_lang, symbol_name, is_attribute_alias, is_metadata)` so metadata and non-metadata rows with the same symbol_name stay separate, the `edges` CTE restricts ALL metadata edges (not just synthetic aliases) to `tf.has_class_like_kind = 1`, and two new CTEs resolve the remaining false-positive shapes: `metadata_raw_suppression` drops the raw C# `[Foo]` row when the synthetic `FooAttribute` alias already resolves to a class-like target in the same source file (so `[Foo]` does not fan out to both `class FooAttribute` and a plain `class Foo` that shares the bare name), and `target_ambiguity` drops metadata edges when the same-named symbol resolves to multiple class-like definitions in scope (so ambiguous `[MyAudit]` usage is left to `impact` / `references` to disambiguate rather than being fanned out to every same-named attribute class). The existing `GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies` test (which pins `[JsonConverter(...)]` → `class JsonConverter : Attribute` for a target class whose name does NOT end in `Attribute`) still passes because only one class-like target exists in scope, so `target_ambiguity` keeps it, the `is_metadata` / class-like restriction accepts it, and `metadata_raw_suppression` finds no synthetic `JsonConverterAttribute` alias target and therefore does not suppress the raw row. Added `DbReaderTests.GetFileDependencies_CSharpAttributeRawDoesNotLeakToBareNameClass_WhenSuffixTargetExists` (raw `[MyAudit]` row is suppressed when the alias resolves to `class MyAuditAttribute`, so `[MyAudit]` does NOT depend on an unrelated plain `class MyAudit` file), `DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotLeakToSameNameMethodOrProperty` (fully-qualified `[MyAuditAttribute]` metadata does NOT depend on a file that only defines a method / property named `MyAuditAttribute`), and `DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotFanOutWhenMultipleSameNameAttributeClasses` (two same-named attribute classes in different namespaces cause the metadata edge to be dropped as ambiguous). Both the `impact` ambiguity guard (`IsMetadataTargetUnambiguous`) and the `deps` ambiguity CTE (`target_ambiguity`) are additionally tightened to count class-like definitions at symbol-identity level instead of path level: `IsMetadataTargetUnambiguous` now uses `SELECT DISTINCT f.path, s.line, s.name` so two same-named class-like definitions in one source file (e.g. idiomatic C# `namespace A { class FooAttribute { } } namespace B { class FooAttribute { } }` in a single .cs file) register as ambiguous; `target_ambiguity` joins `target_files` back through `files` + `symbols` with `COUNT(*)` over class-like symbol rows so the same same-file duplicate case correctly drops the `[Foo]` metadata edge. Path-level counting previously treated these same-file duplicates as count=1 because `SELECT DISTINCT f.path` and `target_files` (grouped by `dst.path`) both collapse the two definitions into a single row, so `cdidx deps` and `cdidx impact` were confidently reporting an ambiguous short-name metadata reference as a definite dependency / blast-radius edge — contradicting the design intent that "ambiguity" includes duplicates in one file. Added `DbReaderTests.GetFileDependencies_CSharpAttributeAmbiguityCountsSameFileDuplicateClassDefinitions` and `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CountsSameFileDuplicateDefinitions` to pin the symbol-identity-level counting for both `deps` and `impact`. `IsMetadataTargetUnambiguous` is additionally tightened to wrap `--path` / `--exclude-path` parameters through `EscapeLikeQuery` and bind them as `%...%` patterns, matching the LIKE semantics used by the rest of the reader (search / references / deps etc.); without the wrap the in-scope class-like count would underflow to 1 even when two same-named attribute classes sit side-by-side in the requested CLI subtree such as `--path src/A/`, and the metadata bypass would falsely fire. Added `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CliPathPatternEscaping_SuppressesWhenInScopeIsAmbiguous` to pin this. `CSharpNoArgAttributeRegex` is additionally widened to accept an optional generic-argument list after the attribute name (`(?:\s*<[^\n]+?>)?`) so generic no-arg attribute forms such as `[MyAudit]`, `[assembly: MyAttr]`, the multi-line `[\n MyAttr\n]`, and nested-generic forms like `[MyAttr>]` / `[MyOther>>]` are indexed as `attribute` references instead of falling through both `CallRegex` (requires `(`) and the old no-arg regex (which ended at `]` / `,` / EOL and therefore rejected `` between the name and `]`). The inner generic segment now uses a non-greedy `[^\n]+?` so nested `>` tokens (e.g. the closing `>` of an inner `Dictionary`) no longer terminate the match prematurely — the old `[^>\n]+` excluded `>` and therefore broke on the first inner `>`, dropping the reference row for nested-generic attribute usages. Without the widening those sites produced zero reference rows, so `deps` / `impact` / `references` / `inspect` / `analyze_symbol` all missed the dependency edge to the real `FooAttribute` class file. Added `ReferenceExtractorTests.Extract_CsharpGenericNoArgAttribute_ClassifiedAsAttribute`, `ReferenceExtractorTests.Extract_CsharpNestedGenericNoArgAttribute_ClassifiedAsAttribute`, `DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_StillIndexedAndResolvesToAttributeClass`, `DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_AssemblyTarget_IsIndexed`, and `DbReaderTests.GetFileDependencies_CSharpNestedGenericNoArgAttribute_ResolvesToAttributeClass` to pin the end-to-end behavior. Both the `impact` ambiguity guard (`IsMetadataTargetUnambiguous`) and the `deps` ambiguity path (`target_files` / `target_ambiguity`) are additionally tightened to treat a C# class as a valid `[Attr]` metadata target ONLY when its signature inherits from an `Attribute`-suffixed base class (`signature LIKE '%: %Attribute%'`). Plain `class MyAuditAttribute { }` without `: Attribute` inheritance is not a valid `[MyAudit]` target at compile time, so counting it as an ambiguity candidate would under-report the `impact` blast radius and suppress a real `deps` metadata edge when a same-named plain class coexists with the real attribute class. `target_files` now exposes a new `has_metadata_target_kind` MAX column that applies the same C# signature filter, and `metadata_raw_suppression` / `target_ambiguity` / the metadata clause in `edges` are all switched to `has_metadata_target_kind`; Java / Kotlin / Scala targets remain unaffected because their metadata markers cannot be captured with a single portable signature LIKE, so they keep the broader class-like candidate set. Added `DbReaderTests.GetFileDependencies_CSharp_PlainClassWithAttributeSuffixName_DoesNotCountAsAmbiguity` to pin the `deps` path. Round-17 follow-up loosens the C# metadata-target signature filter from `'%: %Attribute%'` (requires the immediate base class to end in `Attribute`) to `'%: %'` (any inheritance clause qualifies) so indirect inheritance like `class MyAuditAttribute : BaseAudit` where `BaseAudit : Attribute` is also recognized as a valid `[MyAudit]` target; transitive base-type resolution is not available at SQL time, so the inheritance-clause check is the best portable approximation — non-attribute derivations are over-accepted, but an invalid `[MyFoo]` against a non-attribute class would fail to compile and therefore does not appear as a real reference. The shared `BuildCSharpMetadataTargetFilter` helper additionally degrades to `1 = 1` (the pre-metadata-eligibility behavior) when the `symbols.signature` column is missing on legacy / read-only DBs where `TryMigrateForRead` could not add it, so `deps` / `impact` queries return results instead of crashing with `no such column: s.signature`. Added `DbReaderTests.GetFileDependencies_CSharp_IndirectAttributeInheritance_ResolvesAsMetadataTarget` to pin the indirect-inheritance path. Round-18 follow-up replaces the boolean `BuildCSharpMetadataTargetFilter` helper with a unified `BuildMetadataTargetKindExpr(fileAlias)` expression that narrows the metadata-target candidate kind per language: C# accepts only `kind = 'class'` with an inheritance clause (with the same legacy `1 = 1` fallback to class-only when `symbols.signature` is missing), JavaScript/TypeScript accept `class` / `struct` / `interface` / `function` (so function-decorator factories like `function sealed(target) { ... }` targeted by `@sealed` stay as valid `deps` edges), and other graph-supported languages keep the broader class/struct/interface candidate set. The old filter applied only the C# signature clause and left `s.kind IN ('class', 'struct', 'interface')` outside, which (a) wrongly counted `struct MyAuditAttribute` / `interface MyAuditAttribute` as C# metadata-target ambiguity candidates even though neither can derive from `System.Attribute`, and (b) dropped JS/TS decorator edges to a `function sealed` factory because `has_metadata_target_kind` required a class-like kind. The new expression is applied at three call sites: `IsMetadataTargetUnambiguous` (via a precomputed `metadataTargetKindExprF` local), the `target_files` CTE `has_metadata_target_kind` MAX column, and the `target_ambiguity` CTE JOIN condition (where the outer `s.kind IN (...)` narrowing is removed since the new expression already encodes the per-language kind rule). Added `DbReaderTests.GetFileDependencies_JavaScriptFunctionDecorator_ResolvesAsDependency` (JS `@sealed` class → `function sealed(target)` factory file now produces a deps edge instead of being dropped) and `DbReaderTests.GetFileDependencies_CSharp_SameNameInterface_DoesNotBlockMetadataEdge` (same-named `interface MyAuditAttribute` no longer ambiguates `[MyAudit]` → `class MyAuditAttribute : Attribute`). Round-19 follow-up additionally tightens the C# clause to accept rows whose `signature` is NULL when the column itself exists (`s.signature IS NULL OR s.signature LIKE '%: %'`): the previous `s.signature IS NOT NULL AND s.signature LIKE '%: %'` rejected every row on a DB whose schema was migrated in place by `TryMigrateForRead` without reindexing (the column is added but existing rows keep NULL signatures), so real `[MyAudit]` → `class MyAuditAttribute : System.Attribute` edges silently disappeared from `deps` / `impact` until the user re-ran `cdidx index`. Matching the spirit of the column-missing `1 = 1` fallback, NULL signatures are now treated as eligible instead of being rejected outright. Added `DbReaderTests.GetFileDependencies_CSharp_LegacyDbWithNullSignature_StillResolvesAttributeEdge` (NULL-signature attribute class still resolves the `deps` edge). The remaining edge case — a non-attribute class with the same name as a real attribute class silently suppressing the real edge through over-broad `LIKE '%: %'` matching — is tracked as #435 and requires a schema-level `is_metadata_target` column to resolve cleanly, so it is deliberately deferred out of this PR to keep the #293 scope focused on the already-landed attribute / annotation classification work. Affected: `src/CodeIndex/Database/DbReader.cs`, `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. +- **`deps` C# attribute suffix alias now only matches class-like targets, and `impact` metadata bypass is suppressed when the class-like target name is ambiguous across namespaces (#293 follow-up)** — The earlier `#293` follow-up made `deps` UNION a C#-only `symbol_name || 'Attribute'` suffix alias into the logical-reference CTE so `[Foo]` attribute consumers could reach `FooAttribute` class files, and made `impact` (`GetFileDependencyHintsToResolvedType`) bypass the structured-evidence guard whenever any contributing reference was `attribute` / `annotation`. Both changes were too broad. On `deps` the alias joined purely by name, so `[Foo]` would also depend on any file that merely defined a function / property / variable named `FooAttribute` (e.g. `void MyAuditAttribute()` in an unrelated helper). On `impact` the metadata bypass did not check whether the target name was uniquely a class-like definition, so when the same unqualified name (e.g. `MyAuditAttribute`) existed in multiple namespaces the rename/remove blast radius was over-reported — attribute / annotation reference rows only keep the short name and cannot be disambiguated. `DbReader.GetFileDependencies` now tags synthetic alias rows with `is_attribute_alias = 1` (real rows are `0`), propagates the flag through `source_name_counts` via `MIN`, and restricts alias edges to class-like target kinds (`class` / `struct` / `interface`) in the `edges` CTE join. The `target_files` CTE is also switched from `DISTINCT` over `(path, lang, name, kind)` to `GROUP BY (path, lang, name)` with a `MAX(...)` `has_class_like_kind` flag, so files that define both a class and a same-named function (e.g. the C# constructor `Target()` inside `class Target`) no longer fan out into two target rows and inflate the `deps` reference count. `DbReader.GetFileDependencyHintsToResolvedType` now calls a new `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` helper that counts class-like definitions with the same unqualified name *within the active impact scope* (graph-supported languages, plus the caller's `--lang` / `--path` / `--exclude-path` / `--exclude-tests` filters); the metadata evidence-guard bypass is only applied when that in-scope count is ≤ 1. If the name is ambiguous within scope, metadata-only edges fall through to the normal `SourceFileHasStructuredTypeEvidence` check, so pure `[MyAudit]` consumers are dropped instead of being mis-attributed. Scoping matters: a same-named class in an unrelated language / path / test tree must not suppress the C# metadata bypass, because attribute reference rows are already language-qualified through the graph-supported reference join, and the README contract for `impact` explicitly promises "active `--lang` / `--path` / `--exclude-path` / `--exclude-tests` scope". The C# attribute deps test `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`, the unambiguous single-definition `impact` test, and the pre-existing graph-queries dedupe test all still pass. Added `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets` (ambiguous same-name function must not produce a spurious edge), `DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget` (two `MyAuditAttribute` classes within the same impact scope suppress the metadata heuristic hint), `DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous` (single class definition still surfaces the `[MyAudit]` consumer), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope` (same-named Java annotation must not suppress a `--lang csharp` query), `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope` (out-of-scope `src/B/` same-named class must not suppress a `src/A/`-scoped query), and `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests` (test-tree same-named class must not suppress an `--exclude-tests` query). `ResolveImpactFallbackNames` is additionally tightened so the C# `Attribute`-suffix alias is only applied to the resolved definition's own name: previously the alias was applied to every same-file fallback name, so a nested `BarAttribute` inside the file that defines `FooAttribute` caused `impact FooAttribute` to falsely attribute `[Bar]` use sites to `FooAttribute`'s blast radius. Added `DbReaderTests.GetFileDependencyHints_CSharpAttributeSuffixAlias_DoesNotLeakToSameFileSiblings` to pin this. `GetFileDependencies` is additionally restructured so metadata rows (`attribute` / `annotation`) cannot leak onto bare-name call-graph targets: the `logical_references` CTE now carries an `is_metadata` flag, `source_name_counts` groups by `(source_file_id, source_path, source_lang, symbol_name, is_attribute_alias, is_metadata)` so metadata and non-metadata rows with the same symbol_name stay separate, the `edges` CTE restricts ALL metadata edges (not just synthetic aliases) to `tf.has_class_like_kind = 1`, and two new CTEs resolve the remaining false-positive shapes: `metadata_raw_suppression` drops the raw C# `[Foo]` row when the synthetic `FooAttribute` alias already resolves to a class-like target in the same source file (so `[Foo]` does not fan out to both `class FooAttribute` and a plain `class Foo` that shares the bare name), and `target_ambiguity` drops metadata edges when the same-named symbol resolves to multiple class-like definitions in scope (so ambiguous `[MyAudit]` usage is left to `impact` / `references` to disambiguate rather than being fanned out to every same-named attribute class). The existing `GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies` test (which pins `[JsonConverter(...)]` → `class JsonConverter : Attribute` for a target class whose name does NOT end in `Attribute`) still passes because only one class-like target exists in scope, so `target_ambiguity` keeps it, the `is_metadata` / class-like restriction accepts it, and `metadata_raw_suppression` finds no synthetic `JsonConverterAttribute` alias target and therefore does not suppress the raw row. Added `DbReaderTests.GetFileDependencies_CSharpAttributeRawDoesNotLeakToBareNameClass_WhenSuffixTargetExists` (raw `[MyAudit]` row is suppressed when the alias resolves to `class MyAuditAttribute`, so `[MyAudit]` does NOT depend on an unrelated plain `class MyAudit` file), `DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotLeakToSameNameMethodOrProperty` (fully-qualified `[MyAuditAttribute]` metadata does NOT depend on a file that only defines a method / property named `MyAuditAttribute`), and `DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotFanOutWhenMultipleSameNameAttributeClasses` (two same-named attribute classes in different namespaces cause the metadata edge to be dropped as ambiguous). Both the `impact` ambiguity guard (`IsMetadataTargetUnambiguous`) and the `deps` ambiguity CTE (`target_ambiguity`) are additionally tightened to count class-like definitions at symbol-identity level instead of path level: `IsMetadataTargetUnambiguous` now uses `SELECT DISTINCT f.path, s.line, s.name` so two same-named class-like definitions in one source file (e.g. idiomatic C# `namespace A { class FooAttribute { } } namespace B { class FooAttribute { } }` in a single .cs file) register as ambiguous; `target_ambiguity` joins `target_files` back through `files` + `symbols` with `COUNT(*)` over class-like symbol rows so the same same-file duplicate case correctly drops the `[Foo]` metadata edge. Path-level counting previously treated these same-file duplicates as count=1 because `SELECT DISTINCT f.path` and `target_files` (grouped by `dst.path`) both collapse the two definitions into a single row, so `cdidx deps` and `cdidx impact` were confidently reporting an ambiguous short-name metadata reference as a definite dependency / blast-radius edge — contradicting the design intent that "ambiguity" includes duplicates in one file. Added `DbReaderTests.GetFileDependencies_CSharpAttributeAmbiguityCountsSameFileDuplicateClassDefinitions` and `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CountsSameFileDuplicateDefinitions` to pin the symbol-identity-level counting for both `deps` and `impact`. `IsMetadataTargetUnambiguous` is additionally tightened to wrap `--path` / `--exclude-path` parameters through `EscapeLikeQuery` and bind them as `%...%` patterns, matching the LIKE semantics used by the rest of the reader (search / references / deps etc.); without the wrap the in-scope class-like count would underflow to 1 even when two same-named attribute classes sit side-by-side in the requested CLI subtree such as `--path src/A/`, and the metadata bypass would falsely fire. Added `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CliPathPatternEscaping_SuppressesWhenInScopeIsAmbiguous` to pin this. `CSharpNoArgAttributeRegex` is additionally widened to accept an optional generic-argument list after the attribute name (`(?:\s*<[^\n]+?>)?`) so generic no-arg attribute forms such as `[MyAudit]`, `[assembly: MyAttr]`, the multi-line `[\n MyAttr\n]`, and nested-generic forms like `[MyAttr>]` / `[MyOther>>]` are indexed as `attribute` references instead of falling through both `CallRegex` (requires `(`) and the old no-arg regex (which ended at `]` / `,` / EOL and therefore rejected `` between the name and `]`). The inner generic segment now uses a non-greedy `[^\n]+?` so nested `>` tokens (e.g. the closing `>` of an inner `Dictionary`) no longer terminate the match prematurely — the old `[^>\n]+` excluded `>` and therefore broke on the first inner `>`, dropping the reference row for nested-generic attribute usages. Without the widening those sites produced zero reference rows, so `deps` / `impact` / `references` / `inspect` / `analyze_symbol` all missed the dependency edge to the real `FooAttribute` class file. Added `ReferenceExtractorTests.Extract_CsharpGenericNoArgAttribute_ClassifiedAsAttribute`, `ReferenceExtractorTests.Extract_CsharpNestedGenericNoArgAttribute_ClassifiedAsAttribute`, `DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_StillIndexedAndResolvesToAttributeClass`, `DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_AssemblyTarget_IsIndexed`, and `DbReaderTests.GetFileDependencies_CSharpNestedGenericNoArgAttribute_ResolvesToAttributeClass` to pin the end-to-end behavior. Both the `impact` ambiguity guard (`IsMetadataTargetUnambiguous`) and the `deps` ambiguity path (`target_files` / `target_ambiguity`) are additionally tightened to treat a C# class as a valid `[Attr]` metadata target ONLY when its signature inherits from an `Attribute`-suffixed base class (`signature LIKE '%: %Attribute%'`). Plain `class MyAuditAttribute { }` without `: Attribute` inheritance is not a valid `[MyAudit]` target at compile time, so counting it as an ambiguity candidate would under-report the `impact` blast radius and suppress a real `deps` metadata edge when a same-named plain class coexists with the real attribute class. `target_files` now exposes a new `has_metadata_target_kind` MAX column that applies the same C# signature filter, and `metadata_raw_suppression` / `target_ambiguity` / the metadata clause in `edges` are all switched to `has_metadata_target_kind`; Java / Kotlin / Scala targets remain unaffected because their metadata markers cannot be captured with a single portable signature LIKE, so they keep the broader class-like candidate set. Added `DbReaderTests.GetFileDependencies_CSharp_PlainClassWithAttributeSuffixName_DoesNotCountAsAmbiguity` to pin the `deps` path. Round-17 follow-up loosens the C# metadata-target signature filter from `'%: %Attribute%'` (requires the immediate base class to end in `Attribute`) to `'%: %'` (any inheritance clause qualifies) so indirect inheritance like `class MyAuditAttribute : BaseAudit` where `BaseAudit : Attribute` is also recognized as a valid `[MyAudit]` target; transitive base-type resolution is not available at SQL time, so the inheritance-clause check is the best portable approximation — non-attribute derivations are over-accepted, but an invalid `[MyFoo]` against a non-attribute class would fail to compile and therefore does not appear as a real reference. The shared `BuildCSharpMetadataTargetFilter` helper additionally degrades to `1 = 1` (the pre-metadata-eligibility behavior) when the `symbols.signature` column is missing on legacy / read-only DBs where `TryMigrateForRead` could not add it, so `deps` / `impact` queries return results instead of crashing with `no such column: s.signature`. Added `DbReaderTests.GetFileDependencies_CSharp_IndirectAttributeInheritance_ResolvesAsMetadataTarget` to pin the indirect-inheritance path. Round-18 follow-up replaces the boolean `BuildCSharpMetadataTargetFilter` helper with a unified `BuildMetadataTargetKindExpr(fileAlias)` expression that narrows the metadata-target candidate kind per language: C# accepts only `kind = 'class'` with an inheritance clause (with the same legacy `1 = 1` fallback to class-only when `symbols.signature` is missing), JavaScript/TypeScript accept `class` / `struct` / `interface` / `function` (so function-decorator factories like `function sealed(target) { ... }` targeted by `@sealed` stay as valid `deps` edges), and other graph-supported languages keep the broader class/struct/interface candidate set. The old filter applied only the C# signature clause and left `s.kind IN ('class', 'struct', 'interface')` outside, which (a) wrongly counted `struct MyAuditAttribute` / `interface MyAuditAttribute` as C# metadata-target ambiguity candidates even though neither can derive from `System.Attribute`, and (b) dropped JS/TS decorator edges to a `function sealed` factory because `has_metadata_target_kind` required a class-like kind. The new expression is applied at three call sites: `IsMetadataTargetUnambiguous` (via a precomputed `metadataTargetKindExprF` local), the `target_files` CTE `has_metadata_target_kind` MAX column, and the `target_ambiguity` CTE JOIN condition (where the outer `s.kind IN (...)` narrowing is removed since the new expression already encodes the per-language kind rule). Added `DbReaderTests.GetFileDependencies_JavaScriptFunctionDecorator_ResolvesAsDependency` (JS `@sealed` class → `function sealed(target)` factory file now produces a deps edge instead of being dropped) and `DbReaderTests.GetFileDependencies_CSharp_SameNameInterface_DoesNotBlockMetadataEdge` (same-named `interface MyAuditAttribute` no longer ambiguates `[MyAudit]` → `class MyAuditAttribute : Attribute`). Round-19 follow-up additionally tightens the C# clause to accept rows whose `signature` is NULL when the column itself exists (`s.signature IS NULL OR s.signature LIKE '%: %'`): the previous `s.signature IS NOT NULL AND s.signature LIKE '%: %'` rejected every row on a DB whose schema was migrated in place by `TryMigrateForRead` without reindexing (the column is added but existing rows keep NULL signatures), so real `[MyAudit]` → `class MyAuditAttribute : System.Attribute` edges silently disappeared from `deps` / `impact` until the user re-ran `cdidx index`. Matching the spirit of the column-missing `1 = 1` fallback, NULL signatures are now treated as eligible instead of being rejected outright. Added `DbReaderTests.GetFileDependencies_CSharp_LegacyDbWithNullSignature_StillResolvesAttributeEdge` (NULL-signature attribute class still resolves the `deps` edge). Round-20 follow-up tightens the NULL-signature fallback to the canonical C# attribute naming convention (`name LIKE '%Attribute'`) instead of accepting every NULL-signature class, so a legacy-migration DB no longer counts arbitrary same-named non-attribute classes (`HelperClient : BaseService`) as metadata-target candidates that would silently inject false ambiguity against a real `[MyAudit]` → `MyAuditAttribute` edge. DBs without any `signature` column at all degrade to the same naming heuristic, and the existing `GetFileDependencies_CSharp_LegacyDbWithNullSignature_StillResolvesAttributeEdge` regression still passes because `MyAuditAttribute` ends with `Attribute` and therefore survives the tightened fallback. Round-20 also drops `interface` from the JavaScript / TypeScript metadata-target candidate kind set (`class` + `function` only): TypeScript `interface` is a compile-time type-only construct and cannot be a decorator target at runtime, so including it would let a same-named `interface` inject false ambiguity against a real `function` / `class` decorator provider and silently drop the `@sealed` → `function sealed(target)` deps edge. Added `DbReaderTests.GetFileDependencies_CSharp_LegacyDbWithNullSignature_NonAttributeName_DoesNotBlockMetadataEdge` (arbitrary NULL-signature class with a non-`Attribute` name no longer injects false ambiguity) and `DbReaderTests.GetFileDependencies_JavaScript_SameNameInterface_DoesNotBlockFunctionDecoratorEdge` (same-name `interface sealed` does not suppress the `@sealed` → `function sealed(target)` deps edge). The remaining edge case — a non-attribute class that already follows the `Attribute` naming convention or has an unrelated `: ` inheritance clause silently suppressing the real edge — is tracked as #435 and requires a schema-level `is_metadata_target` column to resolve cleanly, so it is deliberately deferred out of this PR to keep the #293 scope focused on the already-landed attribute / annotation classification work. Affected: `src/CodeIndex/Database/DbReader.cs`, `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. - **MCP `callers` / `callees` `kind` schema, CLI help, and README `--kind` option row no longer advertise metadata kinds as valid filter values (#293 follow-up)** — The `callers` / `callees` handlers reject `kind: "attribute"` / `kind: "annotation"` with a usage error (CLI) / `isError` tool response (MCP), but the MCP tool schema's `kind` property description still listed all five kinds as if they were valid filter values (`"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"`), `cdidx --help` similarly lumped `callers` / `callees` together with `references` for the `--kind` help row, and the README `--kind` option table (EN line 436 / JP line 1307) still directed readers to pass `--kind attribute` or `--kind annotation` on `callers` / `callees`. Schema and handler now agree: the `callers` / `callees` `kind` description now reads `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."`, the CLI `--kind` help row splits `references` (accepts all five kinds) from `callers` / `callees` (call-graph kinds only, metadata rejected, redirects to `references`), and the README `--kind` row (both English and Japanese) now explicitly states that `callers` / `callees` reject `--kind attribute|annotation` with a usage error and points to `references --kind attribute|annotation` as the correct metadata-enumeration path. The MCP `references` tool description is unchanged because it still accepts every indexed kind. Added `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` to pin the corrected schema wording. Affected: `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `src/CodeIndex/Cli/ConsoleUi.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `README.md`. - **`deps`, `impact`, `references`, `inspect`, and `analyze_symbol` retain metadata references as compile-time dependency edges and canonicalize the C# attribute-suffix convention (#293 follow-up)** — `DbReader.GetFileDependencies` (backing `cdidx deps`) and `DbReader.GetFileDependencyHintsToResolvedType` (backing `cdidx impact` / MCP `impact_analysis` heuristic file-hint fallback) keep metadata references in their logical reference CTE. Renaming or removing an attribute class still breaks annotated sites such as `[JsonConverterAttribute]` at compile time, so file-level blast-radius analysis must surface those edges as real dependencies — now labeled `attribute` / `annotation` in `logical_reference_kind` (previously these same sites leaked in as phantom `call` rows before classification was corrected). `GetFileDependencies` additionally canonicalizes the C# attribute naming convention: idiomatic `[Foo]` usage is stored with `symbol_name = "Foo"` but the defining class is `FooAttribute`, so the logical-reference CTE now UNION-ALLs a suffix-aliased copy of every C# `attribute`-kind row with `'Attribute'` appended (only when the original symbol_name does not already end in `Attribute`) so the target-file join reaches the attribute class file. `GetFileDependencyHintsToResolvedType` also aggregates a per-candidate `has_metadata_ref` flag and bypasses the `SourceFileHasStructuredTypeEvidence` guard for edges whose contributing references are purely metadata, so pure-attribute consumer files such as `[MyAudit] class Svc` now surface through `impact MyAuditAttribute` instead of being filtered out. `ResolveImpactFallbackNames` symmetrically adds suffix-stripped aliases for C# class names ending in `Attribute`, so the BFS-less file-hint path can find `[Foo]` sites whose source symbol_name does not match the defining class name. The same canonicalization is propagated to the symbol-level reference reader: `DbReader.SearchReferences`, `CountSearchReferences`, and `CountSearchReferencesTotal` (backing `references`, `inspect`, MCP `analyze_symbol`) now compute a C#-only `queryAttributeAlias` (defined in the new `ComputeCSharpAttributeSuffixAlias` helper) and OR it into both exact (folded and NOCASE) and substring WHERE clauses, so `references MyAuditAttribute` (and `inspect MyAuditAttribute` / `analyze_symbol MyAuditAttribute`) surface the idiomatic `[MyAudit]` call site consistently with `deps` / `impact`. Substring mode uses an exact-OR for the alias (not a second LIKE) so user queries like `MyAuditAttribute` do not over-match unrelated names such as `FooAuditLog`. The alias is C# only, so Java / Kotlin / Scala / TypeScript annotation sites remain unaffected. `callers` / `callees` intentionally skip the alias because they filter to the shared call-graph kinds (`call`, `instantiate`, `subscribe`) and reject `--kind attribute|annotation` at the CLI / MCP boundary — those commands model the dynamic call graph, not the dependency graph. The alias branch is additionally scoped so it cannot produce false positives: `ComputeCSharpAttributeSuffixAlias` returns `null` when `referenceKind` is a non-`attribute` call-graph kind (so `references FooAttribute --kind call` stays strict and does not match `Foo()`), and the SQL alias disjunct in all three reader methods is clamped to `f.lang = 'csharp' AND r.reference_kind = 'attribute'` so an unscoped `references FooAttribute` (no `--lang` / no `--kind`) still cannot bleed into Java annotations or C# call rows. Suffix detection is also case-insensitive (`OrdinalIgnoreCase`) so `references myauditattribute` and `inspect MyAuditATTRIBUTE` still produce the `MyAudit` alias, consistent with the NOCASE / folded contract of the surrounding exact / substring query paths. Added regression coverage `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies` (runtime + metadata edges both surface), `DbReaderTests.GetFileDependencies_MatchesCSharpAttributeSuffixConvention` (`[MyAudit]` → `MyAuditAttribute` canonicalization through `deps`), `DbReaderTests.SearchReferences_MatchesCSharpAttributeSuffixConvention_Substring` / `_Exact` / `_CSharpAttributeSuffixAliasDoesNotBleedToOtherLanguages` (same canonicalization through `references` / `inspect` / `analyze_symbol`, plus C#-only scope guard), `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_NotAppliedToCallKind` (alias must not fire under `--kind call`), `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_UnscopedLangStillLimitsToCSharpAttributeRows` (unscoped query must not match Java annotations or C# call rows), and `DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_CaseInsensitiveQuery` (lowercase / mixed-case queries still produce the alias). Affected: `src/CodeIndex/Database/DbReader.cs`, `tests/CodeIndex.Tests/DbReaderTests.cs`. - **`callers` / `callees` now reject `--kind attribute|annotation` at the CLI and MCP boundary, and Swift / Gradle `@` metadata is reclassified as `annotation` (#293 follow-up)** — Metadata rows (`attribute` / `annotation`) are attributed to their enclosing body-range symbol rather than the annotated target, so `callers Obsolete --kind attribute` would silently return `[Obsolete] void M()` under the enclosing class (not `M`), and file-level targets such as `[assembly: CLSCompliant]` or Kotlin `@file:JvmName` would drop entirely because `container_name` is `NULL`. `QueryCommandRunner.RunCallers` / `RunCallees` now reject `--kind attribute` and `--kind annotation` with `CommandExitCodes.UsageError`, and the MCP `callers` / `callees` tools return an `isError` tool response; both paths direct users to `references --kind attribute|annotation`, which IS a correct metadata enumeration path. Separately, `AnnotationLanguages` now includes `swift` and `gradle`, so Swift `@available(iOS 13.0, *)` / `@objc` / `@MainActor` and Gradle/Groovy `@CompileStatic` / `@TaskAction` are recorded as `annotation` instead of `call` (previously the `@Name(args)` forms leaked into `callers` / `callees` / `hotspots` / `impact` as phantom call edges, and the no-arg `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` forms dropped from the index entirely because no-arg annotation emission was gated to languages in this set). Added `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError` (theory, 4 cases), `McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError` (theory, 4 cases), `ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`, and `ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation`. Affected: `src/CodeIndex/Cli/QueryCommandRunner.cs`, `src/CodeIndex/Mcp/McpToolHandlers.cs`, `src/CodeIndex/Indexer/ReferenceExtractor.cs`, `tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`, `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. @@ -686,7 +686,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **DEVELOPER_GUIDE が SQLite の位置付けとリリース番号の例示を明確化** — cdidx が「設定ゼロ・単一ファイル・本番依存1個」の CLI であり、その唯一の本番依存が `Microsoft.Data.Sqlite` であることを明記した。あわせて、リリース手順のチェックリスト中に出てくる `1.9.0` は固定値ではなく例示であり、実際のリリース番号へ読み替える前提だと明示した。対象: `DEVELOPER_GUIDE.md`。 #### 修正 -- **`deps` の C# attribute サフィックス別名を class-like ターゲットに限定し、`impact` の metadata bypass も class-like ターゲット名が名前空間をまたいで曖昧な場合は抑止するよう修正 (#293 追加対応)** — 直前の #293 追加対応では、`deps` の logical-reference CTE に C# 限定で `symbol_name || 'Attribute'` のサフィックス別名を UNION することで `[Foo]` の attribute consumer が `FooAttribute` クラスファイルへ到達するようにし、`impact`(`GetFileDependencyHintsToResolvedType`)も寄与参照に `attribute` / `annotation` が含まれていれば structured-evidence guard をバイパスするようにしていたが、どちらも広すぎた。`deps` 側では別名が純粋に名前だけで join していたため、`[Foo]` が同名の関数・プロパティ・変数(例: 無関係なヘルパーの `void MyAuditAttribute()`)を定義するファイルにも依存を張ってしまっていた。`impact` 側の metadata bypass も、対象名が一意に class-like 定義を指すかを確認していなかったため、`MyAuditAttribute` のような短い名前が複数の namespace に存在すると rename / remove の blast-radius が過剰報告されていた(`attribute` / `annotation` 参照行は短い名前しか保持できず曖昧性解消ができない)。`DbReader.GetFileDependencies` は合成された別名行に `is_attribute_alias = 1`(本物の行は `0`)をタグ付けし、`source_name_counts` では `MIN` でフラグを伝播し、`edges` CTE の join で別名 edge をターゲット側 kind が `class` / `struct` / `interface` のときだけに制限するようにした。`target_files` CTE も `DISTINCT (path, lang, name, kind)` から `GROUP BY (path, lang, name)` + `MAX(...)` の `has_class_like_kind` フラグに切り替えたため、class と同名関数(例: C# コンストラクタ `Target()` と `class Target`)を同じファイルで定義しているケースでもターゲット行が 2 本に増えず `deps` の参照数を水増ししない。`DbReader.GetFileDependencyHintsToResolvedType` は新ヘルパー `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` を呼び、*active な impact スコープ内* で同名の class-like 定義数を数える(graph 対応言語に加えて、呼び出し側の `--lang` / `--path` / `--exclude-path` / `--exclude-tests` を同じフィルタで適用)。in-scope でその数が 1 以下のときに限り metadata evidence-guard bypass を適用する。スコープ内で曖昧なときは metadata 単独 edge も通常の `SourceFileHasStructuredTypeEvidence` に戻り、純 `[MyAudit]` consumer は誤帰属せずに落ちる。スコープ尊重が重要: 別言語 / 別 path / test ツリーにある同名クラスが C# の metadata bypass を潰してはならない(参照側が既に graph 対応 join で言語修飾されているため、曖昧性は scope 内で判定すべき)。README の `impact` 契約も「active な `--lang` / `--path` / `--exclude-path` / `--exclude-tests` スコープ」と明記している。既存の C# attribute deps テスト `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`、曖昧ではない単一定義の `impact` テスト、および既存の graph-queries dedupe テストは全てグリーンのまま。回帰テストとして `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets`(同名関数があっても虚偽 edge が出ないこと)、`DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget`(impact スコープ内で `MyAuditAttribute` が複数ある場合に metadata heuristic hint を抑止)、`DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous`(単一クラス定義のみのときは従来どおり `[MyAudit]` consumer を浮かせる)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope`(別言語の同名 annotation は `--lang csharp` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope`(スコープ外 `src/B/` の同名クラスは `src/A/` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests`(test ツリーの同名クラスは `--exclude-tests` クエリを潰さない)を追加。`ResolveImpactFallbackNames` も併せて引き締め、C# の `Attribute` サフィックス別名は *解決済み定義自身* の名前にのみ適用するようにした: 以前は同一ファイル内の fallback 名全てに別名を付けていたため、`FooAttribute` を定義するファイルに nested で `BarAttribute` が存在するだけで `impact FooAttribute` が `[Bar]` 利用サイトを `FooAttribute` の影響範囲として誤報告していた。`DbReaderTests.GetFileDependencyHints_CSharpAttributeSuffixAlias_DoesNotLeakToSameFileSiblings` を追加して固定。さらに `GetFileDependencies` を再構成し、metadata 行(`attribute` / `annotation`)が bare-name の call-graph ターゲットに漏れないようにした: `logical_references` CTE に `is_metadata` フラグを付与し、`source_name_counts` の GROUP BY を `(source_file_id, source_path, source_lang, symbol_name, is_attribute_alias, is_metadata)` に拡張して metadata と非 metadata で同じ symbol_name の行を別グループ化する。`edges` CTE は全 metadata edge(合成別名だけでなく実体行も)に `tf.has_class_like_kind = 1` を要求し、さらに 2 つの CTE で残る誤検知を潰す: `metadata_raw_suppression` は同じ source file で合成別名 `FooAttribute` が class-like ターゲットに解決できているとき、生の C# `[Foo]` 行を落とす(`[Foo]` が `class FooAttribute` と無関係な同名 `class Foo` に fan-out しない)。`target_ambiguity` はスコープ内で同名 class-like ターゲットが複数あるとき metadata edge を落とす(曖昧な `[MyAudit]` は deps で fan-out させず、`impact` / `references` に判断を委ねる)。既存の `GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies`(`[JsonConverter(...)]` → `class JsonConverter : Attribute` をターゲット名に `Attribute` サフィックスがなくても固定)は、class-like ターゲットが 1 つしかないので `target_ambiguity` で残り、`is_metadata` / class-like 制限も受理し、合成 `JsonConverterAttribute` 別名は class-like に解決できないので `metadata_raw_suppression` も発火せず、そのまま通る。回帰テストとして `DbReaderTests.GetFileDependencies_CSharpAttributeRawDoesNotLeakToBareNameClass_WhenSuffixTargetExists`(別名が `class MyAuditAttribute` に解決できたとき生 `[MyAudit]` 行は抑止され、無関係な plain `class MyAudit` ファイルに依存しない)、`DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotLeakToSameNameMethodOrProperty`(完全形 `[MyAuditAttribute]` は同名メソッド / プロパティしか持たないファイルに依存しない)、`DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotFanOutWhenMultipleSameNameAttributeClasses`(別名前空間に同名 attribute クラスが 2 つあると metadata edge はあいまいとして落とす)を追加。加えて `impact` の曖昧性ガード (`IsMetadataTargetUnambiguous`) と `deps` の曖昧性 CTE (`target_ambiguity`) も、path 単位ではなく symbol identity 単位で class-like 定義を数えるように引き締めた: `IsMetadataTargetUnambiguous` は `SELECT DISTINCT f.path, s.line, s.name` に変更し、C# でよくある 1 つの .cs ファイル内に別名前空間で同名 class-like が 2 つ定義されている形 (`namespace A { class FooAttribute { } } namespace B { class FooAttribute { } }`) でも ambiguity を 2 として検出する。`target_ambiguity` は `target_files` を `files` + `symbols` に JOIN し直して `COUNT(*)` を class-like symbol 行に対して取るため、同じ同名ファイル内重複ケースでも `[Foo]` metadata edge を正しく落とせる。path 単位カウントでは `SELECT DISTINCT f.path` も `target_files`(`dst.path` で GROUP BY)も同一ファイル内の 2 定義を 1 行に潰してしまうため、従来は count=1 として曖昧性を取りこぼし、`cdidx deps` と `cdidx impact` が同名 short-name metadata 参照を確定的な依存 / blast-radius edge として出していた。これは曖昧性に "duplicates in one file" を含める前提の設計契約に反していた。`deps` と `impact` 両方で symbol-identity 単位カウントを固定する `DbReaderTests.GetFileDependencies_CSharpAttributeAmbiguityCountsSameFileDuplicateClassDefinitions` と `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CountsSameFileDuplicateDefinitions` を追加。さらに `IsMetadataTargetUnambiguous` の `--path` / `--exclude-path` パラメータを、他の reader 経路(search / references / deps 等)と同じ LIKE セマンティクスに揃えるため `EscapeLikeQuery` でエスケープしたうえで `%...%` で包んでバインドするようにした。生値のままだと `--path src/A/` のような CLI 形では LIKE が一致せず、リクエストされたサブツリーに同名 attribute クラスが 2 件並んでいても in-scope class-like カウントが 1 に過小化され、本来抑止すべき metadata bypass が誤発火してしまっていた。`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CliPathPatternEscaping_SuppressesWhenInScopeIsAmbiguous` を追加して固定する。あわせて `CSharpNoArgAttributeRegex` を拡張し、属性名の直後に任意のジェネリック引数リスト `(?:\s*<[^\n]+?>)?` を許容するようにしたため、`[MyAudit]`・`[assembly: MyAttr]`・複数行の `[\n MyAttr\n]`・ネストジェネリックの `[MyAttr>]` や `[MyOther>>]` のような引数なしジェネリック属性も `attribute` 参照としてインデックスされるようになった。内側ジェネリックセグメントは非貪欲な `[^\n]+?` に変更し、内側の `>`(例: `Dictionary` を閉じる `>`)で match が打ち切られないようにした — 従来の `[^>\n]+` は `>` を除外していたため最初の内側 `>` で崩れ、ネストジェネリック属性の参照行が落ちていた。従来は `CallRegex`(`(` 必須)でも旧 no-arg regex(`]` / `,` / EOL で終端しなければならず、名前と `]` の間に `` があると拒否)でも拾えず、参照行が 0 件となり、`deps` / `impact` / `references` / `inspect` / `analyze_symbol` が実属性クラス `FooAttribute` への依存エッジを全て取りこぼしていた。`ReferenceExtractorTests.Extract_CsharpGenericNoArgAttribute_ClassifiedAsAttribute`、`ReferenceExtractorTests.Extract_CsharpNestedGenericNoArgAttribute_ClassifiedAsAttribute`、`DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_StillIndexedAndResolvesToAttributeClass`、`DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_AssemblyTarget_IsIndexed`、`DbReaderTests.GetFileDependencies_CSharpNestedGenericNoArgAttribute_ResolvesToAttributeClass` を追加して end-to-end の振る舞いを固定する。加えて、`impact` の曖昧性ガード (`IsMetadataTargetUnambiguous`) と `deps` の曖昧性判定 (`target_files` / `target_ambiguity`) は、C# クラスが有効な `[Attr]` metadata target になるのは「signature が `Attribute` サフィックス付き基底を継承している」場合 (`signature LIKE '%: %Attribute%'`) に限定するよう引き締めた。`: Attribute` 継承を書いていない plain `class MyAuditAttribute { }` はコンパイル時に `[MyAudit]` の target にならないため、曖昧性候補として数えると `impact` の影響範囲が過少報告され、`deps` 側でも同名 plain クラスが共存する scene で実 attribute クラスへの metadata edge が落ちてしまう。`target_files` に新しい `has_metadata_target_kind` MAX 列を追加して同じ C# signature フィルタを適用し、`metadata_raw_suppression` / `target_ambiguity` / `edges` の metadata 節はすべて `has_metadata_target_kind` に切り替えた。Java / Kotlin / Scala は portable な signature LIKE で metadata marker を表現しづらいため従来通り class-like 全体を候補に残す。`deps` 側の挙動を固定する `DbReaderTests.GetFileDependencies_CSharp_PlainClassWithAttributeSuffixName_DoesNotCountAsAmbiguity` を追加。Round-17 追加対応では、C# metadata target の signature フィルタを `'%: %Attribute%'`(直接基底が `Attribute` で終わることを要求)から `'%: %'`(何らかの継承節を持てば候補)に緩和した。`class MyAuditAttribute : BaseAudit` で `BaseAudit : Attribute` のような間接継承でも `[MyAudit]` の有効な target として認識される。SQL 時点で基底型を遡ることはできないため、継承節チェックは portable な近似として採用する — 非 attribute 由来のクラスは過剰に受理するが、無効な `[MyFoo]` をコンパイルすることはできず実参照にはならないので実害はない。共有ヘルパー `BuildCSharpMetadataTargetFilter` はさらに、`TryMigrateForRead` が追加できなかった legacy / read-only DB で `symbols.signature` 列が欠落しているときは `1 = 1`(metadata-eligibility 導入前の挙動)へ degrade するので、`deps` / `impact` クエリは `no such column: s.signature` でクラッシュせず結果を返す。間接継承パスを固定する `DbReaderTests.GetFileDependencies_CSharp_IndirectAttributeInheritance_ResolvesAsMetadataTarget` を追加。Round-18 では boolean の `BuildCSharpMetadataTargetFilter` ヘルパーを、ファイル別エイリアスを取り言語ごとに metadata-target 候補 kind を絞る `BuildMetadataTargetKindExpr(fileAlias)` 式へ置き換える。C# は継承節を持つ `kind = 'class'` のみ許可(`symbols.signature` が欠落する legacy DB では従来通り class-only にフォールバック)、JavaScript / TypeScript は `class` / `struct` / `interface` / `function` を許可(`function sealed(target) { ... }` のような factory 関数を `@sealed` が target とする decorator edge が `deps` に残る)、その他の graph 対応言語は従来通り `class` / `struct` / `interface` を受ける。旧フィルタは C# の signature 句だけを適用し、`s.kind IN ('class', 'struct', 'interface')` が外側に残っていたため、(a) C# で `struct MyAuditAttribute` / `interface MyAuditAttribute` が `System.Attribute` を継承できないにも関わらず metadata-target の ambiguity 候補として数えられ、(b) JS/TS で decorator の対象が `function sealed` factory の場合に `has_metadata_target_kind` が class-like を要求するために deps edge が落ちていた。新しい式は 3 箇所で使う: `IsMetadataTargetUnambiguous`(事前計算の `metadataTargetKindExprF` 経由)、`target_files` CTE の `has_metadata_target_kind` MAX 列、`target_ambiguity` CTE の JOIN 条件(言語ごとの kind narrowing が式に内包されたため、外側の `s.kind IN (...)` 絞り込みは削除)。`DbReaderTests.GetFileDependencies_JavaScriptFunctionDecorator_ResolvesAsDependency`(JS の `@sealed` class → `function sealed(target)` factory ファイルが落ちずに deps edge を生成)と `DbReaderTests.GetFileDependencies_CSharp_SameNameInterface_DoesNotBlockMetadataEdge`(同名の `interface MyAuditAttribute` が `[MyAudit]` → `class MyAuditAttribute : Attribute` を ambiguity 扱いにして抑止しない)を追加。Round-19 の追加では、C# 句を `s.signature IS NULL OR s.signature LIKE '%: %'` に緩め、`signature` 列は存在するが row の値が NULL の DB(`TryMigrateForRead` でその場 migration された列を持つが再インデックスを行っていない DB。既存行は NULL のまま)でも degrade するようにする。旧 `s.signature IS NOT NULL AND s.signature LIKE '%: %'` ではそうした DB で全行が reject され、本物の `[MyAudit]` → `class MyAuditAttribute : System.Attribute` edge がユーザーが `cdidx index` を再実行するまで silent に `deps` / `impact` から消えていた。列欠落時の `1 = 1` fallback と同じ精神で NULL signature を eligible 扱いにする。`DbReaderTests.GetFileDependencies_CSharp_LegacyDbWithNullSignature_StillResolvesAttributeEdge`(NULL signature の attribute class でも deps edge が解決する)を追加。残る edge case — 真の attribute class と同名の non-attribute class が `LIKE '%: %'` の過剰マッチで実 edge を silent に抑止する問題 — は schema レベルの `is_metadata_target` 列が必要になるため、#293 PR の scope を広げないために #435 で追跡する別 issue として意図的に deferred。対象: `src/CodeIndex/Database/DbReader.cs`、`src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 +- **`deps` の C# attribute サフィックス別名を class-like ターゲットに限定し、`impact` の metadata bypass も class-like ターゲット名が名前空間をまたいで曖昧な場合は抑止するよう修正 (#293 追加対応)** — 直前の #293 追加対応では、`deps` の logical-reference CTE に C# 限定で `symbol_name || 'Attribute'` のサフィックス別名を UNION することで `[Foo]` の attribute consumer が `FooAttribute` クラスファイルへ到達するようにし、`impact`(`GetFileDependencyHintsToResolvedType`)も寄与参照に `attribute` / `annotation` が含まれていれば structured-evidence guard をバイパスするようにしていたが、どちらも広すぎた。`deps` 側では別名が純粋に名前だけで join していたため、`[Foo]` が同名の関数・プロパティ・変数(例: 無関係なヘルパーの `void MyAuditAttribute()`)を定義するファイルにも依存を張ってしまっていた。`impact` 側の metadata bypass も、対象名が一意に class-like 定義を指すかを確認していなかったため、`MyAuditAttribute` のような短い名前が複数の namespace に存在すると rename / remove の blast-radius が過剰報告されていた(`attribute` / `annotation` 参照行は短い名前しか保持できず曖昧性解消ができない)。`DbReader.GetFileDependencies` は合成された別名行に `is_attribute_alias = 1`(本物の行は `0`)をタグ付けし、`source_name_counts` では `MIN` でフラグを伝播し、`edges` CTE の join で別名 edge をターゲット側 kind が `class` / `struct` / `interface` のときだけに制限するようにした。`target_files` CTE も `DISTINCT (path, lang, name, kind)` から `GROUP BY (path, lang, name)` + `MAX(...)` の `has_class_like_kind` フラグに切り替えたため、class と同名関数(例: C# コンストラクタ `Target()` と `class Target`)を同じファイルで定義しているケースでもターゲット行が 2 本に増えず `deps` の参照数を水増ししない。`DbReader.GetFileDependencyHintsToResolvedType` は新ヘルパー `IsMetadataTargetUnambiguous(definition, lang, pathPatterns, excludePathPatterns, excludeTests)` を呼び、*active な impact スコープ内* で同名の class-like 定義数を数える(graph 対応言語に加えて、呼び出し側の `--lang` / `--path` / `--exclude-path` / `--exclude-tests` を同じフィルタで適用)。in-scope でその数が 1 以下のときに限り metadata evidence-guard bypass を適用する。スコープ内で曖昧なときは metadata 単独 edge も通常の `SourceFileHasStructuredTypeEvidence` に戻り、純 `[MyAudit]` consumer は誤帰属せずに落ちる。スコープ尊重が重要: 別言語 / 別 path / test ツリーにある同名クラスが C# の metadata bypass を潰してはならない(参照側が既に graph 対応 join で言語修飾されているため、曖昧性は scope 内で判定すべき)。README の `impact` 契約も「active な `--lang` / `--path` / `--exclude-path` / `--exclude-tests` スコープ」と明記している。既存の C# attribute deps テスト `GetFileDependencies_MatchesCSharpAttributeSuffixConvention`、曖昧ではない単一定義の `impact` テスト、および既存の graph-queries dedupe テストは全てグリーンのまま。回帰テストとして `DbReaderTests.GetFileDependencies_CSharpAttributeAliasOnlyMatchesClassLikeTargets`(同名関数があっても虚偽 edge が出ないこと)、`DbReaderTests.GetFileDependencyHints_SuppressesCSharpAttributeMetadataBypassOnAmbiguousTarget`(impact スコープ内で `MyAuditAttribute` が複数ある場合に metadata heuristic hint を抑止)、`DbReaderTests.GetFileDependencyHints_CSharpAttributeMetadataBypassAppliesWhenTargetUnambiguous`(単一クラス定義のみのときは従来どおり `[MyAudit]` consumer を浮かせる)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsLangScope`(別言語の同名 annotation は `--lang csharp` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsPathScope`(スコープ外 `src/B/` の同名クラスは `src/A/` クエリを潰さない)、`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_RespectsExcludeTests`(test ツリーの同名クラスは `--exclude-tests` クエリを潰さない)を追加。`ResolveImpactFallbackNames` も併せて引き締め、C# の `Attribute` サフィックス別名は *解決済み定義自身* の名前にのみ適用するようにした: 以前は同一ファイル内の fallback 名全てに別名を付けていたため、`FooAttribute` を定義するファイルに nested で `BarAttribute` が存在するだけで `impact FooAttribute` が `[Bar]` 利用サイトを `FooAttribute` の影響範囲として誤報告していた。`DbReaderTests.GetFileDependencyHints_CSharpAttributeSuffixAlias_DoesNotLeakToSameFileSiblings` を追加して固定。さらに `GetFileDependencies` を再構成し、metadata 行(`attribute` / `annotation`)が bare-name の call-graph ターゲットに漏れないようにした: `logical_references` CTE に `is_metadata` フラグを付与し、`source_name_counts` の GROUP BY を `(source_file_id, source_path, source_lang, symbol_name, is_attribute_alias, is_metadata)` に拡張して metadata と非 metadata で同じ symbol_name の行を別グループ化する。`edges` CTE は全 metadata edge(合成別名だけでなく実体行も)に `tf.has_class_like_kind = 1` を要求し、さらに 2 つの CTE で残る誤検知を潰す: `metadata_raw_suppression` は同じ source file で合成別名 `FooAttribute` が class-like ターゲットに解決できているとき、生の C# `[Foo]` 行を落とす(`[Foo]` が `class FooAttribute` と無関係な同名 `class Foo` に fan-out しない)。`target_ambiguity` はスコープ内で同名 class-like ターゲットが複数あるとき metadata edge を落とす(曖昧な `[MyAudit]` は deps で fan-out させず、`impact` / `references` に判断を委ねる)。既存の `GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies`(`[JsonConverter(...)]` → `class JsonConverter : Attribute` をターゲット名に `Attribute` サフィックスがなくても固定)は、class-like ターゲットが 1 つしかないので `target_ambiguity` で残り、`is_metadata` / class-like 制限も受理し、合成 `JsonConverterAttribute` 別名は class-like に解決できないので `metadata_raw_suppression` も発火せず、そのまま通る。回帰テストとして `DbReaderTests.GetFileDependencies_CSharpAttributeRawDoesNotLeakToBareNameClass_WhenSuffixTargetExists`(別名が `class MyAuditAttribute` に解決できたとき生 `[MyAudit]` 行は抑止され、無関係な plain `class MyAudit` ファイルに依存しない)、`DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotLeakToSameNameMethodOrProperty`(完全形 `[MyAuditAttribute]` は同名メソッド / プロパティしか持たないファイルに依存しない)、`DbReaderTests.GetFileDependencies_CSharpAttributeDoesNotFanOutWhenMultipleSameNameAttributeClasses`(別名前空間に同名 attribute クラスが 2 つあると metadata edge はあいまいとして落とす)を追加。加えて `impact` の曖昧性ガード (`IsMetadataTargetUnambiguous`) と `deps` の曖昧性 CTE (`target_ambiguity`) も、path 単位ではなく symbol identity 単位で class-like 定義を数えるように引き締めた: `IsMetadataTargetUnambiguous` は `SELECT DISTINCT f.path, s.line, s.name` に変更し、C# でよくある 1 つの .cs ファイル内に別名前空間で同名 class-like が 2 つ定義されている形 (`namespace A { class FooAttribute { } } namespace B { class FooAttribute { } }`) でも ambiguity を 2 として検出する。`target_ambiguity` は `target_files` を `files` + `symbols` に JOIN し直して `COUNT(*)` を class-like symbol 行に対して取るため、同じ同名ファイル内重複ケースでも `[Foo]` metadata edge を正しく落とせる。path 単位カウントでは `SELECT DISTINCT f.path` も `target_files`(`dst.path` で GROUP BY)も同一ファイル内の 2 定義を 1 行に潰してしまうため、従来は count=1 として曖昧性を取りこぼし、`cdidx deps` と `cdidx impact` が同名 short-name metadata 参照を確定的な依存 / blast-radius edge として出していた。これは曖昧性に "duplicates in one file" を含める前提の設計契約に反していた。`deps` と `impact` 両方で symbol-identity 単位カウントを固定する `DbReaderTests.GetFileDependencies_CSharpAttributeAmbiguityCountsSameFileDuplicateClassDefinitions` と `DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CountsSameFileDuplicateDefinitions` を追加。さらに `IsMetadataTargetUnambiguous` の `--path` / `--exclude-path` パラメータを、他の reader 経路(search / references / deps 等)と同じ LIKE セマンティクスに揃えるため `EscapeLikeQuery` でエスケープしたうえで `%...%` で包んでバインドするようにした。生値のままだと `--path src/A/` のような CLI 形では LIKE が一致せず、リクエストされたサブツリーに同名 attribute クラスが 2 件並んでいても in-scope class-like カウントが 1 に過小化され、本来抑止すべき metadata bypass が誤発火してしまっていた。`DbReaderTests.GetFileDependencyHints_MetadataBypassAmbiguityGuard_CliPathPatternEscaping_SuppressesWhenInScopeIsAmbiguous` を追加して固定する。あわせて `CSharpNoArgAttributeRegex` を拡張し、属性名の直後に任意のジェネリック引数リスト `(?:\s*<[^\n]+?>)?` を許容するようにしたため、`[MyAudit]`・`[assembly: MyAttr]`・複数行の `[\n MyAttr\n]`・ネストジェネリックの `[MyAttr>]` や `[MyOther>>]` のような引数なしジェネリック属性も `attribute` 参照としてインデックスされるようになった。内側ジェネリックセグメントは非貪欲な `[^\n]+?` に変更し、内側の `>`(例: `Dictionary` を閉じる `>`)で match が打ち切られないようにした — 従来の `[^>\n]+` は `>` を除外していたため最初の内側 `>` で崩れ、ネストジェネリック属性の参照行が落ちていた。従来は `CallRegex`(`(` 必須)でも旧 no-arg regex(`]` / `,` / EOL で終端しなければならず、名前と `]` の間に `` があると拒否)でも拾えず、参照行が 0 件となり、`deps` / `impact` / `references` / `inspect` / `analyze_symbol` が実属性クラス `FooAttribute` への依存エッジを全て取りこぼしていた。`ReferenceExtractorTests.Extract_CsharpGenericNoArgAttribute_ClassifiedAsAttribute`、`ReferenceExtractorTests.Extract_CsharpNestedGenericNoArgAttribute_ClassifiedAsAttribute`、`DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_StillIndexedAndResolvesToAttributeClass`、`DbReaderTests.GetFileDependencies_CSharpGenericNoArgAttribute_AssemblyTarget_IsIndexed`、`DbReaderTests.GetFileDependencies_CSharpNestedGenericNoArgAttribute_ResolvesToAttributeClass` を追加して end-to-end の振る舞いを固定する。加えて、`impact` の曖昧性ガード (`IsMetadataTargetUnambiguous`) と `deps` の曖昧性判定 (`target_files` / `target_ambiguity`) は、C# クラスが有効な `[Attr]` metadata target になるのは「signature が `Attribute` サフィックス付き基底を継承している」場合 (`signature LIKE '%: %Attribute%'`) に限定するよう引き締めた。`: Attribute` 継承を書いていない plain `class MyAuditAttribute { }` はコンパイル時に `[MyAudit]` の target にならないため、曖昧性候補として数えると `impact` の影響範囲が過少報告され、`deps` 側でも同名 plain クラスが共存する scene で実 attribute クラスへの metadata edge が落ちてしまう。`target_files` に新しい `has_metadata_target_kind` MAX 列を追加して同じ C# signature フィルタを適用し、`metadata_raw_suppression` / `target_ambiguity` / `edges` の metadata 節はすべて `has_metadata_target_kind` に切り替えた。Java / Kotlin / Scala は portable な signature LIKE で metadata marker を表現しづらいため従来通り class-like 全体を候補に残す。`deps` 側の挙動を固定する `DbReaderTests.GetFileDependencies_CSharp_PlainClassWithAttributeSuffixName_DoesNotCountAsAmbiguity` を追加。Round-17 追加対応では、C# metadata target の signature フィルタを `'%: %Attribute%'`(直接基底が `Attribute` で終わることを要求)から `'%: %'`(何らかの継承節を持てば候補)に緩和した。`class MyAuditAttribute : BaseAudit` で `BaseAudit : Attribute` のような間接継承でも `[MyAudit]` の有効な target として認識される。SQL 時点で基底型を遡ることはできないため、継承節チェックは portable な近似として採用する — 非 attribute 由来のクラスは過剰に受理するが、無効な `[MyFoo]` をコンパイルすることはできず実参照にはならないので実害はない。共有ヘルパー `BuildCSharpMetadataTargetFilter` はさらに、`TryMigrateForRead` が追加できなかった legacy / read-only DB で `symbols.signature` 列が欠落しているときは `1 = 1`(metadata-eligibility 導入前の挙動)へ degrade するので、`deps` / `impact` クエリは `no such column: s.signature` でクラッシュせず結果を返す。間接継承パスを固定する `DbReaderTests.GetFileDependencies_CSharp_IndirectAttributeInheritance_ResolvesAsMetadataTarget` を追加。Round-18 では boolean の `BuildCSharpMetadataTargetFilter` ヘルパーを、ファイル別エイリアスを取り言語ごとに metadata-target 候補 kind を絞る `BuildMetadataTargetKindExpr(fileAlias)` 式へ置き換える。C# は継承節を持つ `kind = 'class'` のみ許可(`symbols.signature` が欠落する legacy DB では従来通り class-only にフォールバック)、JavaScript / TypeScript は `class` / `struct` / `interface` / `function` を許可(`function sealed(target) { ... }` のような factory 関数を `@sealed` が target とする decorator edge が `deps` に残る)、その他の graph 対応言語は従来通り `class` / `struct` / `interface` を受ける。旧フィルタは C# の signature 句だけを適用し、`s.kind IN ('class', 'struct', 'interface')` が外側に残っていたため、(a) C# で `struct MyAuditAttribute` / `interface MyAuditAttribute` が `System.Attribute` を継承できないにも関わらず metadata-target の ambiguity 候補として数えられ、(b) JS/TS で decorator の対象が `function sealed` factory の場合に `has_metadata_target_kind` が class-like を要求するために deps edge が落ちていた。新しい式は 3 箇所で使う: `IsMetadataTargetUnambiguous`(事前計算の `metadataTargetKindExprF` 経由)、`target_files` CTE の `has_metadata_target_kind` MAX 列、`target_ambiguity` CTE の JOIN 条件(言語ごとの kind narrowing が式に内包されたため、外側の `s.kind IN (...)` 絞り込みは削除)。`DbReaderTests.GetFileDependencies_JavaScriptFunctionDecorator_ResolvesAsDependency`(JS の `@sealed` class → `function sealed(target)` factory ファイルが落ちずに deps edge を生成)と `DbReaderTests.GetFileDependencies_CSharp_SameNameInterface_DoesNotBlockMetadataEdge`(同名の `interface MyAuditAttribute` が `[MyAudit]` → `class MyAuditAttribute : Attribute` を ambiguity 扱いにして抑止しない)を追加。Round-19 の追加では、C# 句を `s.signature IS NULL OR s.signature LIKE '%: %'` に緩め、`signature` 列は存在するが row の値が NULL の DB(`TryMigrateForRead` でその場 migration された列を持つが再インデックスを行っていない DB。既存行は NULL のまま)でも degrade するようにする。旧 `s.signature IS NOT NULL AND s.signature LIKE '%: %'` ではそうした DB で全行が reject され、本物の `[MyAudit]` → `class MyAuditAttribute : System.Attribute` edge がユーザーが `cdidx index` を再実行するまで silent に `deps` / `impact` から消えていた。列欠落時の `1 = 1` fallback と同じ精神で NULL signature を eligible 扱いにする。`DbReaderTests.GetFileDependencies_CSharp_LegacyDbWithNullSignature_StillResolvesAttributeEdge`(NULL signature の attribute class でも deps edge が解決する)を追加。Round-20 の追加対応では、NULL signature 時の fallback を「C# attribute 命名規約(`name LIKE '%Attribute'`)」にさらに絞り込み、NULL signature の非 attribute クラスを metadata-target 候補から除外する。`HelperClient : BaseService` のように `Attribute` サフィックスを持たないクラスは真の `[MyAudit]` → `MyAuditAttribute` edge に対する偽の曖昧性候補として数えられなくなり、legacy-migration DB でも silent に metadata edge が落ちない。`signature` 列自体が欠落している DB でも同じ命名規約 fallback に degrade する。既存の `GetFileDependencies_CSharp_LegacyDbWithNullSignature_StillResolvesAttributeEdge` 回帰は `MyAuditAttribute` が `Attribute` サフィックスを満たすので引き続きグリーン。加えて Round-20 では JavaScript / TypeScript の metadata-target 候補 kind から `interface` を外し(`class` + `function` のみ)、TypeScript の `interface` はコンパイル時の型専用構成子で runtime の decorator target になれないため、同名 `interface` が真の `function` / `class` decorator provider への edge を偽の曖昧性で潰す事態を防ぐ。`DbReaderTests.GetFileDependencies_CSharp_LegacyDbWithNullSignature_NonAttributeName_DoesNotBlockMetadataEdge`(NULL signature・非 `Attribute` 命名の無関係クラスが metadata edge を落とさない)と `DbReaderTests.GetFileDependencies_JavaScript_SameNameInterface_DoesNotBlockFunctionDecoratorEdge`(同名 `interface sealed` が `@sealed` → `function sealed(target)` の deps edge を抑止しない)を追加。残る edge case — 真の attribute class と同名の non-attribute class が `LIKE '%: %'` の過剰マッチで実 edge を silent に抑止する問題 — は schema レベルの `is_metadata_target` 列が必要になるため、#293 PR の scope を広げないために #435 で追跡する別 issue として意図的に deferred。対象: `src/CodeIndex/Database/DbReader.cs`、`src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 - **MCP の `callers` / `callees` の `kind` schema・CLI help・README の `--kind` オプション行が metadata kinds を有効フィルタ値として広告しないよう修正 (#293 追加対応)** — `callers` / `callees` の handler は `kind: "attribute"` / `kind: "annotation"` を usage error(CLI)/ `isError` ツールレスポンス(MCP)で拒否するにもかかわらず、MCP ツール schema の `kind` プロパティ説明は 5 種すべてを列挙した `"Filter by reference kind (call, instantiate, subscribe, attribute, annotation)"` のままで、`cdidx --help` の `--kind` 行も `callers` / `callees` を `references` と同じ行にまとめていた。さらに README の `--kind` オプション表(英語 436 行目 / 日本語 1307 行目)も、`callers` / `callees` で `--kind attribute` / `--kind annotation` を渡せるかのように案内していた。schema と handler の契約を一致させ、`callers` / `callees` の `kind` description を `"Filter by call-graph reference kind (call, instantiate, subscribe). Metadata kinds (attribute, annotation) are rejected here; use \`references\` for metadata enumeration."` に修正し、CLI の `--kind` help 行も `references`(5 種すべて受理)と `callers` / `callees`(call-graph 種別のみ、metadata kinds は拒否して `references` へ誘導)を分けるようにし、README の `--kind` 行(英語・日本語の両方)も `callers` / `callees` が `--kind attribute|annotation` を usage error で拒否すること、metadata 列挙の正規経路が `references --kind attribute|annotation` であることを明示する文言に直した。MCP の `references` ツール説明は全 reference kind を受け付ける契約のままなので変更なし。修正後の schema 文言を固定する `McpServerTests.ToolsList_CallersCalleesKindDescription_ExcludesMetadataKinds` を追加。対象: `src/CodeIndex/Mcp/McpToolDefinitions.cs`、`src/CodeIndex/Cli/ConsoleUi.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`、`README.md`。 - **`deps`・`impact`・`references`・`inspect`・`analyze_symbol` が metadata 参照を compile-time 依存として保持し、C# の attribute サフィックス命名規約を正規化 (#293 追加対応)** — `cdidx deps` の実装である `DbReader.GetFileDependencies` と、`cdidx impact` / MCP `impact_analysis` の heuristic file-hint フォールバックである `DbReader.GetFileDependencyHintsToResolvedType` は、logical reference CTE に metadata 参照も含める。attribute クラスを rename / 削除すれば `[JsonConverterAttribute]` のような注釈サイトも compile-time で壊れるため、ファイル単位の blast-radius 分析はそれらも本物の依存 edge として表示する必要があり、`logical_reference_kind` では `attribute` / `annotation` として正しくラベル付けされる(分類修正前は同じサイトが phantom `call` として漏れていた)。さらに `GetFileDependencies` は C# の attribute 命名規約を正規化する: 慣用的な `[Foo]` は `symbol_name = "Foo"` で保存されるが、定義クラスは `FooAttribute` 命名となるため、logical-reference CTE に C# `attribute` 種別の行をサフィックス `'Attribute'` を付与した別名コピーとして UNION ALL で追加する(元の symbol_name が既に `Attribute` で終わっている場合は重複しないようスキップ)。これにより target ファイル join が attribute クラス側のファイルへ到達する。`GetFileDependencyHintsToResolvedType` はさらに candidate ごとに `has_metadata_ref` フラグを集約し、寄与する参照が metadata だけである edge については `SourceFileHasStructuredTypeEvidence` guard をバイパスするため、`[MyAudit] class Svc` のような純 attribute consumer ファイルも `impact MyAuditAttribute` で出るようになった(以前は filter で落ちていた)。`ResolveImpactFallbackNames` も対称的に、`Attribute` で終わる C# クラス名にはサフィックスを外した別名を追加するため、BFS を使わない file-hint 経路でも source symbol_name が定義クラス名と一致しない `[Foo]` サイトを発見できる。同じ正規化をシンボル単位の参照 reader にも伝播する: `references`・`inspect`・MCP `analyze_symbol` を支える `DbReader.SearchReferences`・`CountSearchReferences`・`CountSearchReferencesTotal` は、新しいヘルパー `ComputeCSharpAttributeSuffixAlias` で C# 限定の `queryAttributeAlias` を計算し、exact(folded と NOCASE)・substring の両方の WHERE 句に OR で追加する。これにより `references MyAuditAttribute`(および `inspect MyAuditAttribute` / `analyze_symbol MyAuditAttribute`)でも慣用的な `[MyAudit]` 参照サイトが `deps` / `impact` と一貫して surface する。substring モードでは alias を二つ目の LIKE にせず exact-OR で束ねているため、ユーザーが `MyAuditAttribute` と打った時に `FooAuditLog` のような無関係な名前に over-match しない。alias は C# 限定なので、Java / Kotlin / Scala / TypeScript の annotation サイトには影響しない。`callers` / `callees` は共有の call-graph 種別 (`call` / `instantiate` / `subscribe`) に絞って `--kind attribute|annotation` を CLI / MCP 境界で拒否する契約なので、alias は意図的に適用しない(dynamic call graph と dependency graph は別契約)。さらに alias 節が誤一致を生まないようスコープする: `ComputeCSharpAttributeSuffixAlias` は `referenceKind` が `attribute` 以外の call-graph 種別のときは `null` を返す(`references FooAttribute --kind call` は厳格一致のままで `Foo()` に一致しない)、3 つの reader メソッドの SQL alias 節は `f.lang = 'csharp' AND r.reference_kind = 'attribute'` に限定するため、`--lang` / `--kind` 無指定の `references FooAttribute` でも Java の annotation や C# の call 行を拾わない。suffix 検出も `OrdinalIgnoreCase` にして case-insensitive 化したため、`references myauditattribute` / `inspect MyAuditATTRIBUTE` のような小文字・混在ケースでも alias `MyAudit` を生成でき、周辺の NOCASE / folded 契約と整合する。回帰テスト `DbReaderTests.GetFileDependencies_IncludesMetadataReferencesAsCompileTimeDependencies`(runtime と metadata の両方の edge が出ることを固定)、`DbReaderTests.GetFileDependencies_MatchesCSharpAttributeSuffixConvention`(`deps` 経由での `[MyAudit]` → `MyAuditAttribute` 正規化)、`DbReaderTests.SearchReferences_MatchesCSharpAttributeSuffixConvention_Substring` / `_Exact` / `_CSharpAttributeSuffixAliasDoesNotBleedToOtherLanguages`(`references` / `inspect` / `analyze_symbol` での同一正規化と C# 限定スコープ)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_NotAppliedToCallKind`(`--kind call` で alias が発火しないこと)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_UnscopedLangStillLimitsToCSharpAttributeRows`(無指定でも Java annotation や C# call 行に一致しないこと)、`DbReaderTests.SearchReferences_CSharpAttributeSuffixAlias_CaseInsensitiveQuery`(小文字・混在ケースのクエリでも alias を生成)を追加。対象: `src/CodeIndex/Database/DbReader.cs`、`tests/CodeIndex.Tests/DbReaderTests.cs`。 - **`callers` / `callees` が CLI と MCP 境界で `--kind attribute|annotation` を拒否し、Swift / Gradle の `@` metadata を `annotation` に再分類 (#293 追加対応)** — metadata 行 (`attribute` / `annotation`) は注釈対象そのものではなく body-range の外側シンボルに帰属するため、`callers Obsolete --kind attribute` は `[Obsolete] void M()` を `M` ではなく外側クラスの下に黙って返し、`[assembly: CLSCompliant]` や Kotlin の `@file:JvmName` のような file-level target は `container_name = NULL` で完全脱落していた。`QueryCommandRunner.RunCallers` / `RunCallees` は `--kind attribute` / `--kind annotation` を `CommandExitCodes.UsageError` で弾き、MCP の `callers` / `callees` ツールは `isError` ツールレスポンスを返すようにした。どちらの経路も、metadata 列挙の正しいパスである `references --kind attribute|annotation` へ誘導する。あわせて `AnnotationLanguages` に `swift` と `gradle` を追加し、Swift の `@available(iOS 13.0, *)` / `@objc` / `@MainActor` や Gradle / Groovy の `@CompileStatic` / `@TaskAction` を `call` ではなく `annotation` として記録するようにした(修正前は引数付きの `@Name(args)` 形は `callers` / `callees` / `hotspots` / `impact` に phantom call edge として漏れ、引数なしの `@objc` / `@MainActor` / `@CompileStatic` / `@TaskAction` 形はこの集合で gate されていた no-arg annotation 出力から外れてインデックス自体に残らなかった)。回帰テストとして `QueryCommandRunnerTests.RunCallersCallees_RejectMetadataKind_WithUsageError`(theory 4 ケース)、`McpServerTests.ToolsCall_CallersOrCallees_MetadataKindReturnsToolError`(theory 4 ケース)、`ReferenceExtractorTests.Extract_SwiftAttributeWithArgs_ClassifiedAsAnnotation`、`ReferenceExtractorTests.Extract_GradleAnnotation_ClassifiedAsAnnotation` を追加。対象: `src/CodeIndex/Cli/QueryCommandRunner.cs`、`src/CodeIndex/Mcp/McpToolHandlers.cs`、`src/CodeIndex/Indexer/ReferenceExtractor.cs`、`tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`、`tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。 diff --git a/src/CodeIndex/Database/DbReader.cs b/src/CodeIndex/Database/DbReader.cs index b183737b35..f6166fc8f2 100644 --- a/src/CodeIndex/Database/DbReader.cs +++ b/src/CodeIndex/Database/DbReader.cs @@ -2580,50 +2580,72 @@ internal string GetFileColumnSql(string columnName, string? fallbackSql = null) // plausible metadata target (`[Attribute]` / `@Annotation` / `@decorator`). // Rules by language: // - C# (`csharp`): only `kind = 'class'` with an inheritance clause - // (signature LIKE '%: %'). `struct` / `interface` cannot be attribute - // targets even when they share the `MyAuditAttribute` name, so they - // must not count toward the ambiguity guard or produce metadata - // edges. Transitive base-type resolution is not available at SQL - // time, so "has an inheritance clause" is the portable approximation - // for direct `: Attribute` plus indirect `: BaseAudit` where - // BaseAudit itself derives from Attribute. Legacy DBs without a - // `symbols.signature` column degrade to `kind = 'class'` only so - // read-only / pre-migration indexes still return a result instead of - // crashing with `no such column`. - // - JS / TS (`javascript` / `typescript`): decorators legitimately - // target both class-like constructs AND factory `function` - // definitions (e.g. `function sealed(target) {}` used as - // `@sealed class Foo {}`), so `function` must be accepted too. + // (`signature LIKE '%: %'`). Transitive base-type resolution is not + // available at SQL time, so "has any inheritance clause" is the + // portable approximation for direct `: Attribute` plus indirect + // `: BaseAudit` where `BaseAudit` itself derives from Attribute. + // Extractor-driven authoritative `is_metadata_target` classification is + // tracked as a follow-up (issue #435) and would let `deps` / `impact` + // reject non-attribute classes like `class MyAuditAttribute : BaseService` + // that this heuristic cannot distinguish. + // For legacy-migration DBs whose `signature` column exists but stores + // NULL for individual C# class rows, fall back to the canonical C# + // attribute-naming convention (`name LIKE '%Attribute'`). This is + // strictly narrower than the previous unconditional NULL-signature + // pass-through and prevents every NULL-signature class from being + // treated as a plausible metadata target. DBs without any `signature` + // column at all degrade to the same naming heuristic. + // - JS / TS (`javascript` / `typescript`): decorators target runtime + // entities — classes and factory `function` definitions + // (e.g. `function sealed(target) {}` used as `@sealed class Foo {}`). + // TypeScript `interface` is a compile-time type-only construct and + // cannot be a decorator target at runtime; including it would let a + // same-name `interface` inject false ambiguity against the real + // `function` or `class` provider and silently drop the decorator edge. // - Everything else (Java `@interface`, Kotlin `annotation class`, // Scala annotation classes, etc.): the annotation target is a // class-like declaration, so keep the original class-like candidate // set (`class` / `struct` / `interface`). // `deps` と `impact` で共有する言語別 metadata-target 適格性判定。 - // C# は class 限定 + 継承節チェック(signature 列欠落時、または row 側 signature が - // NULL の legacy-migration DB では class 限定のみへ縮退)。 - // JS / TS は decorator が function factory も対象にするため function を許容。 + // C# は `kind = 'class'` かつ継承節を持つ行を対象とする(直接/間接の Attribute 継承を + // ポータブルに近似するため)。signature 列は存在するが値が NULL の legacy-migration + // DB では C# の命名規約 `name LIKE '%Attribute'` にフォールバック — 従来の + // 無条件許容より厳密で、NULL-signature の全 class を metadata target 扱いしない。 + // signature 列自体が無い旧 DB も同じ命名規約ヒューリスティックを使う。 + // extractor 主導の authoritative な `is_metadata_target` 判定は follow-up(issue #435) + // として追跡しており、schema 化すれば `class MyAuditAttribute : BaseService` のような + // 非 attribute 継承も厳密に除外できるが、現状のヒューリスティックでは判別できない。 + // JS / TS は decorator が runtime entity (class / factory function) のみ対象。 + // TypeScript の `interface` は型定義で runtime decorator target にならないため除外し、 + // 同名 `interface` が本物の `function` / `class` provider を曖昧化するのを防ぐ。 // それ以外は従来どおり class-like を候補にする。 private string BuildMetadataTargetKindExpr(string fileAlias) { // C# clause — class only (interface/struct cannot be attribute targets). - // When the `signature` column exists but an individual row's signature is - // NULL (e.g. a DB whose schema was migrated in-place without reindexing), - // we cannot verify the inheritance clause, so we conservatively keep such - // rows eligible just like the column-missing `1 = 1` fallback. Otherwise a - // real `[MyAudit]` → `class MyAuditAttribute : System.Attribute` edge would - // silently disappear from `deps` / `impact` on legacy-migration DBs. + // Non-NULL signature: accept any inheritance clause (`: %`) as the portable + // approximation of direct/indirect Attribute derivation (see issue #435). + // NULL signature: require the C# attribute naming convention + // (`name LIKE '%Attribute'`). This is strictly narrower than the previous + // unconditional NULL pass-through and prevents arbitrary NULL-signature + // classes on a legacy-migration DB from being treated as metadata targets. + // DBs missing the `signature` column entirely degrade to the same naming + // heuristic. // C# は class のみ(interface/struct は attribute target にできない)。 - // signature 列は存在するが row の signature が NULL のケース(その場 migration で列だけ - // 追加された legacy DB)では inheritance clause を検証できないため、列欠落時の - // `1 = 1` fallback と同じ扱いで eligibility を保守的に維持する。そうしないと - // 本物の `[MyAudit]` → `class MyAuditAttribute : System.Attribute` edge が - // legacy-migration DB で silent に落ちる。 + // 非 NULL signature は従来どおり継承節 `: %` で判定(直接/間接 Attribute の近似)。 + // NULL signature は C# 命名規約 `name LIKE '%Attribute'` に縮退 — 従来の + // 無条件許容より厳密で、legacy-migration DB で任意の NULL-signature class が + // metadata target 扱いされるのを防ぐ。signature 列欠落 DB も同じ命名規約を使う。 var csharpClause = _symbolColumns.Contains("signature") - ? $"({fileAlias}.lang = 'csharp' AND s.kind = 'class' AND (s.signature IS NULL OR s.signature LIKE '%: %'))" - : $"({fileAlias}.lang = 'csharp' AND s.kind = 'class')"; - // JS / TS clause — decorators target classes and factory functions. - // JS / TS は decorator が class と factory function を対象にする。 - var jsClause = $"({fileAlias}.lang IN ('javascript','typescript') AND s.kind IN ('class','struct','interface','function'))"; + ? $"({fileAlias}.lang = 'csharp' AND s.kind = 'class' AND ((s.signature IS NOT NULL AND s.signature LIKE '%: %') OR (s.signature IS NULL AND s.name LIKE '%Attribute')))" + : $"({fileAlias}.lang = 'csharp' AND s.kind = 'class' AND s.name LIKE '%Attribute')"; + // JS / TS clause — decorators target runtime entities (classes and factory + // functions). TS `interface` is a type-only construct that cannot be a + // decorator target, so excluding it avoids false ambiguity against a + // real function/class provider sharing the same name. + // JS / TS: decorator は runtime entity (class / factory function) のみ対象。 + // TS の `interface` は型定義のため除外しないと同名 interface が偽の曖昧さを + // 発生させる。 + var jsClause = $"({fileAlias}.lang IN ('javascript','typescript') AND s.kind IN ('class','function'))"; // All other graph-supported languages keep the original class-like set. // その他の graph 対応言語は従来どおり class-like を対象にする。 var otherClause = $"({fileAlias}.lang NOT IN ('csharp','javascript','typescript') AND s.kind IN ('class','struct','interface'))"; diff --git a/tests/CodeIndex.Tests/DbReaderTests.cs b/tests/CodeIndex.Tests/DbReaderTests.cs index 978dae6f06..69c643bd90 100644 --- a/tests/CodeIndex.Tests/DbReaderTests.cs +++ b/tests/CodeIndex.Tests/DbReaderTests.cs @@ -3408,6 +3408,107 @@ public class Svc d.TargetPath == "src/MyAuditAttribute.cs"); } + [Fact] + public void GetFileDependencies_CSharp_LegacyDbWithNullSignature_NonAttributeName_DoesNotBlockMetadataEdge() + { + // issue #293 round-20: the NULL-signature fallback must not treat + // arbitrary classes as metadata targets. Before this round the clause + // accepted `signature IS NULL` for every C# `class`, so on a legacy-migration + // DB a non-attribute class named `HelperClient` could share a name with an + // attribute-applied site and silently inject false ambiguity. The tightened + // fallback requires the canonical C# attribute naming convention + // (`name LIKE '%Attribute'`), so a NULL-signature `HelperClient` is no + // longer counted and the real `[MyAudit]` edge to `MyAuditAttribute` + // survives even when both rows have NULL signatures. + // issue #293 round-20: NULL-signature フォールバックが任意の class を + // metadata target 扱いしないこと。以前は legacy-migration DB で + // `signature IS NULL` のすべての C# class を許容しており、attribute 名と + // 同名の非 attribute class (`HelperClient`) が偽の曖昧さを発生させ得た。 + // 新しいフォールバックは C# の命名規約 `name LIKE '%Attribute'` を要求する + // ため、NULL-sig かつ非 *Attribute 名の class は候補から外れ、本物の + // `[MyAudit]` → `MyAuditAttribute` edge は両行の signature が NULL でも + // 残る。 + InsertIndexedFile("src/MyAuditAttribute.cs", "csharp", + """ + public sealed class MyAuditAttribute : System.Attribute + { + } + """); + InsertIndexedFile("src/HelperClient.cs", "csharp", + """ + namespace Unrelated; + + public class HelperClient : BaseService + { + } + """); + InsertIndexedFile("src/Svc.cs", "csharp", + """ + [MyAudit] + public class Svc + { + } + """); + + // Simulate partial-migration: all C# class rows have NULL signature. + // partial-migration 再現: すべての C# class 行の signature を NULL 化。 + using (var cmd = _db.Connection.CreateCommand()) + { + cmd.CommandText = "UPDATE symbols SET signature = NULL WHERE kind = 'class'"; + cmd.ExecuteNonQuery(); + } + + var deps = _reader.GetFileDependencies( + limit: 50, + lang: "csharp"); + + Assert.Contains(deps, d => + d.SourcePath == "src/Svc.cs" && + d.TargetPath == "src/MyAuditAttribute.cs"); + } + + [Fact] + public void GetFileDependencies_JavaScript_SameNameInterface_DoesNotBlockFunctionDecoratorEdge() + { + // issue #293 round-20: TypeScript `interface` is a compile-time type-only + // construct and cannot be a runtime decorator target, so a same-name + // `interface` must NOT count toward metadata-target ambiguity against a + // real `function` provider. The metadata-target predicate for JS/TS + // therefore restricts candidate kinds to `class` and `function` only. + // issue #293 round-20: TS の `interface` はコンパイル時型のため runtime + // decorator target になれない。同名 `interface` が本物の `function` + // provider への decorator edge を潰さないよう、JS/TS の metadata-target + // 候補 kind は `class` と `function` に限定する。 + InsertIndexedFile("src/decorators.ts", "typescript", + """ + export function sealed(target: any): void { + Object.freeze(target); + } + """); + InsertIndexedFile("src/types.ts", "typescript", + """ + export interface sealed { + readonly frozen: boolean; + } + """); + InsertIndexedFile("src/model.ts", "typescript", + """ + import { sealed } from './decorators'; + + @sealed + class Foo { + } + """); + + var deps = _reader.GetFileDependencies( + limit: 50, + lang: "typescript"); + + Assert.Contains(deps, d => + d.SourcePath == "src/model.ts" && + d.TargetPath == "src/decorators.ts"); + } + [Fact] public void GetFileDependencies_CSharp_SameNameInterface_DoesNotBlockMetadataEdge() {