From 113690da4dcb348ef51e15d335663e51ac58a73b Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 18 Apr 2026 16:39:17 +0900 Subject: [PATCH 1/2] Lock in regression for C# interpolated and verbatim string reference extraction (#264) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit StructuralLineMasker already handles C# single-line $"...", multi-line $@"..." / @$"...", and non-interpolated @"..." correctly — interpolation-hole call sites are captured and multi-line verbatim bodies do not leak phantom edges — but no end-to-end test pinned that behavior on the exact fixture from issue #264, leaving the invariant exposed to silent regressions in future masker refactors. Add Extract_CsharpInterpolatedAndVerbatimStrings_Issue264_Repro_CapturesHoleCallsAndSuppressesPhantoms which feeds the issue's repro verbatim (single-line $"...", multi-line $@"..." with an interpolation hole, nested Helper.Format(Helper.GetName()), plain non-interpolated call site, and a multi-line @"..." body carrying PhantomCall() / PhantomTable() / MoreFake()) through ReferenceExtractor.Extract and pins four invariants: 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 surfaces for the phantom identifiers embedded in the verbatim body. Closes #264 --- CHANGELOG.md | 2 + .../ReferenceExtractorTests.cs | 53 +++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c354db0eb2..46658e4670 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# 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. - **Common language-specific filenames and extensions are no longer silently dropped from the index (#189)** — `FileIndexer.TryDetectLanguage` previously only mapped a handful of extensions and exact filenames, so everyday files that real projects ship — Ruby's `Gemfile` / `Rakefile` / `Podfile` / `Guardfile` / `Capfile` / `.rake` / `.gemspec` / `.podspec`, Podman's `Containerfile`, explicit `GNUmakefile`, Makefile variants `Makefile.common` / `Dockerfile.dev`, Bazel `BUILD` / `BUILD.bazel` / `WORKSPACE` / `WORKSPACE.bazel` (indexed as Python because Starlark is a Python dialect), Python stubs and Windows launcher scripts `.pyi` / `.pyw` (mapped to `python`), Cython `.pyx` / `.pxd` (mapped to their own `cython` bucket — search-only, because `cdef class` / `cpdef` / `cdef` are not parsed by the Python symbol extractor and advertising them as `python` would claim `symbol_extraction=true` while emitting zero symbols, the same mismatch that sunk the earlier `.sass` / `.styl` → `css` mapping), T-SQL / PostgreSQL / Oracle PL/SQL dialects `.tsql` / `.pgsql` / `.plsql` / `.psql`, `.htm`, CSS-family preprocessors `.less` / `.pcss` (mapped to `css`), indentation-based Sass `.sass` and Stylus `.styl` (mapped to their own `sass` / `stylus` buckets because the CSS symbol extractor is brace-based and cannot parse indented syntax — they are search-only, matching the current `cdidx languages` `symbol_extraction` contract), and plain `.mk` — were all treated as binary/unknown and skipped without a shebang fallback match. `LangMap` has been extended with the missing extensions, `FileNameMap` with the missing exact-match filenames, and a new `FileNamePrefixMap` handles the `Dockerfile.` / `Containerfile.` / `Makefile.` / `GNUmakefile.` pattern that is idiomatic on real Dockerfile and GNU Make projects. The prefix match requires a non-empty suffix (`fileName.Length > prefix.Length`) so bare trailing-dot inputs like `Dockerfile.` remain unrecognized rather than being misclassified, and the prefix check runs after the exact-filename check so existing `Dockerfile` / `Makefile` / `GNUmakefile` rows keep their original language mapping. Extension lookup still runs first so tool-specific extensions like `.gemspec` are not shadowed by an accidental `.rb`-style prefix collision. `GetLanguageExtensions()` — the public API behind `cdidx languages` and the MCP language listing — now also surfaces the prefix variants as `` pseudo-entries (`Dockerfile.`, `Containerfile.`, `Makefile.`, `GNUmakefile.`) so the advertised language list matches what `TryDetectLanguage` actually recognizes at scan time. Added `FileIndexer` regression coverage that pins every new extension, every new exact filename, every prefix variant, an end-to-end `ScanFiles` test that constructs all 10 files from the issue's repro and asserts exactly 10 files come back indexed (previously only 1 survived), a negative theory that locks `Dockerfile.` / `Containerfile.` / `Makefile.` / `GNUmakefile.` bare trailing-dot inputs out of the prefix map, and a dedicated `GetLanguageExtensions_ExposesPrefixAndFileNameVariants` test that pins the exact-name and prefix-variant entries plus the `sass` / `stylus` split away from `css`. Affected: `src/CodeIndex/Indexer/FileIndexer.cs`, `tests/CodeIndex.Tests/FileIndexerTests.cs`, `README.md`. Closes #189. @@ -714,6 +715,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。 - **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。 - **よく使われる言語固有ファイル名と拡張子がインデックスから黙って落ちる問題を修正 (#189)** — `FileIndexer.TryDetectLanguage` はこれまでごく一部の拡張子と厳密ファイル名しかマップしていなかったため、実プロジェクトで普通に同梱される日常的なファイル — Ruby の `Gemfile` / `Rakefile` / `Podfile` / `Guardfile` / `Capfile` / `.rake` / `.gemspec` / `.podspec`、Podman の `Containerfile`、明示的な `GNUmakefile`、Makefile バリアント `Makefile.common` / `Dockerfile.dev`、Bazel の `BUILD` / `BUILD.bazel` / `WORKSPACE` / `WORKSPACE.bazel`(Starlark は Python 方言なので python として扱う)、Python の stub と Windows 起動スクリプト `.pyi` / `.pyw`(python にマップ)、Cython の `.pyx` / `.pxd`(独立の `cython` バケットにマップ。`cdef class` / `cpdef` / `cdef` は Python 用抽出器では拾えないため、`.sass` / `.styl` → `css` と同じく python にマップすると `symbol_extraction=true` と広告しつつ 0 件しか出ない齟齬になるので、search-only の独立バケットに分ける)、T-SQL / PostgreSQL / Oracle PL/SQL 方言の `.tsql` / `.pgsql` / `.plsql` / `.psql`、`.htm`、CSS ファミリーのプリプロセッサ `.less` / `.pcss`(いずれも `css` にマップ)、インデント構文の Sass `.sass` と Stylus `.styl`(CSS のシンボル抽出は波括弧ベースでインデント構文を解釈できないため、それぞれ `sass` / `stylus` の独立バケットにマップする search-only 言語として扱う。これは `cdidx languages` の `symbol_extraction` 契約とも整合する)、素の `.mk` — がすべて binary/unknown 扱いになり、shebang フォールバックにも該当しないためスキャン対象から無言で除外されていた。`LangMap` に不足していた拡張子を、`FileNameMap` に不足していた厳密ファイル名を追加し、実 Dockerfile / GNU Make プロジェクトで慣用的な `Dockerfile.` / `Containerfile.` / `Makefile.` / `GNUmakefile.` 形を扱う `FileNamePrefixMap` を新設した。プレフィクスマッチは非空 suffix を要求し(`fileName.Length > prefix.Length`)、`Dockerfile.` のような裸の末尾ドット入力は誤分類せず従来どおり未認識のままにする。プレフィクスチェックは厳密ファイル名チェックの後に走らせるため、既存の `Dockerfile` / `Makefile` / `GNUmakefile` の言語マッピングは変わらない。拡張子ルックアップが最優先で走る仕組みは維持されるので、ツール固有拡張子 `.gemspec` が誤って `.rb` 形のプレフィクスに飲み込まれることもない。`GetLanguageExtensions()`(`cdidx languages` と MCP の言語一覧が内部で使う公開 API)からも、プレフィクス変種を `` 形の擬似エントリ(`Dockerfile.`、`Containerfile.`、`Makefile.`、`GNUmakefile.`)として露出するようにし、広告される対応言語一覧が走査時の `TryDetectLanguage` の実挙動と一致するようにした。新設した拡張子・厳密ファイル名・プレフィクスバリアントのすべてを押さえる `FileIndexer` 回帰テストに加え、issue の再現ケース 10 ファイルを用意してちょうど 10 ファイルがインデックスされること(修正前は 1 ファイルのみ生存)を `ScanFiles` end-to-end で固定し、`Dockerfile.` / `Containerfile.` / `Makefile.` / `GNUmakefile.` の末尾ドット単独入力がプレフィクスマップに拾われないことを押さえる否定テスト、さらに厳密名エントリ・プレフィクス変種エントリ、そして `sass` / `stylus` が `css` から分離していることを固定する `GetLanguageExtensions_ExposesPrefixAndFileNameVariants` を追加した。対象: `src/CodeIndex/Indexer/FileIndexer.cs`、`tests/CodeIndex.Tests/FileIndexerTests.cs`、`README.md`。Closes #189。 diff --git a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs index 9a0aca840a..395fc74d1b 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs @@ -425,6 +425,59 @@ 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 $@"..." verbatim-interpolated strings, and + // non-interpolated @"..." verbatim strings must all be handled correctly: + // interpolation-hole call sites are captured 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 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(4, getNameCalls.Count); + Assert.All(getNameCalls, r => Assert.Equal("Work", r.ContainerName)); + + Assert.Contains(references, r => + r.SymbolName == "GetAge" && r.ReferenceKind == "call" && r.ContainerName == "Work"); + 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() { From 5377e864ddb8c51dfb94462c77e71eb493c772b3 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 18 Apr 2026 16:46:35 +0900 Subject: [PATCH 2/2] Extend #264 regression to cover @$"..." verbatim-interpolated ordering Round 1 codex review flagged that the new fixture only exercised the $@"..." ordering. StructuralLineMasker has dedicated branches for both legal spellings, so add a sibling @$"..." case to close the coverage gap claimed by the changelog entry. --- .../ReferenceExtractorTests.cs | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs index 395fc74d1b..878634b201 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs @@ -429,12 +429,14 @@ private void BadCall() { } public void Extract_CsharpInterpolatedAndVerbatimStrings_Issue264_Repro_CapturesHoleCallsAndSuppressesPhantoms() { // Regression for issue #264 exact repro: single-line $"..." interpolated - // strings, multi-line $@"..." verbatim-interpolated strings, and - // non-interpolated @"..." verbatim strings must all be handled correctly: - // interpolation-hole call sites are captured while pure verbatim bodies - // never leak phantom references. - // issue #264 repro 回帰: 単行 $"...", 複数行 $@"..." の補間ホール内の呼び出し - // は捕捉され、非補間 @"..." 本体からは phantom 参照を漏らさないこと。 + // 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 @@ -452,6 +454,8 @@ public void Work() 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"; @@ -465,11 +469,15 @@ public void Work() var getNameCalls = references .Where(r => r.SymbolName == "GetName" && r.ReferenceKind == "call") .ToList(); - Assert.Equal(4, getNameCalls.Count); + Assert.Equal(5, getNameCalls.Count); Assert.All(getNameCalls, r => Assert.Equal("Work", r.ContainerName)); - Assert.Contains(references, r => - r.SymbolName == "GetAge" && r.ReferenceKind == "call" && r.ContainerName == "Work"); + 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");