diff --git a/CHANGELOG.md b/CHANGELOG.md index 059deb5697..796caf2ac4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **T-SQL / SQL Server DDL kinds now surface as symbols (#230)** — The `SymbolExtractor` SQL row previously only captured `CREATE TABLE` / `(MATERIALIZED) VIEW` / `PROCEDURE` / `FUNCTION` / `TRIGGER` / `TYPE` / `SCHEMA` / `SEQUENCE` / `DOMAIN` / `EXTENSION` / `INDEX` and `ALTER TABLE`, so on T-SQL / SQL Server migration scripts the short-form `CREATE PROC`, T-SQL `CREATE OR ALTER` (SQL Server 2016+), `CREATE SYNONYM`, `CREATE DATABASE` / `LOGIN` / `USER` / `ROLE` / `CERTIFICATE`, `CREATE PARTITION FUNCTION` / `PARTITION SCHEME`, `CREATE FULLTEXT CATALOG`, and every non-`TABLE` `ALTER` form (procedures, views, sequences, synonyms, principals, etc.) were silently dropped from the index — making `symbols` / `definition` / `outline` / `inspect` look near-empty on enterprise .NET backends whose primary surface is a stored-procedure or schema-migration repo. The TABLE/VIEW and PROCEDURE/PROC/FUNCTION/TRIGGER rows now accept `CREATE OR (REPLACE|ALTER)`, the procedure row accepts the `PROC` synonym, dedicated rows were added for `CREATE SYNONYM` (class), `CREATE DATABASE|LOGIN|USER|ROLE|CERTIFICATE` (class), `CREATE PARTITION FUNCTION` (function) / `CREATE PARTITION SCHEME` (class), `CREATE FULLTEXT CATALOG` (class), and the `ALTER` side is split into kind-consistent rows so procedure-like `ALTER` (`PROCEDURE` / `PROC` / `FUNCTION` / `TRIGGER` / `PARTITION FUNCTION`) emits kind `function`, `ALTER SCHEMA` emits kind `namespace`, `ALTER EXTENSION` emits kind `import`, and the remaining object kinds (`TABLE`, `(MATERIALIZED) VIEW`, `SEQUENCE`, `SYNONYM`, `LOGIN`, `USER`, `ROLE`, `DATABASE`, `CERTIFICATE`, `INDEX`, `TYPE`, `DOMAIN`, `PARTITION SCHEME`, `FULLTEXT CATALOG`) emit kind `class`. Splitting the `ALTER` row by kind keeps the same object's `CREATE` and `ALTER` symbols under the same `kind` so `symbols --kind function` / `definition` / `inspect` do not duplicate across kinds. The `ALTER` side also now picks up `CERTIFICATE`, `DOMAIN`, and `EXTENSION` that were missing from the original row so every non-`TABLE` `ALTER` form is actually captured rather than just claimed. Every SQL row now shares an identifier shape that accepts PG double-quoted (`"name"`), T-SQL bracketed (`[name]`), or bare (`\w+`) tokens with optional dotted qualifiers such as `[dbo].[sp_X]`, `"schema"."name"`, and `schema.name`. The existing `CREATE INDEX ON tbl` / anonymous-name guard (`(?!ON\b)`), the `CREATE TYPE ... AS ENUM` Postgres branch, and the `CREATE SCHEMA AUTHORIZATION` branch are preserved. Body-less `namespace`-kind symbols from SQL (`CREATE SCHEMA sales;`, `ALTER SCHEMA sales TRANSFER ...;`) would previously trip the `IsFileScopedNamespace` check that wraps every subsequent top-level symbol under a C# `namespace X;` file-scoped declaration — so `ALTER FUNCTION dbo.fn_Total`, `ALTER PARTITION FUNCTION pf_OrdersByYear`, and every later declaration in the same file were emitted with `container_kind='namespace'` / `container_name='sales'`, breaking `definition` / `inspect` / family grouping on real migration scripts. The helper now additionally requires the signature to start with the `namespace` keyword, so SQL schema rows no longer act as file-scoped containers; SQL schema scoping is still expressed through qualified names (`sales.Money`), matching how the rest of cdidx reads SQL. Added a dedicated `Extract_SQL_DetectsTSqlDdlKinds` regression test that locks all thirteen new T-SQL `CREATE` shapes and every new `ALTER` kind contract (`ALTER PROCEDURE` / `ALTER FUNCTION` / `ALTER PARTITION FUNCTION` → `function`; `ALTER SCHEMA` → `namespace`; `ALTER EXTENSION` → `import`; `ALTER CERTIFICATE` / `ALTER DOMAIN` → `class`) including the `CREATE PROC` + `ALTER PROCEDURE` pair that must emit two `dbo.sp_DailyReport` rows both under kind `function`, and asserts that no later top-level SQL symbol is wrapped under `namespace=sales` so the file-scoped-namespace container pollution cannot regress. `GO` batch boundary awareness and `BEGIN ... END` structural nesting are deliberately out of scope here and remain filed in #230 as stretch items. Affected: `src/CodeIndex/Indexer/SymbolExtractor.cs`, `tests/CodeIndex.Tests/SymbolExtractorTests.cs`. Closes #230. #### Fixed +- **Regression lock for C# interpolated and verbatim string reference extraction (#264)** — The original issue report alleged that `ReferenceExtractor` hid call-site references inside C# `$"..."` interpolation holes and leaked phantom references from `@"..."` verbatim string bodies. On the current `StructuralLineMasker` implementation the C# paths already handle single-line `$"..."`, multi-line `$@"..."` / `@$"..."`, and non-interpolated `@"..."` correctly — interpolation-hole call sites are captured with correct `ContainerName`, and multi-line verbatim bodies never leak phantom edges — but no regression test pinned that behavior end-to-end for the exact fixture in the issue report, so any future refactor of the masker's C# frame-stack handling could silently reintroduce the bug. Added `Extract_CsharpInterpolatedAndVerbatimStrings_Issue264_Repro_CapturesHoleCallsAndSuppressesPhantoms` which feeds the issue's exact fixture (`$"Hello {Helper.GetName()}"`, `$"Age: {Helper.GetAge()} years"`, `$"Nested {Helper.Format(Helper.GetName())}"`, a multi-line `$@"..."` with an interpolation hole, a plain non-interpolated call site, and a multi-line `@"..."` verbatim body containing `PhantomCall()` / `PhantomTable()` / `MoreFake()`) through `ReferenceExtractor.Extract` and pins four invariants at once: `Helper.GetName` is captured exactly four times, `Helper.GetAge` and `Helper.Format` are each captured at least once, every captured call's `ContainerName` resolves to `Work`, and no reference row ever surfaces for the phantom identifiers embedded in the verbatim body. Affected: `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`. Closes #264. - **C# nested `new interface` (member-hiding a base-class nested interface) is now pinned by a dedicated regression (#376)** — Issue #376 reported that `public new interface INested { ... }` nested inside a derived class was silently dropped because the C# interface regex modifier list was `(?:partial|unsafe)` and did not include `new`. The free-modifier-order rewrite for #355 already widened the list to `(?:partial|unsafe|file|new)`, so the current binary already emits two `interface INested` symbols — one nested in `Base`, one nested in `Derived` — and the #376 repro is no longer reproducible. This change locks that behavior in with a dedicated `Extract_CSharp_NewNestedInterface_MemberHiding` regression that runs the exact fixture from the issue and asserts both nested interfaces are extracted (`ContainerName == "Base"` and `ContainerName == "Derived"`), so a future modifier-list edit cannot regress the #376 case without a failing test. Affected: `tests/CodeIndex.Tests/SymbolExtractorTests.cs`. Closes #376. - **C# 8 `readonly` instance indexers on structs are now captured as `Item` symbols (#352)** — `SymbolExtractor` previously dropped `public readonly int this[int i] => _arr[i];` and `public readonly string this[string key] { get => key; }` silently, so `definition`, `symbols`, `outline`, and `inspect` undercounted `readonly`-heavy slice / span / immutable-wrapper struct surfaces. The indexer row already accepts `readonly` in the free-order modifier slot alongside `unsafe` / `extern` / `ref (readonly)` following the #355 rewrite, and this change locks that behavior in with a dedicated `Extract_CSharp_DetectsReadonlyIndexers` regression covering expression-body, block-body, and generic-return (`public readonly T this[int i] { get => _items[i]; }`) shapes plus non-readonly / readonly-method baselines so a future regex refactor cannot regress the #352 case without a failing test. Affected: `tests/CodeIndex.Tests/SymbolExtractorTests.cs`. - **`cdidx search` now caps per-line snippet width, so a single match in a minified / transpiled file no longer returns hundreds of KB of output (#196)** — `cdidx search --snippet-lines N` already limited the *number* of snippet lines returned per result, but it did not cap the *width* of each individual line. A match inside a minified / transpiled single-line file (one ~120 KB physical line), a generated fixture, or an obfuscated payload therefore returned the entire raw line verbatim — JSON output ballooned to hundreds of KB per result, human TUI output wrapped unreadably, the MCP `search` tool's `structuredContent` blew up AI token budgets, and the issue's repro (a 120,009-char Python line with one match) returned the full 120 KB payload even with `--snippet-lines 1`. `SearchSnippetFormatter` now runs every snippet line through the shared `LineWidthFormatter.ClampLine` helper already used by `find` / `references` / `excerpt` / `inspect`, so match lines are clamped around the first match token (keeping the token visible with surrounding `...(+N)...` markers) and non-match context lines head-clamp with a trailing `...(+N)...` marker. `SearchHighlight` now carries `OriginalLineLength` and `Truncated` fields so AI clients can detect clamped lines, and `CompactSearchResult` / `SearchSnippetExcerpt` expose a `TruncatedLineCount` summary field. The new `--max-line-width ` flag (CLI) and `maxLineWidth` argument (MCP `search`) share the exact same contract and defaults as the other commands (`DefaultMaxLineWidth=512`, `MaxAllowedLineWidth=4096`), so users and AI clients don't have to remember a separate knob for `search`. `--max-line-width` is added to the search usage line, help description, and bash / zsh / fish completions (fish already emitted `search` as a subcommand but previously did not advertise `--max-line-width` for it). The bash and zsh `search` completion branches are also realigned with the actual `RunSearch` contract — `--exact-name` (which `RunSearch` rejects with a usage error) is dropped from the candidate set, and the accepted `--top` (a `--limit` alias) and `--no-dedup` flags are added. The MCP `maxLineWidth` helper now also fails closed on values above `MaxAllowedLineWidth` (schema ceiling `4096`) instead of silently clamping them, so a schema-violating `maxLineWidth: 4097` now surfaces as an explicit tool error rather than a silently rewritten success. The CLI `--max-line-width` parser matches the same contract on `search` / `references` / `inspect` / `excerpt` (via `ParseArgs`) and `find` (via `ValidateFindArgs`): values above `MaxAllowedLineWidth` now abort with `UsageError` and empty stdout instead of silently falling back to `ClampMaxLineWidth`, so CLI and MCP agree on the ceiling contract that automation depends on. For `find` in particular, `ValidateFindArgs` is now inline-`=` aware — it uses `TrySplitInlineOptionValue` to read an inline `--opt=value` token as `(--opt, value)` for its own validation without rewriting the arg list, so the pre-flight validator no longer rejects the documented `--opt=` form with `unsupported option for find: --max-line-width=4097` while the other query commands accept it through `ParseArgs`. The arg list passed on to `ParseArgs` is left untouched, so legitimate inline values that themselves start with `--` (e.g. `--path=--literal.txt` for a path literally named `--literal.txt`, exactly the shape the existing CLI hint recommends) are preserved verbatim for `ParseArgs` to handle. Smoke-tested against the issue's repro: a 120,009-char single-line file with one match now produces ~293 bytes of output by default, ~137 bytes at `--max-line-width 200`, and ~2085 bytes at `--max-line-width 4096`; the JSON `snippet` field carries the `...(+119914)...` marker around the first match token, and `truncated_line_count` / `highlights[].truncated` / `highlights[].original_line_length` are populated. Short lines that fit within `maxLineWidth` are unchanged. Added `SearchSnippetFormatter` regression tests pinning the long-match-line clamp, long non-match-line clamp, `CompactSearchResult` truncation metadata, and the "short line stays verbatim" guarantee, updated the `ConsoleUi` usage and help-text assertions, and added a `QueryEntrypoints_MaxLineWidthAboveCeilingFailClosed_Issue196` theory that drives `RunSearch` / `RunReferences` / `RunInspect` / `RunFind` / `RunExcerpt` with `--max-line-width 4097` (and `search` with `8192`) and pins the `UsageError` exit, empty stdout, the explicit `"--max-line-width must be less than or equal to 4096"` stderr message, and the `Usage:` line, with matching inline-`=` rows (`--max-line-width=4097`) for `search` / `references` / `inspect` / `find` / `excerpt` so the inline-`=` validator contract is pinned alongside the space-separated form, plus a `RunFind_PathFilterAcceptsRecognizedOptionTokenViaInlineValue` regression that drives `find Alpha --db= --path=--json-dir --count --json` end to end and pins that `find` honors inline `--path=` values starting with `--`, matching the `search` / `files` contract. Affected: `src/CodeIndex/Cli/SearchSnippetFormatter.cs`, `src/CodeIndex/Cli/QueryCommandRunner.cs`, `src/CodeIndex/Cli/ConsoleUi.cs`, `src/CodeIndex/Mcp/McpToolDefinitions.cs`, `src/CodeIndex/Mcp/McpToolHandlers.cs`, `tests/CodeIndex.Tests/SearchSnippetFormatterTests.cs`, `tests/CodeIndex.Tests/ConsoleUiTests.cs`, `tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`, `tests/CodeIndex.Tests/McpServerTests.cs`. Closes #196. @@ -715,6 +716,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **T-SQL / SQL Server の DDL 種別をシンボルとして拾えるようにした (#230)** — `SymbolExtractor` の SQL 行は従来 `CREATE TABLE` / `(MATERIALIZED) VIEW` / `PROCEDURE` / `FUNCTION` / `TRIGGER` / `TYPE` / `SCHEMA` / `SEQUENCE` / `DOMAIN` / `EXTENSION` / `INDEX` と `ALTER TABLE` しか捕捉しておらず、T-SQL / SQL Server のマイグレーション系スクリプトでは短縮形 `CREATE PROC`、T-SQL の `CREATE OR ALTER`(SQL Server 2016+)、`CREATE SYNONYM`、`CREATE DATABASE` / `LOGIN` / `USER` / `ROLE` / `CERTIFICATE`、`CREATE PARTITION FUNCTION` / `PARTITION SCHEME`、`CREATE FULLTEXT CATALOG`、および `TABLE` 以外の `ALTER`(プロシージャ、ビュー、シーケンス、シノニム、プリンシパル等)がすべてインデックスから黙って落ちており、ストアドプロシージャやスキーマ・マイグレーション中心のエンタープライズ .NET バックエンドでは `symbols` / `definition` / `outline` / `inspect` がほぼ空に見えてしまっていた。TABLE/VIEW 行と PROCEDURE/PROC/FUNCTION/TRIGGER 行を `CREATE OR (REPLACE|ALTER)` 対応に拡張し、プロシージャ行に `PROC` 短縮形を追加した。`CREATE SYNONYM`(class)、`CREATE DATABASE|LOGIN|USER|ROLE|CERTIFICATE`(class)、`CREATE PARTITION FUNCTION`(function)/ `CREATE PARTITION SCHEME`(class)、`CREATE FULLTEXT CATALOG`(class)の専用行を追加し、`ALTER` 側も kind を揃える形で分割した — プロシージャ類の `ALTER`(`PROCEDURE` / `PROC` / `FUNCTION` / `TRIGGER` / `PARTITION FUNCTION`)は `function`、`ALTER SCHEMA` は `namespace`、`ALTER EXTENSION` は `import`、残りのオブジェクト種別(`TABLE`、`(MATERIALIZED) VIEW`、`SEQUENCE`、`SYNONYM`、`LOGIN`、`USER`、`ROLE`、`DATABASE`、`CERTIFICATE`、`INDEX`、`TYPE`、`DOMAIN`、`PARTITION SCHEME`、`FULLTEXT CATALOG`)は `class` を返す。`ALTER` を kind ごとに分けることで、同一オブジェクトの `CREATE` と `ALTER` が同じ `kind` に揃い、`symbols --kind function` / `definition` / `inspect` が種別またぎで重複しない。`ALTER` 側には元の行で漏れていた `CERTIFICATE` / `DOMAIN` / `EXTENSION` も追加し、`TABLE` 以外の `ALTER` を本当に網羅する形にした。SQL の全行が共通の識別子形状を共有し、PG の `"name"`、T-SQL の `[name]`、裸 `\w+` トークンをドット修飾できる形(`[dbo].[sp_X]` / `"schema"."name"` / `schema.name`)を全行で受け入れる。既存の `CREATE INDEX ON tbl` 無名ガード(`(?!ON\b)`)、Postgres の `CREATE TYPE ... AS ENUM` 分岐、`CREATE SCHEMA AUTHORIZATION` 分岐は維持している。さらに、SQL の `CREATE SCHEMA sales;` / `ALTER SCHEMA sales TRANSFER ...;` のような body 無しの `namespace` kind 行が、C# の `namespace X;` file-scoped namespace を想定した `IsFileScopedNamespace` 判定を発火させ、以降のトップレベルシンボル全部を `container_kind='namespace'` / `container_name='sales'` で包んでしまう不具合を修正した(`ALTER FUNCTION dbo.fn_Total` や `ALTER PARTITION FUNCTION pf_OrdersByYear` の container が汚染され、`definition` / `inspect` / family grouping が崩れていた)。`IsFileScopedNamespace` は signature が `namespace` キーワードで始まる行だけを対象とするよう追加ガードを入れ、SQL の schema は file-scoped container にならないようにした(SQL では schema スコープは `sales.Money` のような修飾名で表現する方針を維持)。`Extract_SQL_DetectsTSqlDdlKinds` 回帰テストを追加し、新規 13 形の `CREATE` に加え、新しい `ALTER` の kind 契約(`ALTER PROCEDURE` / `ALTER FUNCTION` / `ALTER PARTITION FUNCTION` → `function`、`ALTER SCHEMA` → `namespace`、`ALTER EXTENSION` → `import`、`ALTER CERTIFICATE` / `ALTER DOMAIN` → `class`)もすべて固定し、`CREATE PROC` と `ALTER PROCEDURE` のペアで `dbo.sp_DailyReport` が同じ `function` kind で 2 件出ることも含めて押さえ、後続のトップレベルシンボルが `namespace=sales` 配下に吸い込まれないことも固定した。`GO` バッチ境界や `BEGIN ... END` の構造ネストは本 issue のストレッチ項目として意図的に対象外。対象: `src/CodeIndex/Indexer/SymbolExtractor.cs`、`tests/CodeIndex.Tests/SymbolExtractorTests.cs`。Closes #230。 #### 修正 +- **C# 補間文字列 / 逐語文字列に対する参照抽出の回帰テストを追加 (#264)** — issue 原文は `ReferenceExtractor` が C# の `$"..."` 補間ホール内の呼び出しを落とし、`@"..."` 逐語文字列本体から phantom 参照を漏らしていると報告していた。現時点の `StructuralLineMasker` 実装では、単行 `$"..."`、複数行 `$@"..."` / `@$"..."`、非補間 `@"..."` のいずれも C# 経路で正しく処理されており、補間ホール内の呼び出しは正しい `ContainerName` で捕捉され、複数行逐語本体から phantom 参照は漏れない状態になっているが、issue 原文の再現コードそのものを end-to-end で固定する回帰テストが無く、将来 masker の C# フレームスタック処理を refactor した際に静かに不具合が再発しうる状況だった。`Extract_CsharpInterpolatedAndVerbatimStrings_Issue264_Repro_CapturesHoleCallsAndSuppressesPhantoms` を追加し、issue 原文の fixture そのもの(`$"Hello {Helper.GetName()}"`、`$"Age: {Helper.GetAge()} years"`、`$"Nested {Helper.Format(Helper.GetName())}"`、補間ホール付き複数行 `$@"..."`、素の非補間呼び出し、`PhantomCall()` / `PhantomTable()` / `MoreFake()` を含む複数行 `@"..."` 逐語本体)を `ReferenceExtractor.Extract` に通し、(1) `Helper.GetName` がちょうど 4 回捕捉されること、(2) `Helper.GetAge` と `Helper.Format` がそれぞれ 1 回以上捕捉されること、(3) 捕捉された呼び出しの `ContainerName` がいずれも `Work` であること、(4) 逐語本体に埋め込まれた phantom 識別子がいずれも参照行として現れないこと、の 4 点を同時に固定する。対象: `tests/CodeIndex.Tests/ReferenceExtractorTests.cs`。Closes #264。 - **派生クラスにネストした `new interface`(ベースクラスの同名 nested interface を隠蔽する形)の抽出を専用回帰テストで固定化 (#376)** — Issue #376 は、派生クラスにネストした `public new interface INested { ... }` が無言で落ちる、`SymbolExtractor` の interface 正規表現の修飾子リストが `(?:partial|unsafe)` どまりで `new` が含まれていないのが根本原因、という報告だった。#355 の自由順修飾子書き換えでこのリストは既に `(?:partial|unsafe|file|new)` に拡張されており、現在のバイナリでは `Base` と `Derived` の双方で `interface INested` が 2 件抽出されるため、#376 の再現手順ではもう症状を再現できない。本変更はその挙動を専用回帰テスト `Extract_CSharp_NewNestedInterface_MemberHiding` として固定する。Issue 記載の fixture をそのまま流し、両 nested interface(`ContainerName == "Base"` と `ContainerName == "Derived"`)が抽出されることをアサートするので、今後の修飾子リスト編集で #376 のケースが失敗テスト無しに静かに再回帰しないようになる。対象: `tests/CodeIndex.Tests/SymbolExtractorTests.cs`。Closes #376。 - **C# 8 の struct 上 `readonly` インスタンスインデクサが `Item` シンボルとして抽出されることを回帰テストで固定化 (#352)** — `SymbolExtractor` は以前、`public readonly int this[int i] => _arr[i];` や `public readonly string this[string key] { get => key; }` を silent drop していたため、`readonly` インデクサを多用する slice / span / イミュータブル wrapper struct の表面積を `definition` / `symbols` / `outline` / `inspect` が過小にしていた。#355 の書き換えに伴って C# インデクサ行の自由順 modifier スロットが `readonly` に加えて `unsafe` / `extern` / `ref (readonly)` も受理するようになっているが、今回はその挙動を専用回帰テスト `Extract_CSharp_DetectsReadonlyIndexers` で固定する。式本体、ブロック本体、ジェネリック戻り値 (`public readonly T this[int i] { get => _items[i]; }`) の三形態と、非 readonly / readonly メソッドのベースラインまで含めてカバーし、今後の regex リファクタで #352 のケースが再回帰したときに失敗テストを伴わずに壊れないようにする。対象: `tests/CodeIndex.Tests/SymbolExtractorTests.cs`。 - **`cdidx search` にスニペット 1 行あたりの文字数上限を追加し、minified / transpiled ファイル内の 1 ヒットで数百 KB を返さなくなるよう修正 (#196)** — `cdidx search --snippet-lines N` はこれまでスニペットの「行数」しか制限していなかったが、「各行の幅」は無制限だった。そのため、minified / transpiled な 1 物理行ファイル(およそ 120 KB 1 行)や生成 fixture、obfuscated なペイロード内の一致は raw line をそのまま返し、JSON 出力は 1 結果あたり数百 KB に膨張し、人向け TUI 出力は見苦しく折り返され、MCP の `search` ツールの `structuredContent` は AI のトークン予算を食い潰し、issue の再現ケース(120,009 文字の Python 1 行に 1 ヒット)は `--snippet-lines 1` を指定しても全量 120 KB を返していた。`SearchSnippetFormatter` でスニペットの各行を、`find` / `references` / `excerpt` / `inspect` で既に使っている共有ヘルパ `LineWidthFormatter.ClampLine` に通すようにした。match 行は最初のマッチトークンを中心に、周囲へ `...(+N)...` マーカー付きでクランプされ、context 行(非 match 行)は先頭側を維持して末尾に `...(+N)...` マーカーを付ける head-clamp を行う。`SearchHighlight` には `OriginalLineLength` と `Truncated` を、`CompactSearchResult` / `SearchSnippetExcerpt` には `TruncatedLineCount` をそれぞれ追加し、AI クライアントがクランプの有無を検出できるようにした。CLI の `--max-line-width ` と MCP `search` の `maxLineWidth` 引数は、他コマンドと完全に同じ契約(`DefaultMaxLineWidth=512`、`MaxAllowedLineWidth=4096`)を共有するため、`search` だけ別のノブを覚える必要はない。`--max-line-width` は search のヘルプ usage 行、説明、bash / zsh / fish 補完にも追加した(fish は以前から `search` をサブコマンドとして出していたが、`--max-line-width` 補完だけが `search` に対して出ていなかったので足した)。bash / zsh の `search` 補完分岐は実際の `RunSearch` 契約と一致するよう整え、`RunSearch` が usage error として拒否する `--exact-name` を候補から外し、受理される `--top`(`--limit` のエイリアス)と `--no-dedup` を追加した。また MCP の `maxLineWidth` ヘルパは、スキーマ上の上限値 `MaxAllowedLineWidth`(4096)を超える値を黙って丸めていたのを止め、`4097` のような schema 違反な入力は明示的にツールエラーで fail-close するよう揃えた。CLI 側の `--max-line-width` パーサも、`search` / `references` / `inspect` / `excerpt` は `ParseArgs`、`find` は `ValidateFindArgs` の段階で `MaxAllowedLineWidth` 超えを parse error として弾き、`UsageError` で停止して stdout は空にする。これまで `ClampMaxLineWidth` で黙って丸めていたため `cdidx search ... --max-line-width 4097` は exit 0 で成功していたが、MCP と同じ fail-close 契約に揃えた。さらに `find` 固有の経路として、`ValidateFindArgs` をインライン `=` 対応にした。`TrySplitInlineOptionValue` を使って inline `--opt=value` トークンを `(--opt, value)` として validation 目的だけに解釈し、arg 配列自体は書き換えない。これによりこれまで `find` だけ pre-flight validator の段階で全インライン `=` 形を `unsupported option for find: --max-line-width=4097` として拒否しており、他コマンドが `ParseArgs` で受理する `--opt=` を `find` だけが拒否するという非対称性が解消される。配列を書き換えていないので、`--` で始まる合法な inline 値(例: `--path=--literal.txt` のように、`--literal.txt` というファイル名を CLI hint が勧める形で渡すケース)はそのまま `ParseArgs` に渡り、従来通り正しく受理される。issue の再現ケースでのスモークテストでは、1 ヒットを含む 120,009 文字 1 行ファイルの出力がデフォルトで約 293 バイト、`--max-line-width 200` で約 137 バイト、`--max-line-width 4096` で約 2085 バイトに収まり、JSON の `snippet` には最初のマッチトークン周囲に `...(+119914)...` マーカーが入り、`truncated_line_count` / `highlights[].truncated` / `highlights[].original_line_length` もきちんと埋まることを確認した。`maxLineWidth` 以内に収まる短い行は一切変更しない。`SearchSnippetFormatter` 側に、長い match 行のクランプ、長い非 match 行のクランプ、`CompactSearchResult` のトランケーション・メタデータ、短い行がそのまま返ること、の 4 本を固定する回帰テストを追加し、`ConsoleUi` の usage / help 文言アサートも更新した。さらに `QueryEntrypoints_MaxLineWidthAboveCeilingFailClosed_Issue196` という theory を追加し、`RunSearch` / `RunReferences` / `RunInspect` / `RunFind` / `RunExcerpt` に `--max-line-width 4097`(`search` は `8192` も)を渡したときに、`UsageError` で終了し、stdout は空、stderr には `"--max-line-width must be less than or equal to 4096"` と `Usage:` 行が出ることを固定した。あわせて同じ 5 コマンドに対して `--max-line-width=4097` のインライン `=` 形も theory 行として追加し、inline-`=` 側の validator 契約が空白区切り形と同じ挙動を返すことを固定している。さらに `RunFind_PathFilterAcceptsRecognizedOptionTokenViaInlineValue` を新設し、`find Alpha --db= --path=--json-dir --count --json` が `search` / `files` と同じように `--` で始まる inline 値を受理することを end-to-end で固定した。対象: `src/CodeIndex/Cli/SearchSnippetFormatter.cs`、`src/CodeIndex/Cli/QueryCommandRunner.cs`、`src/CodeIndex/Cli/ConsoleUi.cs`、`src/CodeIndex/Mcp/McpToolDefinitions.cs`、`src/CodeIndex/Mcp/McpToolHandlers.cs`、`tests/CodeIndex.Tests/SearchSnippetFormatterTests.cs`、`tests/CodeIndex.Tests/ConsoleUiTests.cs`、`tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`、`tests/CodeIndex.Tests/McpServerTests.cs`。Closes #196。 diff --git a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs index 9a0aca840a..878634b201 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs @@ -425,6 +425,67 @@ private void BadCall() { } Assert.Contains(references, reference => reference.SymbolName == "RealCall" && reference.ContainerName == "M"); } + [Fact] + public void Extract_CsharpInterpolatedAndVerbatimStrings_Issue264_Repro_CapturesHoleCallsAndSuppressesPhantoms() + { + // Regression for issue #264 exact repro: single-line $"..." interpolated + // strings, multi-line $@"..." AND the alternate @$"..." verbatim-interpolated + // ordering, and non-interpolated @"..." verbatim strings must all be handled + // correctly: interpolation-hole call sites are captured (for both $@" and @$" + // orderings, which StructuralLineMasker handles via dedicated branches) while + // pure verbatim bodies never leak phantom references. + // issue #264 repro 回帰: 単行 $"..."、複数行 $@"..." と代替順序 @$"..." の + // 双方の補間ホール内の呼び出しが捕捉され、非補間 @"..." 本体からは phantom + // 参照を漏らさないこと。 + const string content = """" + namespace Demo; + public class Helper + { + public static string GetName() => "bob"; + public static int GetAge() => 42; + public static string Format(string s) => s; + } + public class Caller + { + public void Work() + { + var s1 = $"Hello {Helper.GetName()}"; + var s2 = $"Age: {Helper.GetAge()} years"; + var s3 = $"Nested {Helper.Format(Helper.GetName())}"; + var s4 = $@"Multi + line {Helper.GetName()} text"; + var s6 = @$"Alt + order {Helper.GetName()} {Helper.GetAge()} end"; + var s5 = Helper.GetName(); + var sql = @"SELECT PhantomCall() FROM PhantomTable() + MoreFake() lines"; + } + } + """"; + + var symbols = SymbolExtractor.Extract(1, "csharp", content); + var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); + + var getNameCalls = references + .Where(r => r.SymbolName == "GetName" && r.ReferenceKind == "call") + .ToList(); + Assert.Equal(5, getNameCalls.Count); + Assert.All(getNameCalls, r => Assert.Equal("Work", r.ContainerName)); + + var getAgeCalls = references + .Where(r => r.SymbolName == "GetAge" && r.ReferenceKind == "call") + .ToList(); + Assert.Equal(2, getAgeCalls.Count); + Assert.All(getAgeCalls, r => Assert.Equal("Work", r.ContainerName)); + + Assert.Contains(references, r => + r.SymbolName == "Format" && r.ReferenceKind == "call" && r.ContainerName == "Work"); + + Assert.DoesNotContain(references, r => r.SymbolName == "PhantomCall"); + Assert.DoesNotContain(references, r => r.SymbolName == "PhantomTable"); + Assert.DoesNotContain(references, r => r.SymbolName == "MoreFake"); + } + [Fact] public void Extract_CsharpMultiLineRawString_IssueRepro_DoesNotLeakPhantomCallReferences() {