From 0438e693f5f9fc5a8b76152cdcb2a77de19c2ad9 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 31 Jul 2026 00:30:56 +0900 Subject: [PATCH 1/5] Add canonical member-read references (#4894) --- DEVELOPER_GUIDE.md | 8 +- TESTING_GUIDE.md | 2 + USER_GUIDE.md | 16 ++- changelog.d/unreleased/4894.added.md | 24 ++++ src/CodeIndex/Cli/CliFlagSchema.cs | 1 + src/CodeIndex/Cli/ConsoleUi.cs | 6 +- ...eryCommandRunner.ArgumentParser.Results.cs | 3 + .../Cli/QueryCommandRunner.ArgumentParser.cs | 2 + .../Cli/QueryCommandRunner.Dependencies.cs | 3 +- src/CodeIndex/Cli/QueryCommandRunner.Graph.cs | 32 ++--- .../Cli/QueryCommandRunner.GraphOutput.cs | 4 +- src/CodeIndex/Cli/QueryCommandRunner.cs | 1 + .../Database/DbReader.GraphQueries.cs | 76 ++++++----- src/CodeIndex/Database/DbReader.cs | 11 +- ...arpReferenceExtractor.QualifiedPatterns.cs | 5 +- .../CSharpReferenceExtractor.Support.cs | 19 +++ .../Symbols/SymbolExtractor.Contracts.cs | 2 +- src/CodeIndex/Mcp/McpToolArgumentContracts.cs | 4 +- src/CodeIndex/Mcp/McpToolCatalog.cs | 3 + .../Mcp/McpToolHandlers.ArgumentValidation.cs | 2 +- src/CodeIndex/Mcp/McpToolHandlers.Graph.cs | 34 +++-- .../Mcp/McpToolHandlers.ImpactAnalysis.cs | 5 +- src/CodeIndex/Models/SymbolKindCatalog.cs | 1 + tests/CodeIndex.Tests/CliFlagSchemaTests.cs | 14 ++ tests/CodeIndex.Tests/DbReaderTests.cs | 75 +++++++++++ .../IndexCommandRunnerFullScanTests.cs | 73 ++++++++++- .../McpServerToolsCallTests.cs | 55 +++++++- .../McpServerToolsListTests.cs | 19 +++ .../QueryCommandRunnerImpactTests.cs | 1 + .../QueryCommandRunnerInspectTests.cs | 13 +- .../QueryCommandRunnerReferencesTests.cs | 96 ++++++++------ .../ReferenceExtractorCSharpTests.cs | 122 ++++++++++-------- .../ReferenceExtractorTests.cs | 41 +++++- 33 files changed, 576 insertions(+), 197 deletions(-) create mode 100644 changelog.d/unreleased/4894.added.md diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index d8927cbcd..3abc78d2c 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -861,6 +861,7 @@ filters remain exact, so `--kind import` does not include local type declaration | `instantiate` | Constructor or object creation | | `join_condition_reference` | SQL join/merge condition column reference | | `lifetime_reference` | Rust/C#-style lifetime or lifetime-like type reference | +| `member_read` | Non-invoking member/value read, including qualified C# enum constants, constants, static readonly fields, and static properties | | `metadata` | Metadata-only reference | | `reference` | Generic persisted reference row used by fixtures or extractors without a narrower edge kind | | `razor_event_binding` | Razor event binding relationship | @@ -1281,7 +1282,7 @@ every non-inspect command must reject that cursor family before execution. ### Reference taxonomy -`symbol_references.reference_kind` stores raw extractor labels. Default call-graph surfaces (`callers`, `callees`, inspect/analyze caller and callee bundles, and their JSON/MCP fields) expose the canonical public vocabulary `call`, `instantiate`, and `subscribe`. The primary `reference_kind`, `reference_kinds`, and `reference_kind_counts` keys use that same vocabulary. Use `--raw-kinds` on `callers` / `callees`, or `references --kind `, when debugging raw extractor output. +`symbol_references.reference_kind` stores raw extractor labels. Default call-graph surfaces (`callers`, `callees`, inspect/analyze caller and callee bundles, and their JSON/MCP fields) expose the canonical public vocabulary `call`, `instantiate`, and `subscribe`. Non-invoking value reads use the canonical `member_read` label and stay out of default callers, callees, and impact traversal; opt in with CLI `--include-member-reads` or MCP `includeMemberReads`. The primary `reference_kind`, `reference_kinds`, and `reference_kind_counts` keys use that same vocabulary. Legacy indexes remain readable but stored these reads as `call`, so they retain the historical inclusive behavior until re-indexed. Use `--raw-kinds` on `callers` / `callees`, or `references --kind `, when debugging raw extractor output. `ReferenceRecord.SpanLength` and `symbol_references.span_length` persist the physical matched-token width rather than deriving it from the resolved symbol name; this matters for constructor-chain tokens such as `base`, `super`, and `this`. `DbReader.GetCallees` preserves that span while aggregating counts: it selects the smallest `(line, column_number)` among rows with a stored column, exposes that 1-based pair as `first_line` / nullable `first_column`, and carries the same row's nullable width as `first_length`; `reference_count` remains the independent aggregate. When every contributing legacy row has `column_number IS NULL`, the reader retains the minimum line and a null column. A migrated row can also retain a column with a null span length. CLI/MCP location adapters degrade either case without fabricating precision. @@ -1290,6 +1291,7 @@ Reference extraction deduplicates only within the same indexed file and language | Raw kind | Logical graph kind | Notes | |---|---|---| | `call` | `call` | Direct executable invocation edges. | +| `member_read` | `member_read` | Non-invoking member/value reads; excluded from default invocation graphs and included only by an explicit kind filter or the member-read compatibility option. | | `instantiate` | `instantiate` | Constructor / construction edges. | | `goroutine_spawn` | `goroutine_spawn` | Go `go f()` async spawn edges; the ordinary `call` edge is also emitted for the invoked function. | | `channel_send`, `channel_receive` | raw label | Go channel communication edges for send and receive expressions; excluded from default invocation graphs. | @@ -4248,6 +4250,7 @@ filter、downstream JSON consumer が同じ値を理解できるようにして | `instantiate` | constructor または object creation | | `join_condition_reference` | SQL join / merge condition column reference | | `lifetime_reference` | Rust / C# 風 lifetime または lifetime-like type reference | +| `member_read` | C# の修飾付き enum 定数、定数、static readonly field、static property など、呼び出しを伴わない member / value 読み取り | | `metadata` | metadata-only reference | | `reference` | より狭い edge kind を持たない fixture / extractor 用の generic persisted reference row | | `razor_event_binding` | Razor event binding relationship | @@ -4703,13 +4706,14 @@ inspect 以外の各 command は実行前にその cursor family を拒否しな ### 参照 taxonomy -`symbol_references.reference_kind` には extractor が出力した raw label を保存する。既定の call-graph 表示(`callers`、`callees`、inspect/analyze の caller / callee bundle、および JSON/MCP フィールド)は、公開 canonical 語彙 `call`、`instantiate`、`subscribe` を返す。primary `reference_kind`、`reference_kinds`、`reference_kind_counts` の key はすべて同じ語彙を使う。raw extractor 出力を調べる場合は、`callers` / `callees` の `--raw-kinds`、または `references --kind ` を使う。 +`symbol_references.reference_kind` には extractor が出力した raw label を保存する。既定の call-graph 表示(`callers`、`callees`、inspect/analyze の caller / callee bundle、および JSON/MCP フィールド)は、公開 canonical 語彙 `call`、`instantiate`、`subscribe` を返す。呼び出しを伴わない value read は canonical `member_read` を使い、既定の callers / callees / impact traversal から除外する。含める場合は CLI の `--include-member-reads` または MCP の `includeMemberReads` を明示する。primary `reference_kind`、`reference_kinds`、`reference_kind_counts` の key はすべて同じ語彙を使う。legacy index は引き続き読み取れるが、これらの read を `call` として保存しているため、再 index するまでは従来の inclusive な挙動を維持する。raw extractor 出力を調べる場合は、`callers` / `callees` の `--raw-kinds`、または `references --kind ` を使う。 `ReferenceRecord.SpanLength` と `symbol_references.span_length` は、解決後の symbol 名から導出せず、物理的に一致した token 幅を永続化する。これは `base`、`super`、`this` のような constructor-chain token で重要になる。`DbReader.GetCallees` は count を集約しながらその span を保持し、列が保存された row のうち最小の `(line, column_number)` を選び、その 1-based 座標を `first_line` / nullable な `first_column`、同じ row の nullable な幅を `first_length` として公開し、`reference_count` は独立した集約値のままにする。寄与する legacy row がすべて `column_number IS NULL` の場合は最小行と null 列を保持する。移行済み row では列があっても span 長が null の場合がある。CLI/MCP の location adapter はどちらの場合も精度を捏造せず劣化させる。 | Raw kind | Logical graph kind | 備考 | |---|---|---| | `call` | `call` | 直接実行される呼び出しエッジ。 | +| `member_read` | `member_read` | 呼び出しを伴わない member / value read。既定の invocation graph から除外し、明示 kind filter または member-read compatibility option のときだけ含める。 | | `instantiate` | `instantiate` | constructor / construction エッジ。 | | `goroutine_spawn` | `goroutine_spawn` | Go の `go f()` による非同期 spawn edge。呼び出し先には通常の `call` edge も併せて出力する。 | | `channel_send`, `channel_receive` | raw label | Go の channel send / receive 式を表す通信エッジ。既定の invocation graph からは除外する。 | diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md index 1f17e6363..8a66b69f1 100644 --- a/TESTING_GUIDE.md +++ b/TESTING_GUIDE.md @@ -102,6 +102,7 @@ Use `docs/test-doc-maintenance-plan.md` before moving oversized suites or adding `ReferenceExtractorTests.ExtractSymbolsAndReferences(...)` owns the common symbol-then-reference extraction setup for tests that need both lists; use it instead of repeating the two extractor calls when the fixture does not need a specialized path or workspace symbol setup, and discard the symbol tuple element with `_` instead of keeping an unused `symbols` local when the test only asserts references. C# named-argument coverage keeps syntax discrimination in `ReferenceExtractorCSharpTests` and persisted query/dependency behavior in `QueryCommandRunnerIssue4833Tests`; preserve positional and reordered arguments, attributes, constructors, nested and multiline calls, expression-side, named-`out`, explicitly typed lambda/anonymous-method, and typed LINQ range-variable type references, property-subpattern types, and negative controls for aliases, labels, nullable types, and ternaries (#4833; regressions #106 and #122). C# qualified common-call coverage keeps static BCL, instance, LINQ extension, alias-qualified, current-instance, and unresolved-receiver cases together. Assert that extraction retains every row, default bare-name references/callers/callees and hotspot counts retain resolved evidence while excluding unresolved noise, the completeness option restores that noise deterministically, and dependency queries remain identity-scoped (#4867). + C# member-read coverage keeps enum and const patterns, ordinary qualified constants, static readonly fields, static properties, and a true method invocation together. Assert `member_read` extraction, default callers/callees/impact exclusion, explicit compatibility inclusion, and legacy `call` readability in coordinated extractor and `DbReaderTests` fixtures (#4894). Crystal, Groovy, Tcl, Prolog, and `ambiguous_pl` graph fixtures keep imports, parenthesized calls, conservative same-file command/predicate calls, caller containers, and keyword false-positive controls independently diagnosable; capability tests must keep their advertised symbol/reference/graph flags aligned with those extractor fixtures (#4746). Database status tests must also pin stale and current extractor-version stamps for those languages so pre-graph rows cannot report authoritative graph readiness. Database page-attribution coverage keeps empty/schema-only, WAL-visible overflow, database-truncating WAL, and a connection-pinned read snapshot after a newer WAL commit, cancellation of both `dbstat` aggregation and WAL validation, the bounded large-page-count rejection, reconciliation, corrupt-file rejection, main/WAL/SHM separation, the 20-object and 128-character support-safe output bounds, explicit unavailable/not-requested values, and unchanged `total_changes()` / `PRAGMA query_only` in coordinated `DbReaderTests` fixtures. @@ -1043,6 +1044,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests" `ReferenceExtractorTests.ExtractSymbolsAndReferences(...)` は symbol 抽出から reference 抽出までの共通 setup を所有します。fixture が特殊な path や workspace symbol setup を必要としない場合は 2 つの extractor 呼び出しを繰り返さずこの helper を使い、reference だけを検証するテストでは未使用の `symbols` local を残さず symbol 側を `_` で捨ててください。 C# named-argument の coverage は構文の判別を `ReferenceExtractorCSharpTests`、永続化後の query / dependency 動作を `QueryCommandRunnerIssue4833Tests` に保持します。positional argument との混在、並べ替え、attribute、constructor、nested / multiline call、value 式側、named `out` declaration、明示型 lambda / anonymous method、および型付き LINQ range variable の type reference、property subpattern の型、ならびに alias、label、nullable type、ternary に対する負例を維持してください(#4833、回帰 #106 / #122)。 C# の修飾付き一般名 call の coverage は、static BCL、instance、LINQ extension、alias 修飾、current instance、未解決 receiver の各 case を同じ fixture に維持します。extraction が全 row を保持すること、無修飾名による references / callers / callees と hotspot count の既定動作が解決済み evidence を維持しつつ未解決 noise を除外すること、completeness option がその noise を決定的に復元すること、dependency query が identity scope のままであることを検証してください(#4867)。 + C# member-read coverage は enum / const pattern、通常の修飾付き定数、static readonly field、static property、真の method invocation を同じ fixture に維持します。連携する extractor / `DbReaderTests` fixture で `member_read` 抽出、既定 callers / callees / impact からの除外、明示 compatibility option による復元、legacy `call` row の読み取りを検証してください(#4894)。 Crystal、Groovy、Tcl、Prolog、`ambiguous_pl` の graph fixture では、import、括弧付き call、同一ファイルに限定した保守的な command / predicate call、caller container、keyword の false-positive control を個別に診断可能な状態で維持し、capability test の symbol / reference / graph 広告を extractor fixture と一致させてください(#4746)。 さらに database status test でこれらの言語の古い extractor-version stamp と現行 stamp を固定し、graph 対応前の row が authoritative な graph readiness を報告できないことを検証してください。 database page-attribution coverage では、empty / schema-only、WAL から可視な overflow、database を縮小する WAL、後続 WAL commit 後も connection に固定された read snapshot の各 case、`dbstat` 集約と WAL 検証のキャンセル、large page count の上限付き拒否、再照合、破損 file の拒否、main/WAL/SHM の分離、20 object / 128文字の support-safe 出力上限、明示的な unavailable / not-requested 値、`total_changes()` / `PRAGMA query_only` が不変であることを、連携した `DbReaderTests` fixture で維持してください。 diff --git a/USER_GUIDE.md b/USER_GUIDE.md index a896743b2..1af964c5a 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -777,6 +777,7 @@ they are re-indexed with a current binary. cdidx impact Run --max-hops 2 --exclude-tests cdidx impact Run --max-hops 0 --json cdidx impact FolderDiffService --with-paths --json +cdidx impact CurrentValue --include-member-reads --json ``` `impact` resolves a symbol and walks transitive callers through call-graph edges. @@ -784,7 +785,12 @@ cdidx impact FolderDiffService --with-paths --json chains for converging routes. Metadata-only edges such as attributes, annotations, and type-position references are excluded from the symbol-level BFS so metadata cycles do not inflate caller counts; single-type queries may still -return heuristic file-level dependency hints. +return heuristic file-level dependency hints. Current indexes store non-invoking +member/value reads as `member_read`, which callers, callees, and impact exclude by +default. Use `--include-member-reads` (MCP: `includeMemberReads`) when read +dependencies are intentionally part of the graph. Legacy indexes stored those +reads as `call`; they remain readable and keep their historical inclusive behavior +until re-indexed. On a current index, cycle detection follows the resolved source/target symbol IDs on real directed edges. Two distinct methods with the same display name are not a @@ -4046,6 +4052,7 @@ binary で再 index されるまで互換性のある raw-reference fallback を cdidx impact Run --max-hops 2 --exclude-tests cdidx impact Run --max-hops 0 --json cdidx impact FolderDiffService --with-paths --json +cdidx impact CurrentValue --include-member-reads --json ``` `impact` は symbol を解決し、call-graph edges を通じて transitive callers を探索します。 @@ -4053,7 +4060,12 @@ cdidx impact FolderDiffService --with-paths --json call chains を出力します。Attributes、annotations、type-position references のような metadata-only edges は symbol-level BFS から除外されるため、metadata cycle で caller count が膨らむことはありません。ただし single-type query では heuristic file-level -dependency hints が返る場合があります。 +dependency hints が返る場合があります。current index は呼び出しを伴わない +member / value read を `member_read` として保存し、callers / callees / impact は既定で +除外します。read dependency を graph に含める場合は `--include-member-reads` +(MCP は `includeMemberReads`)を明示してください。legacy index はこれらの read を +`call` として保存しているため、引き続き読み取り可能で、再 index するまでは従来の +inclusive な挙動を維持します。 current index では、cycle 判定は実在する有向辺の解決済み source/target symbol ID を 辿ります。表示名が同じ別 method は cycle にせず、直接再帰は singleton cycle として diff --git a/changelog.d/unreleased/4894.added.md b/changelog.d/unreleased/4894.added.md new file mode 100644 index 000000000..2abdec024 --- /dev/null +++ b/changelog.d/unreleased/4894.added.md @@ -0,0 +1,24 @@ +--- +category: added +issues: + - 4894 +affected: + - src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.QualifiedPatterns.cs + - src/CodeIndex/Database/DbReader.GraphQueries.cs + - src/CodeIndex/Cli/CliFlagSchema.cs + - src/CodeIndex/Mcp/McpToolCatalog.cs + - src/CodeIndex/Models/SymbolKindCatalog.cs + - tests/CodeIndex.Tests/ReferenceExtractorTests.cs + - tests/CodeIndex.Tests/DbReaderTests.cs + - USER_GUIDE.md + - DEVELOPER_GUIDE.md + - TESTING_GUIDE.md +--- + +## English + +- **Added canonical `member_read` references for non-invoking C# member/value reads (#4894)** — new indexes distinguish qualified enum constants, constants, static readonly fields, static properties, and constant-pattern reads from true `call` edges. Callers, callees, and impact exclude these reads by default and expose explicit CLI `--include-member-reads` / MCP `includeMemberReads` compatibility controls; legacy indexes remain readable with their historical `call` rows. + +## 日本語 + +- **呼び出しを伴わない C# member / value read 向けに canonical `member_read` reference を追加しました (#4894)** — 新しい index は修飾付き enum 定数、定数、static readonly field、static property、constant-pattern read を真の `call` edge と区別します。callers、callees、impact は既定でこれらを除外し、CLI `--include-member-reads` / MCP `includeMemberReads` の明示 compatibility control で復元できます。legacy index は従来の `call` row のまま引き続き読み取れます。 diff --git a/src/CodeIndex/Cli/CliFlagSchema.cs b/src/CodeIndex/Cli/CliFlagSchema.cs index a2233f931..96b9fff8c 100644 --- a/src/CodeIndex/Cli/CliFlagSchema.cs +++ b/src/CodeIndex/Cli/CliFlagSchema.cs @@ -481,6 +481,7 @@ private static IReadOnlyList BuildAll() new() { Name = "--sort", ValuePlaceholder = "", Description = "Symbols/outline: order audit output by a ranking signal; outline also accepts source, kind, references, size, complexity, path, and name", PrimaryCommands = Set(SymbolSortCommands) }, new() { Name = "--raw-kinds", Description = "Show raw reference kinds instead of logical graph kinds", PrimaryCommands = Set(RawKindsCommands) }, new() { Name = "--include-qualified-common-calls", Description = "Include unresolved receiver/type-qualified C# calls with common member names", PrimaryCommands = Set("references", "callers", "callees") }, + new() { Name = "--include-member-reads", Description = "Include canonical member/value-read edges in callers, callees, and impact results", PrimaryCommands = Set("callers", "callees", "impact") }, new() { Name = "--count", Description = "Count only; result limits are ignored by count modes, but scan caps can still mark approximate counts as degraded", PrimaryCommands = Set(CountCommands) }, new() { Name = "--group-partials", Description = "Definition/Symbols/Inspect: collapse C# partial-type declarations into logical families", PrimaryCommands = Set("definition", "symbols", "inspect") }, new() { Name = "--strict-not-found", Description = "Return exit code 2 when a valid query has zero rows", PrimaryCommands = Set(StrictNotFoundCommands), Safety = CliOptionSafety.StrictFailure }, diff --git a/src/CodeIndex/Cli/ConsoleUi.cs b/src/CodeIndex/Cli/ConsoleUi.cs index c4f5e2067..de397103f 100644 --- a/src/CodeIndex/Cli/ConsoleUi.cs +++ b/src/CodeIndex/Cli/ConsoleUi.cs @@ -90,8 +90,8 @@ private static readonly (string Command, string Usage)[] CommandUsageLines = ("definition", "cdidx definition |--query |-- [--db ] [--json] [--redact-paths|--show-paths] [--format ] [--fields ] [--cursor ] [--max-json-bytes ] [--verbose] [--limit |--top ] [--lang ] [--kind ] [--visibility ] [--exclude-visibility ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--exact|--exact-name] [--count] [--group-partials] [--since ]"), ("goto", "cdidx goto |--query |-- [--db ] [--json] [--limit |--top ] [--lang ] [--kind ] [--path ] [--exclude-path ] [--exclude-tests] [--exact-name] [--all]"), ("references", "cdidx references |--query |-- [--db ] [--json] [--redact-paths|--show-paths] [--format ] [--fields ] [--cursor ] [--max-json-bytes ] [--verbose] [--limit |--top ] [--lang ] [--kind ] [--path ] [--exclude-path ] [--exclude-tests] [--include-qualified-common-calls] [--body] [--snippet-lines ] [--max-line-width ] [--exact|--exact-name] [--count]"), - ("callers", "cdidx callers |--query |-- [--db ] [--json] [--redact-paths|--show-paths] [--format ] [--fields ] [--cursor ] [--max-json-bytes ] [--verbose] [--limit |--top ] [--lang ] [--kind ] [--rank-by ] [--raw-kinds] [--path ] [--exclude-path ] [--exclude-tests] [--include-qualified-common-calls] [--body] [--snippet-lines ] [--max-line-width ] [--exact|--exact-name] [--count]"), - ("callees", "cdidx callees |--query |-- [--db ] [--json] [--redact-paths|--show-paths] [--format ] [--fields ] [--cursor ] [--max-json-bytes ] [--verbose] [--limit |--top ] [--lang ] [--kind ] [--rank-by ] [--raw-kinds] [--path ] [--exclude-path ] [--exclude-tests] [--include-qualified-common-calls] [--body] [--snippet-lines ] [--max-line-width ] [--exact|--exact-name] [--count]"), + ("callers", "cdidx callers |--query |-- [--db ] [--json] [--redact-paths|--show-paths] [--format ] [--fields ] [--cursor ] [--max-json-bytes ] [--verbose] [--limit |--top ] [--lang ] [--kind ] [--rank-by ] [--raw-kinds] [--path ] [--exclude-path ] [--exclude-tests] [--include-qualified-common-calls] [--include-member-reads] [--body] [--snippet-lines ] [--max-line-width ] [--exact|--exact-name] [--count]"), + ("callees", "cdidx callees |--query |-- [--db ] [--json] [--redact-paths|--show-paths] [--format ] [--fields ] [--cursor ] [--max-json-bytes ] [--verbose] [--limit |--top ] [--lang ] [--kind ] [--rank-by ] [--raw-kinds] [--path ] [--exclude-path ] [--exclude-tests] [--include-qualified-common-calls] [--include-member-reads] [--body] [--snippet-lines ] [--max-line-width ] [--exact|--exact-name] [--count]"), ("symbols", "cdidx symbols [query|--query |-- ] [--name ] [--db ] [--json[=ndjson|array]] [--compact] [--format ] [--summary-only] [--cursor ] [--max-json-bytes ] [--allow-partial] [--verbose] [--limit |--top ] [--sort ] [--lang ] [--kind ] [--visibility ] [--exclude-visibility ] [--path ] [--exclude-path ] [--exclude-tests] [--exact|--exact-name] [--count] [--group-partials] [--since ]"), ("files", "cdidx files [query||--query |-- ] [--db ] [--json[=ndjson|array]] [--format ] [--summary-only] [--cursor ] [--max-json-bytes ] [--allow-partial] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--count] [--since ] [--bytes]"), ("find", "cdidx find (--path |--all) [--db ] [--json] [--format ] [--fields ] [--cursor ] [--max-json-bytes ] [--verbose] [--limit |--top ] [--lang ] [--exclude-path ] [--exclude-tests] [--context ] [--before ] [--after ] [--snippet-lines ] [--focus-line ] [--focus-column ] [--max-line-width ] [--line-scan-limit ] [--allow-partial] [--exact] [--regex] [--count]"), @@ -129,7 +129,7 @@ private static readonly (string Command, string Usage)[] CommandUsageLines = ("diff", $"cdidx diff [--json] [--summary-only] [--detailed] [--data-only|--include-telemetry] [--include-content] [--max-json-bytes ] [--limit ] [--offset |--cursor ]"), ("report", "cdidx report --output [--overwrite] [--db ] [--json] [--redact-paths] [--log-lines ] [--no-log] [--include-args]"), ("validate", "cdidx validate [--db ] [--json[=array]] [--format ] [--verbose] [--limit |--top ] [--kind ] [--severity ] [--path ]"), - ("impact", "cdidx impact |--query |-- [--db ] [--json] [--redact-paths|--show-paths] [--format ] [--compact] [--fields ] [--cursor ] [--max-json-bytes ] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--snippet-lines ] [--max-line-width ] [--max-hops ] [--exact-name] [--count] [--with-paths]"), + ("impact", "cdidx impact |--query |-- [--db ] [--json] [--redact-paths|--show-paths] [--format ] [--compact] [--fields ] [--cursor ] [--max-json-bytes ] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--snippet-lines ] [--max-line-width ] [--max-hops ] [--exact-name] [--count] [--with-paths] [--include-member-reads]"), ("deps", "cdidx deps [--db ] [--json] [--format ] [--summary-only] [--max-json-bytes ] [--verbose] [--limit |--top ] [--cursor ] [--graph-budget ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--reverse] [--cycles] [--suppress-noise] [--symbol ] [--symbol-family ]"), ("unused", "cdidx unused [--db ] [--json] [--compact] [--summary-only] [--verbose] [--limit |--top ] [--cursor ] [--audit-scope ] [--kind ] [--bucket ] [--min-confidence |--confidence ] [--actionable] [--all] [--visibility ] [--exclude-visibility ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--count] [--by-bucket]"), ("hotspots", "cdidx hotspots [--db ] [--json] [--format ] [--compact] [--fields ] [--cursor ] [--summary-only] [--max-json-bytes ] [--verbose] [--limit |--top ] [--kind ] [--visibility ] [--exclude-visibility ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--count] [--group-by ] [--group-by-name]"), diff --git a/src/CodeIndex/Cli/QueryCommandRunner.ArgumentParser.Results.cs b/src/CodeIndex/Cli/QueryCommandRunner.ArgumentParser.Results.cs index a62e1e99a..9ae3da075 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.ArgumentParser.Results.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.ArgumentParser.Results.cs @@ -242,6 +242,9 @@ private bool TryParseResultOption(string normalizedArg, string currentArg, strin case "--include-qualified-common-calls": includeQualifiedCommonCalls = true; break; + case "--include-member-reads": + includeMemberReads = true; + break; case "--verbose": verbose = true; break; diff --git a/src/CodeIndex/Cli/QueryCommandRunner.ArgumentParser.cs b/src/CodeIndex/Cli/QueryCommandRunner.ArgumentParser.cs index 22a9c8df6..19130b8e6 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.ArgumentParser.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.ArgumentParser.cs @@ -117,6 +117,7 @@ private sealed partial class QueryArgumentParser private bool rawBytes; private bool rawKinds; private bool includeQualifiedCommonCalls; + private bool includeMemberReads; private bool verbose; private bool profile; private int? slowQueryMs; @@ -508,6 +509,7 @@ private QueryCommandOptions BuildOptions(DbPathResolution dbResolution, string r RawBytes = rawBytes, RawKinds = rawKinds, IncludeQualifiedCommonCalls = includeQualifiedCommonCalls, + IncludeMemberReads = includeMemberReads, Verbose = verbose, Profile = profile, SlowQueryMs = slowQueryMs, diff --git a/src/CodeIndex/Cli/QueryCommandRunner.Dependencies.cs b/src/CodeIndex/Cli/QueryCommandRunner.Dependencies.cs index a799fad8d..b891f3766 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.Dependencies.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.Dependencies.cs @@ -74,7 +74,8 @@ public static int RunImpact(string[] cmdArgs, JsonSerializerOptions jsonOptions) options.ExcludeTests, options.WithPaths, JsonEnvelopeWrapper.GetBoundedResponseOffset("impact"), - JsonEnvelopeWrapper.GetBoundedImpactCollection()); + JsonEnvelopeWrapper.GetBoundedImpactCollection(), + options.IncludeMemberReads); if (options.IncludeBody) AttachBodyExcerpts(reader, analysis.Callers, options.SnippetLines, options.MaxLineWidth); ApplyBodyRecoveryCommands(analysis.Callers, options.DbPath, options.RedactPaths ?? true); diff --git a/src/CodeIndex/Cli/QueryCommandRunner.Graph.cs b/src/CodeIndex/Cli/QueryCommandRunner.Graph.cs index e55049b1b..35c8d293c 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.Graph.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.Graph.cs @@ -178,16 +178,16 @@ public static int RunCallers(string[] cmdArgs, JsonSerializerOptions jsonOptions : null; if (options.CountOnly) { - var counts = reader.CountCallersTotal(query, options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact, options.RawKinds, options.IncludeQualifiedCommonCalls); + var counts = reader.CountCallersTotal(query, options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact, options.RawKinds, options.IncludeQualifiedCommonCalls, options.IncludeMemberReads); var effectiveSqlGraphSignal = NarrowSqlGraphContractSignal( baseSqlGraphSignal, counts.IncludesSql || DbReader.IsSqlLanguage(options.Lang) || DbReader.IsSqlLanguage(exactGraphLanguage)); var exactSignalForCount = reader.GetCallersExactQuerySignal(options.Lang, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, includeSqlGraphContractSignal: effectiveSqlGraphSignal.Relevant); var exactZeroHintForCount = BuildExactZeroHint( exact && reader._hasReferencesTable, - () => reader.CountCallers(query, ExactZeroHintProbeLimit, options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact: false, rawKinds: options.RawKinds, includeQualifiedCommonCalls: options.IncludeQualifiedCommonCalls) > 0, - () => reader.CountCallers(query, options.Limit, options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact: false, rawKinds: options.RawKinds, includeQualifiedCommonCalls: options.IncludeQualifiedCommonCalls), - () => reader.GetCallers(query, Math.Min(options.Limit, ExactZeroHintSampleLimit), options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact: false, rawKinds: options.RawKinds, rankMode: options.RankMode, includeQualifiedCommonCalls: options.IncludeQualifiedCommonCalls), + () => reader.CountCallers(query, ExactZeroHintProbeLimit, options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact: false, rawKinds: options.RawKinds, includeQualifiedCommonCalls: options.IncludeQualifiedCommonCalls, includeMemberReads: options.IncludeMemberReads) > 0, + () => reader.CountCallers(query, options.Limit, options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact: false, rawKinds: options.RawKinds, includeQualifiedCommonCalls: options.IncludeQualifiedCommonCalls, includeMemberReads: options.IncludeMemberReads), + () => reader.GetCallers(query, Math.Min(options.Limit, ExactZeroHintSampleLimit), options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact: false, rawKinds: options.RawKinds, rankMode: options.RankMode, includeQualifiedCommonCalls: options.IncludeQualifiedCommonCalls, includeMemberReads: options.IncludeMemberReads), r => r.CalleeName); WriteExactGraphWarningIfNeeded(exact, options.Json, exactSignalForCount, reader, options); WriteSqlGraphContractWarningIfNeeded(options.Json, effectiveSqlGraphSignal, reader, options); @@ -202,7 +202,7 @@ public static int RunCallers(string[] cmdArgs, JsonSerializerOptions jsonOptions return CommandExitCodes.Success; } - var results = reader.GetCallers(query, options.Limit, options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact, options.RawKinds, options.RankMode, offset: JsonEnvelopeWrapper.GetBoundedResponseOffset("callers"), includeQualifiedCommonCalls: options.IncludeQualifiedCommonCalls); + var results = reader.GetCallers(query, options.Limit, options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact, options.RawKinds, options.RankMode, offset: JsonEnvelopeWrapper.GetBoundedResponseOffset("callers"), includeQualifiedCommonCalls: options.IncludeQualifiedCommonCalls, includeMemberReads: options.IncludeMemberReads); if (options.IncludeBody) AttachBodyExcerpts(reader, results, options.SnippetLines, options.MaxLineWidth); ApplyBodyRecoveryCommands(results, options.DbPath, options.RedactPaths ?? true); @@ -210,9 +210,9 @@ public static int RunCallers(string[] cmdArgs, JsonSerializerOptions jsonOptions var exactSignal = reader.GetCallersExactQuerySignal(options.Lang, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, includeSqlGraphContractSignal: sqlGraphSignal.Relevant); var exactZeroHint = BuildExactZeroHint( exact && reader._hasReferencesTable, - () => reader.CountCallers(query, ExactZeroHintProbeLimit, options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact: false, rawKinds: options.RawKinds, includeQualifiedCommonCalls: options.IncludeQualifiedCommonCalls) > 0, - () => reader.CountCallers(query, options.Limit, options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact: false, rawKinds: options.RawKinds, includeQualifiedCommonCalls: options.IncludeQualifiedCommonCalls), - () => reader.GetCallers(query, Math.Min(options.Limit, ExactZeroHintSampleLimit), options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact: false, rawKinds: options.RawKinds, rankMode: options.RankMode, includeQualifiedCommonCalls: options.IncludeQualifiedCommonCalls), + () => reader.CountCallers(query, ExactZeroHintProbeLimit, options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact: false, rawKinds: options.RawKinds, includeQualifiedCommonCalls: options.IncludeQualifiedCommonCalls, includeMemberReads: options.IncludeMemberReads) > 0, + () => reader.CountCallers(query, options.Limit, options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact: false, rawKinds: options.RawKinds, includeQualifiedCommonCalls: options.IncludeQualifiedCommonCalls, includeMemberReads: options.IncludeMemberReads), + () => reader.GetCallers(query, Math.Min(options.Limit, ExactZeroHintSampleLimit), options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact: false, rawKinds: options.RawKinds, rankMode: options.RankMode, includeQualifiedCommonCalls: options.IncludeQualifiedCommonCalls, includeMemberReads: options.IncludeMemberReads), r => r.CalleeName); WriteExactGraphWarningIfNeeded(exact, options.Json, exactSignal, reader, options); WriteSqlGraphContractWarningIfNeeded(options.Json, sqlGraphSignal, reader, options); @@ -325,16 +325,16 @@ public static int RunCallees(string[] cmdArgs, JsonSerializerOptions jsonOptions : null; if (options.CountOnly) { - var counts = reader.CountCalleesTotal(query, options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact, options.RawKinds, options.IncludeQualifiedCommonCalls); + var counts = reader.CountCalleesTotal(query, options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact, options.RawKinds, options.IncludeQualifiedCommonCalls, options.IncludeMemberReads); var effectiveSqlGraphSignal = NarrowSqlGraphContractSignal( baseSqlGraphSignal, counts.IncludesSql || DbReader.IsSqlLanguage(options.Lang) || DbReader.IsSqlLanguage(exactGraphLanguage)); var exactSignalForCount = reader.GetCalleesExactQuerySignal(options.Lang, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, includeSqlGraphContractSignal: effectiveSqlGraphSignal.Relevant); var exactZeroHintForCount = BuildExactZeroHint( exact && reader._hasReferencesTable, - () => reader.CountCallees(query, ExactZeroHintProbeLimit, options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact: false, rawKinds: options.RawKinds, includeQualifiedCommonCalls: options.IncludeQualifiedCommonCalls) > 0, - () => reader.CountCallees(query, options.Limit, options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact: false, rawKinds: options.RawKinds, includeQualifiedCommonCalls: options.IncludeQualifiedCommonCalls), - () => reader.GetCallees(query, Math.Min(options.Limit, ExactZeroHintSampleLimit), options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact: false, rawKinds: options.RawKinds, rankMode: options.RankMode, includeQualifiedCommonCalls: options.IncludeQualifiedCommonCalls), + () => reader.CountCallees(query, ExactZeroHintProbeLimit, options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact: false, rawKinds: options.RawKinds, includeQualifiedCommonCalls: options.IncludeQualifiedCommonCalls, includeMemberReads: options.IncludeMemberReads) > 0, + () => reader.CountCallees(query, options.Limit, options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact: false, rawKinds: options.RawKinds, includeQualifiedCommonCalls: options.IncludeQualifiedCommonCalls, includeMemberReads: options.IncludeMemberReads), + () => reader.GetCallees(query, Math.Min(options.Limit, ExactZeroHintSampleLimit), options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact: false, rawKinds: options.RawKinds, rankMode: options.RankMode, includeQualifiedCommonCalls: options.IncludeQualifiedCommonCalls, includeMemberReads: options.IncludeMemberReads), r => r.CallerName); WriteExactGraphWarningIfNeeded(exact, options.Json, exactSignalForCount, reader, options); WriteSqlGraphContractWarningIfNeeded(options.Json, effectiveSqlGraphSignal, reader, options); @@ -349,7 +349,7 @@ public static int RunCallees(string[] cmdArgs, JsonSerializerOptions jsonOptions return CommandExitCodes.Success; } - var results = reader.GetCallees(query, options.Limit, options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact, options.RawKinds, options.RankMode, offset: JsonEnvelopeWrapper.GetBoundedResponseOffset("callees"), includeQualifiedCommonCalls: options.IncludeQualifiedCommonCalls); + var results = reader.GetCallees(query, options.Limit, options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact, options.RawKinds, options.RankMode, offset: JsonEnvelopeWrapper.GetBoundedResponseOffset("callees"), includeQualifiedCommonCalls: options.IncludeQualifiedCommonCalls, includeMemberReads: options.IncludeMemberReads); if (options.IncludeBody) AttachBodyExcerpts(reader, results, options.SnippetLines, options.MaxLineWidth); ApplyBodyRecoveryCommands(results, options.DbPath, options.RedactPaths ?? true); @@ -357,9 +357,9 @@ public static int RunCallees(string[] cmdArgs, JsonSerializerOptions jsonOptions var exactSignal = reader.GetCalleesExactQuerySignal(options.Lang, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, includeSqlGraphContractSignal: sqlGraphSignal.Relevant); var exactZeroHint = BuildExactZeroHint( exact && reader._hasReferencesTable, - () => reader.CountCallees(query, ExactZeroHintProbeLimit, options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact: false, rawKinds: options.RawKinds, includeQualifiedCommonCalls: options.IncludeQualifiedCommonCalls) > 0, - () => reader.CountCallees(query, options.Limit, options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact: false, rawKinds: options.RawKinds, includeQualifiedCommonCalls: options.IncludeQualifiedCommonCalls), - () => reader.GetCallees(query, Math.Min(options.Limit, ExactZeroHintSampleLimit), options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact: false, rawKinds: options.RawKinds, rankMode: options.RankMode, includeQualifiedCommonCalls: options.IncludeQualifiedCommonCalls), + () => reader.CountCallees(query, ExactZeroHintProbeLimit, options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact: false, rawKinds: options.RawKinds, includeQualifiedCommonCalls: options.IncludeQualifiedCommonCalls, includeMemberReads: options.IncludeMemberReads) > 0, + () => reader.CountCallees(query, options.Limit, options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact: false, rawKinds: options.RawKinds, includeQualifiedCommonCalls: options.IncludeQualifiedCommonCalls, includeMemberReads: options.IncludeMemberReads), + () => reader.GetCallees(query, Math.Min(options.Limit, ExactZeroHintSampleLimit), options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact: false, rawKinds: options.RawKinds, rankMode: options.RankMode, includeQualifiedCommonCalls: options.IncludeQualifiedCommonCalls, includeMemberReads: options.IncludeMemberReads), r => r.CallerName); WriteExactGraphWarningIfNeeded(exact, options.Json, exactSignal, reader, options); WriteSqlGraphContractWarningIfNeeded(options.Json, sqlGraphSignal, reader, options); diff --git a/src/CodeIndex/Cli/QueryCommandRunner.GraphOutput.cs b/src/CodeIndex/Cli/QueryCommandRunner.GraphOutput.cs index f0ee5b93c..10d8b487c 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.GraphOutput.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.GraphOutput.cs @@ -19,7 +19,7 @@ public static partial class QueryCommandRunner // compile-time な `type_reference` エッジを含む。C++ の `friend` 宣言も extractor が出す // dependency edge として受け付け、graph query にも参加させる。 private static readonly string[] AllValidReferenceKinds = - ["annotation", "attribute", "augmentation", "bcl_regex_without_timeout", "binding", "call", "capture", "consumes_hook", "dependency", "friend", "generic_type_argument", "import", "instantiate", "project_reference", "razor_event_binding", "resource_reference", "subscribe", "type_reference", "type_tag", "unsubscribe"]; + ["annotation", "attribute", "augmentation", "bcl_regex_without_timeout", "binding", "call", "capture", "consumes_hook", "dependency", "friend", "generic_type_argument", "import", "instantiate", "member_read", "project_reference", "razor_event_binding", "resource_reference", "subscribe", "type_reference", "type_tag", "unsubscribe"]; // Reference kinds that `callers` / `callees` can legitimately return. Metadata kinds // (`attribute` / `annotation`) and type-position edges (`type_reference`) are structurally @@ -29,7 +29,7 @@ public static partial class QueryCommandRunner // や型位置エッジ (`type_reference`) は構造的に call-graph エッジではないため、CLI / MCP 境界で弾く。 // C++ の `friend` は graph に出す coupling edge。 private static readonly string[] CallGraphOnlyReferenceKinds = - ["augmentation", "call", "capture", "consumes_hook", "friend", "generic_type_argument", "instantiate", "project_reference", "razor_event_binding", "subscribe", "unsubscribe"]; + ["augmentation", "call", "capture", "consumes_hook", "friend", "generic_type_argument", "instantiate", "member_read", "project_reference", "razor_event_binding", "subscribe", "unsubscribe"]; private static void WriteGraphReferenceKindHint(string command, string? kind, bool json) { diff --git a/src/CodeIndex/Cli/QueryCommandRunner.cs b/src/CodeIndex/Cli/QueryCommandRunner.cs index 7eaa31d61..6f0794522 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.cs @@ -183,6 +183,7 @@ public sealed class QueryCommandOptions public bool RawBytes { get; init; } public bool RawKinds { get; init; } public bool IncludeQualifiedCommonCalls { get; init; } + public bool IncludeMemberReads { get; init; } public bool Verbose { get; init; } public bool Profile { get; init; } public int? SlowQueryMs { get; init; } diff --git a/src/CodeIndex/Database/DbReader.GraphQueries.cs b/src/CodeIndex/Database/DbReader.GraphQueries.cs index 4304db5c1..b5905dd7b 100644 --- a/src/CodeIndex/Database/DbReader.GraphQueries.cs +++ b/src/CodeIndex/Database/DbReader.GraphQueries.cs @@ -55,11 +55,11 @@ private static bool ShouldFilterCSharpQualifiedCommonBareMemberQuery(string quer /// Find callers for a referenced symbol. /// 指定シンボルを呼び出している呼び出し元を探す。 /// - public List GetCallers(string query, int limit = 20, string? lang = null, string? referenceKind = null, IReadOnlyList? pathPatterns = null, IReadOnlyList? excludePathPatterns = null, bool excludeTests = false, bool exact = false, bool rawKinds = false, ReferenceRankMode rankMode = ReferenceRankMode.Weighted, bool excludeSelfReferences = false, int offset = 0, bool includeQualifiedCommonCalls = false) - => GetCallersCore(query, limit, lang, referenceKind, pathPatterns, excludePathPatterns, excludeTests, exact, rawKinds, rankMode, excludeSelfReferences, offset, includeQualifiedCommonCalls, targetSymbolId: null); + public List GetCallers(string query, int limit = 20, string? lang = null, string? referenceKind = null, IReadOnlyList? pathPatterns = null, IReadOnlyList? excludePathPatterns = null, bool excludeTests = false, bool exact = false, bool rawKinds = false, ReferenceRankMode rankMode = ReferenceRankMode.Weighted, bool excludeSelfReferences = false, int offset = 0, bool includeQualifiedCommonCalls = false, bool includeMemberReads = false) + => GetCallersCore(query, limit, lang, referenceKind, pathPatterns, excludePathPatterns, excludeTests, exact, rawKinds, rankMode, excludeSelfReferences, offset, includeQualifiedCommonCalls, includeMemberReads, targetSymbolId: null); private List GetCallersForCandidate(DefinitionResult definition, int limit, IReadOnlyList? pathPatterns, IReadOnlyList? excludePathPatterns, bool excludeTests, int offset = 0) - => GetCallersCore(definition.Name, limit, definition.Lang, referenceKind: null, pathPatterns, excludePathPatterns, excludeTests, exact: true, rawKinds: false, ReferenceRankMode.Weighted, excludeSelfReferences: false, offset, includeQualifiedCommonCalls: false, targetSymbolId: definition.SymbolId); + => GetCallersCore(definition.Name, limit, definition.Lang, referenceKind: null, pathPatterns, excludePathPatterns, excludeTests, exact: true, rawKinds: false, ReferenceRankMode.Weighted, excludeSelfReferences: false, offset, includeQualifiedCommonCalls: false, includeMemberReads: false, targetSymbolId: definition.SymbolId); private int CountCallersForCandidate(DefinitionResult definition, IReadOnlyList? pathPatterns, IReadOnlyList? excludePathPatterns, bool excludeTests) { @@ -76,10 +76,11 @@ private int CountCallersForCandidate(DefinitionResult definition, IReadOnlyList< exact: true, rawKinds: false, includeQualifiedCommonCalls: false, + includeMemberReads: false, symbolId).Count; } - private List GetCallersCore(string query, int limit, string? lang, string? referenceKind, IReadOnlyList? pathPatterns, IReadOnlyList? excludePathPatterns, bool excludeTests, bool exact, bool rawKinds, ReferenceRankMode rankMode, bool excludeSelfReferences, int offset, bool includeQualifiedCommonCalls, long? targetSymbolId) + private List GetCallersCore(string query, int limit, string? lang, string? referenceKind, IReadOnlyList? pathPatterns, IReadOnlyList? excludePathPatterns, bool excludeTests, bool exact, bool rawKinds, ReferenceRankMode rankMode, bool excludeSelfReferences, int offset, bool includeQualifiedCommonCalls, bool includeMemberReads, long? targetSymbolId) { if (string.IsNullOrWhiteSpace(query) || IsBareVerbatimQueryToken(query)) return new List(); @@ -114,7 +115,7 @@ WITH logical_references AS ( FROM symbol_references r JOIN files f ON r.file_id = f.id" + referenceLineJoin + @" WHERE " + callerContainerPredicate + @" - AND " + GetCallableReferenceKindPredicateSql("r.reference_kind", referenceKind, "f.lang") + @" + AND " + GetCallableReferenceKindPredicateSql("r.reference_kind", referenceKind, "f.lang", includeMemberReads) + @" AND " + supportedLangPredicate; if (targetSymbolId != null && HasTable("symbol_reference_candidates")) { @@ -286,7 +287,7 @@ FROM logical_references r return results; } - public int CountCallers(string query, int limit = 20, string? lang = null, string? referenceKind = null, IReadOnlyList? pathPatterns = null, IReadOnlyList? excludePathPatterns = null, bool excludeTests = false, bool exact = false, bool rawKinds = false, bool includeQualifiedCommonCalls = false) + public int CountCallers(string query, int limit = 20, string? lang = null, string? referenceKind = null, IReadOnlyList? pathPatterns = null, IReadOnlyList? excludePathPatterns = null, bool excludeTests = false, bool exact = false, bool rawKinds = false, bool includeQualifiedCommonCalls = false, bool includeMemberReads = false) { if (string.IsNullOrWhiteSpace(query) || IsBareVerbatimQueryToken(query)) return 0; @@ -306,7 +307,7 @@ FROM symbol_references r WHERE " + BuildCallerContainerPredicate("f", "r"); groupedSql += $" AND {BuildGraphSupportedLanguagePredicate(cmd, "f", "graphLang")}"; - groupedSql += $" AND {GetCallableReferenceKindPredicateSql("r.reference_kind", referenceKind, "f.lang")}"; + groupedSql += $" AND {GetCallableReferenceKindPredicateSql("r.reference_kind", referenceKind, "f.lang", includeMemberReads)}"; var allowSqlLeafFallback = AllowSqlLeafFallbackForQuery(query); var allowCSharpQualifiedContextMatch = SqlNameResolver.HasQualifier(query) && !HasQualifiedSymbolDefinition(query, lang, pathPatterns, excludePathPatterns, excludeTests); @@ -403,7 +404,7 @@ FROM symbol_references r return raw is long l ? (int)l : Convert.ToInt32(raw); } - public QueryCountResult CountCallersTotal(string query, string? lang = null, string? referenceKind = null, IReadOnlyList? pathPatterns = null, IReadOnlyList? excludePathPatterns = null, bool excludeTests = false, bool exact = false, bool rawKinds = false, bool includeQualifiedCommonCalls = false) + public QueryCountResult CountCallersTotal(string query, string? lang = null, string? referenceKind = null, IReadOnlyList? pathPatterns = null, IReadOnlyList? excludePathPatterns = null, bool excludeTests = false, bool exact = false, bool rawKinds = false, bool includeQualifiedCommonCalls = false, bool includeMemberReads = false) => CountCallersTotalCore( query, lang, @@ -414,6 +415,7 @@ public QueryCountResult CountCallersTotal(string query, string? lang = null, str exact, rawKinds, includeQualifiedCommonCalls, + includeMemberReads, targetSymbolId: null); private QueryCountResult CountCallersTotalCore( @@ -426,6 +428,7 @@ private QueryCountResult CountCallersTotalCore( bool exact, bool rawKinds, bool includeQualifiedCommonCalls, + bool includeMemberReads, long? targetSymbolId) { if (!_hasReferencesTable) @@ -448,7 +451,7 @@ FROM symbol_references r WHERE " + BuildCallerContainerPredicate("f", "r"); groupedSql += $" AND {BuildGraphSupportedLanguagePredicate(cmd, "f", "graphLang")}"; - groupedSql += $" AND {GetCallableReferenceKindPredicateSql("r.reference_kind", referenceKind, "f.lang")}"; + groupedSql += $" AND {GetCallableReferenceKindPredicateSql("r.reference_kind", referenceKind, "f.lang", includeMemberReads)}"; if (targetSymbolId != null && HasTable("symbol_reference_candidates")) { groupedSql += _referenceColumns.Contains("resolution_state") @@ -562,11 +565,11 @@ FROM symbol_reference_candidates AS identity_candidate /// Find callees used by a caller/container symbol. /// 呼び出し元シンボルが使っている呼び出し先を探す。 /// - public List GetCallees(string query, int limit = 20, string? lang = null, string? referenceKind = null, IReadOnlyList? pathPatterns = null, IReadOnlyList? excludePathPatterns = null, bool excludeTests = false, bool exact = false, bool rawKinds = false, ReferenceRankMode rankMode = ReferenceRankMode.Weighted, int offset = 0, bool includeQualifiedCommonCalls = false) - => GetCalleesCore(query, limit, lang, referenceKind, pathPatterns, excludePathPatterns, excludeTests, exact, rawKinds, rankMode, offset, includeQualifiedCommonCalls, sourceSymbolId: null); + public List GetCallees(string query, int limit = 20, string? lang = null, string? referenceKind = null, IReadOnlyList? pathPatterns = null, IReadOnlyList? excludePathPatterns = null, bool excludeTests = false, bool exact = false, bool rawKinds = false, ReferenceRankMode rankMode = ReferenceRankMode.Weighted, int offset = 0, bool includeQualifiedCommonCalls = false, bool includeMemberReads = false) + => GetCalleesCore(query, limit, lang, referenceKind, pathPatterns, excludePathPatterns, excludeTests, exact, rawKinds, rankMode, offset, includeQualifiedCommonCalls, includeMemberReads, sourceSymbolId: null); private List GetCalleesForCandidate(DefinitionResult definition, int limit, IReadOnlyList? pathPatterns, IReadOnlyList? excludePathPatterns, bool excludeTests, int offset = 0) - => GetCalleesCore(definition.Name, limit, definition.Lang, referenceKind: null, pathPatterns, excludePathPatterns, excludeTests, exact: true, rawKinds: false, ReferenceRankMode.Weighted, offset, includeQualifiedCommonCalls: false, sourceSymbolId: definition.SymbolId); + => GetCalleesCore(definition.Name, limit, definition.Lang, referenceKind: null, pathPatterns, excludePathPatterns, excludeTests, exact: true, rawKinds: false, ReferenceRankMode.Weighted, offset, includeQualifiedCommonCalls: false, includeMemberReads: false, sourceSymbolId: definition.SymbolId); private int CountCalleesForCandidate(DefinitionResult definition, IReadOnlyList? pathPatterns, IReadOnlyList? excludePathPatterns, bool excludeTests) { @@ -583,10 +586,11 @@ private int CountCalleesForCandidate(DefinitionResult definition, IReadOnlyList< exact: true, rawKinds: false, includeQualifiedCommonCalls: false, + includeMemberReads: false, symbolId).Count; } - private List GetCalleesCore(string query, int limit, string? lang, string? referenceKind, IReadOnlyList? pathPatterns, IReadOnlyList? excludePathPatterns, bool excludeTests, bool exact, bool rawKinds, ReferenceRankMode rankMode, int offset, bool includeQualifiedCommonCalls, long? sourceSymbolId) + private List GetCalleesCore(string query, int limit, string? lang, string? referenceKind, IReadOnlyList? pathPatterns, IReadOnlyList? excludePathPatterns, bool excludeTests, bool exact, bool rawKinds, ReferenceRankMode rankMode, int offset, bool includeQualifiedCommonCalls, bool includeMemberReads, long? sourceSymbolId) { if (string.IsNullOrWhiteSpace(query) || IsBareVerbatimQueryToken(query)) return new List(); @@ -618,7 +622,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 {GetCallableReferenceKindPredicateSql("r.reference_kind", referenceKind, "f.lang")} + AND {GetCallableReferenceKindPredicateSql("r.reference_kind", referenceKind, "f.lang", includeMemberReads)} AND {BuildGraphSupportedLanguagePredicate(cmd, "f", "graphLang")}"; if (sourceSymbolId != null && _referenceColumns.Contains("source_symbol_id")) sql += " AND r.source_symbol_id = @sourceSymbolId"; @@ -765,7 +769,7 @@ FROM ranked_call_sites r return results; } - public int CountCallees(string query, int limit = 20, string? lang = null, string? referenceKind = null, IReadOnlyList? pathPatterns = null, IReadOnlyList? excludePathPatterns = null, bool excludeTests = false, bool exact = false, bool rawKinds = false, bool includeQualifiedCommonCalls = false) + public int CountCallees(string query, int limit = 20, string? lang = null, string? referenceKind = null, IReadOnlyList? pathPatterns = null, IReadOnlyList? excludePathPatterns = null, bool excludeTests = false, bool exact = false, bool rawKinds = false, bool includeQualifiedCommonCalls = false, bool includeMemberReads = false) { if (string.IsNullOrWhiteSpace(query) || IsBareVerbatimQueryToken(query)) return 0; @@ -784,7 +788,7 @@ FROM symbol_references r WHERE r.container_name IS NOT NULL"; groupedSql += $" AND {BuildGraphSupportedLanguagePredicate(cmd, "f", "graphLang")}"; - groupedSql += $" AND {GetCallableReferenceKindPredicateSql("r.reference_kind", referenceKind, "f.lang")}"; + groupedSql += $" AND {GetCallableReferenceKindPredicateSql("r.reference_kind", referenceKind, "f.lang", includeMemberReads)}"; var allowSqlLeafFallback = AllowSqlLeafFallbackForQuery(query); var allowQualifiedLeafFallback = HasSingleQualifiedSymbolDefinition(query, lang, pathPatterns, excludePathPatterns, excludeTests); var useSqlQualifiedContainerMatch = SqlNameResolver.HasQualifier(query); @@ -870,7 +874,7 @@ FROM symbol_references r return raw is long l ? (int)l : Convert.ToInt32(raw); } - public QueryCountResult CountCalleesTotal(string query, string? lang = null, string? referenceKind = null, IReadOnlyList? pathPatterns = null, IReadOnlyList? excludePathPatterns = null, bool excludeTests = false, bool exact = false, bool rawKinds = false, bool includeQualifiedCommonCalls = false) + public QueryCountResult CountCalleesTotal(string query, string? lang = null, string? referenceKind = null, IReadOnlyList? pathPatterns = null, IReadOnlyList? excludePathPatterns = null, bool excludeTests = false, bool exact = false, bool rawKinds = false, bool includeQualifiedCommonCalls = false, bool includeMemberReads = false) => CountCalleesTotalCore( query, lang, @@ -881,6 +885,7 @@ public QueryCountResult CountCalleesTotal(string query, string? lang = null, str exact, rawKinds, includeQualifiedCommonCalls, + includeMemberReads, sourceSymbolId: null); private QueryCountResult CountCalleesTotalCore( @@ -893,6 +898,7 @@ private QueryCountResult CountCalleesTotalCore( bool exact, bool rawKinds, bool includeQualifiedCommonCalls, + bool includeMemberReads, long? sourceSymbolId) { lang = NormalizeQueryLanguage(lang); @@ -911,7 +917,7 @@ FROM symbol_references r WHERE r.container_name IS NOT NULL"; groupedSql += $" AND {BuildGraphSupportedLanguagePredicate(cmd, "f", "graphLang")}"; - groupedSql += $" AND {GetCallableReferenceKindPredicateSql("r.reference_kind", referenceKind, "f.lang")}"; + groupedSql += $" AND {GetCallableReferenceKindPredicateSql("r.reference_kind", referenceKind, "f.lang", includeMemberReads)}"; if (sourceSymbolId != null && _referenceColumns.Contains("source_symbol_id")) groupedSql += " AND r.source_symbol_id = @sourceSymbolId"; var allowSqlLeafFallback = AllowSqlLeafFallbackForQuery(query); @@ -1134,13 +1140,13 @@ ELSE 5 /// SQL 側で要求された LIMIT/OFFSET を適用し、呼び出し側が要求以上の中間ページを /// materialize しないようにする。 /// - private List GetCallersExact(string symbolName, int limit, int offset = 0, string? lang = null, IReadOnlyList? pathPatterns = null, IReadOnlyList? excludePathPatterns = null, bool excludeTests = false, bool includeAmbiguousMSource = false) - => GetCallersExactCore(symbolName, limit, offset, lang, pathPatterns, excludePathPatterns, excludeTests, targetSymbolId: null, includeAmbiguousMSource); + private List GetCallersExact(string symbolName, int limit, int offset = 0, string? lang = null, IReadOnlyList? pathPatterns = null, IReadOnlyList? excludePathPatterns = null, bool excludeTests = false, bool includeAmbiguousMSource = false, bool includeMemberReads = false) + => GetCallersExactCore(symbolName, limit, offset, lang, pathPatterns, excludePathPatterns, excludeTests, targetSymbolId: null, includeAmbiguousMSource, includeMemberReads); - private List GetCallersExactForTarget(string symbolName, long targetSymbolId, int limit, int offset, string? lang, IReadOnlyList? pathPatterns, IReadOnlyList? excludePathPatterns, bool excludeTests, bool includeAmbiguousMSource = false) - => GetCallersExactCore(symbolName, limit, offset, lang, pathPatterns, excludePathPatterns, excludeTests, targetSymbolId, includeAmbiguousMSource); + private List GetCallersExactForTarget(string symbolName, long targetSymbolId, int limit, int offset, string? lang, IReadOnlyList? pathPatterns, IReadOnlyList? excludePathPatterns, bool excludeTests, bool includeAmbiguousMSource = false, bool includeMemberReads = false) + => GetCallersExactCore(symbolName, limit, offset, lang, pathPatterns, excludePathPatterns, excludeTests, targetSymbolId, includeAmbiguousMSource, includeMemberReads); - private List GetCallersExactCore(string symbolName, int limit, int offset, string? lang, IReadOnlyList? pathPatterns, IReadOnlyList? excludePathPatterns, bool excludeTests, long? targetSymbolId, bool includeAmbiguousMSource) + private List GetCallersExactCore(string symbolName, int limit, int offset, string? lang, IReadOnlyList? pathPatterns, IReadOnlyList? excludePathPatterns, bool excludeTests, long? targetSymbolId, bool includeAmbiguousMSource, bool includeMemberReads) { if (!_hasReferencesTable) return new List(); using var cmd = _conn.CreateCommand(); @@ -1241,7 +1247,7 @@ WITH logical_references AS ( FROM symbol_references r JOIN files f ON r.file_id = f.id{referenceLineJoin} WHERE {callerContainerPredicate} - AND r.reference_kind IN {CallGraphReferenceKindsSql} + AND (r.reference_kind IN {CallGraphReferenceKindsSql}{(includeMemberReads ? " OR r.reference_kind = 'member_read'" : string.Empty)}) AND {supportedLangFilter} {targetCondition}"; if (lang != null) @@ -1375,7 +1381,7 @@ private static string BuildImpactTraversalNodeKey(long? symbolId, string name) /// を true にすると、各 caller に対してルートからの推移経路 /// (ダイヤモンド収束時は複数)を 件まで付与する(issue #1536)。 /// - public (List Results, bool Truncated, string? TruncatedReason, string TerminationReason, List Cycles) GetTransitiveCallers(string symbolName, int maxDepth = 5, int limit = 50, string? lang = null, IReadOnlyList? pathPatterns = null, IReadOnlyList? excludePathPatterns = null, bool excludeTests = false, bool withPaths = false, int maxPathsPerResult = DefaultImpactPathsPerResult, int resultOffset = 0) + public (List Results, bool Truncated, string? TruncatedReason, string TerminationReason, List Cycles) GetTransitiveCallers(string symbolName, int maxDepth = 5, int limit = 50, string? lang = null, IReadOnlyList? pathPatterns = null, IReadOnlyList? excludePathPatterns = null, bool excludeTests = false, bool withPaths = false, int maxPathsPerResult = DefaultImpactPathsPerResult, int resultOffset = 0, bool includeMemberReads = false) { // Resolve the symbol name through definitions first so case-mismatched queries // like "run" find the actual "Run" symbol. Falls back to user input if not found. @@ -1505,8 +1511,8 @@ private static string BuildImpactTraversalNodeKey(long? symbolId, string name) { fetchIterations++; var page = currentSymbolId is long targetSymbolId - ? GetCallersExactForTarget(currentSymbol, targetSymbolId, pageSize, pageOffset, lang, pathPatterns, excludePathPatterns, excludeTests, includeAmbiguousMSource) - : GetCallersExact(currentSymbol, pageSize, pageOffset, lang, pathPatterns, excludePathPatterns, excludeTests, includeAmbiguousMSource); + ? GetCallersExactForTarget(currentSymbol, targetSymbolId, pageSize, pageOffset, lang, pathPatterns, excludePathPatterns, excludeTests, includeAmbiguousMSource, includeMemberReads) + : GetCallersExact(currentSymbol, pageSize, pageOffset, lang, pathPatterns, excludePathPatterns, excludeTests, includeAmbiguousMSource, includeMemberReads); if (page.Count == 0) break; // No more callers for this symbol / このシンボルの caller は尽きた @@ -1672,7 +1678,8 @@ private static string BuildImpactTraversalNodeKey(long? symbolId, string name) pathPatterns, excludePathPatterns, excludeTests, - includeAmbiguousMSource); + includeAmbiguousMSource, + includeMemberReads); maxDepthReached |= boundaryInspection.HasUnvisitedCaller; if (boundaryInspection.ProbeBudgetHit) { @@ -1845,7 +1852,8 @@ private ImpactBoundaryInspection InspectBoundaryCallers( IReadOnlyList? pathPatterns, IReadOnlyList? excludePathPatterns, bool excludeTests, - bool includeAmbiguousMSource) + bool includeAmbiguousMSource, + bool includeMemberReads) { var offset = 0; var probes = 0; @@ -1856,8 +1864,8 @@ private ImpactBoundaryInspection InspectBoundaryCallers( var pageSize = Math.Min(ImpactBoundaryCallerProbePageSize, ImpactBoundaryCallerProbeBudget - probes); var page = symbolId is long targetSymbolId - ? GetCallersExactForTarget(symbolName, targetSymbolId, pageSize, offset, lang, pathPatterns, excludePathPatterns, excludeTests, includeAmbiguousMSource) - : GetCallersExact(symbolName, pageSize, offset, lang, pathPatterns, excludePathPatterns, excludeTests, includeAmbiguousMSource); + ? GetCallersExactForTarget(symbolName, targetSymbolId, pageSize, offset, lang, pathPatterns, excludePathPatterns, excludeTests, includeAmbiguousMSource, includeMemberReads) + : GetCallersExact(symbolName, pageSize, offset, lang, pathPatterns, excludePathPatterns, excludeTests, includeAmbiguousMSource, includeMemberReads); if (page.Count == 0) return new ImpactBoundaryInspection(HasUnvisitedCaller: false, ProbeBudgetHit: false); probes += page.Count; @@ -2220,7 +2228,7 @@ private static ImpactPathNode CloneImpactPathNode(ImpactPathNode node) /// file dependency をフォールバックとして返す。 は inclusive で /// N 指定時は depth 1〜N の caller を返し、maxDepth: 0 は symbol 解決のみで終了する。 /// - public ImpactAnalysisResult AnalyzeImpact(string symbolName, int maxDepth = 5, int limit = 50, string? lang = null, IReadOnlyList? pathPatterns = null, IReadOnlyList? excludePathPatterns = null, bool excludeTests = false, bool withPaths = false, int offset = 0, string? responseCollection = null) + public ImpactAnalysisResult AnalyzeImpact(string symbolName, int maxDepth = 5, int limit = 50, string? lang = null, IReadOnlyList? pathPatterns = null, IReadOnlyList? excludePathPatterns = null, bool excludeTests = false, bool withPaths = false, int offset = 0, string? responseCollection = null, bool includeMemberReads = false) { lang = NormalizeQueryLanguage(lang); var resolvedName = ResolveSymbolName(symbolName, lang); @@ -2288,11 +2296,11 @@ public ImpactAnalysisResult AnalyzeImpact(string symbolName, int maxDepth = 5, i var callerOffset = responseCollection is null || string.Equals(responseCollection, "callers", StringComparison.Ordinal) ? offset : 0; - var (callers, truncated, truncatedReason, terminationReason, cycles) = GetTransitiveCallers(symbolName, maxDepth, limit, lang, pathPatterns, excludePathPatterns, excludeTests, withPaths, resultOffset: callerOffset); + var (callers, truncated, truncatedReason, terminationReason, cycles) = GetTransitiveCallers(symbolName, maxDepth, limit, lang, pathPatterns, excludePathPatterns, excludeTests, withPaths, resultOffset: callerOffset, includeMemberReads: includeMemberReads); var callerExistsBeforeOffset = false; if (callers.Count == 0 && callerOffset > 0) { - var callerProbe = GetTransitiveCallers(symbolName, maxDepth, 1, lang, pathPatterns, excludePathPatterns, excludeTests, withPaths: false, resultOffset: 0); + var callerProbe = GetTransitiveCallers(symbolName, maxDepth, 1, lang, pathPatterns, excludePathPatterns, excludeTests, withPaths: false, resultOffset: 0, includeMemberReads: includeMemberReads); callerExistsBeforeOffset = callerProbe.Results.Count > 0; } diff --git a/src/CodeIndex/Database/DbReader.cs b/src/CodeIndex/Database/DbReader.cs index 92df067cf..e8e080b14 100644 --- a/src/CodeIndex/Database/DbReader.cs +++ b/src/CodeIndex/Database/DbReader.cs @@ -364,13 +364,18 @@ internal static string GetLogicalReferenceKindSql(string referenceKindSql) private static string GetCallableReferenceKindPredicateSql( string referenceKindSql, string? referenceKind, - string? sourceLanguageSql = null) + string? sourceLanguageSql = null, + bool includeMemberReads = false) => referenceKind switch { null when sourceLanguageSql is not null => - $"({referenceKindSql} IN {CallableReferenceKindsSql} OR " + + $"({referenceKindSql} IN {CallableReferenceKindsSql}" + + (includeMemberReads ? $" OR {referenceKindSql} = 'member_read'" : string.Empty) + + " OR " + $"({sourceLanguageSql} = 'dependency_lock' AND {referenceKindSql} = 'dependency'))", - null => $"{referenceKindSql} IN {CallableReferenceKindsSql}", + null => includeMemberReads + ? $"({referenceKindSql} IN {CallableReferenceKindsSql} OR {referenceKindSql} = 'member_read')" + : $"{referenceKindSql} IN {CallableReferenceKindsSql}", "subscribe" => $"{referenceKindSql} IN {EventReferenceKindsSql}", _ => $"{referenceKindSql} = @referenceKind", }; diff --git a/src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.QualifiedPatterns.cs b/src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.QualifiedPatterns.cs index a75aac08d..5ac1871b1 100644 --- a/src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.QualifiedPatterns.cs +++ b/src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.QualifiedPatterns.cs @@ -63,16 +63,13 @@ internal static void EmitCSharpQualifiedEnumMemberReferences( allowSingleSegmentQualifiedMatch: parsed.HasLeadingGlobalQualifier)) continue; - if (IsCSharpQualifiedConstantPatternReferenceSite(preparedLine, parsed)) - continue; - var nextTokenIndex = SkipWhitespace(preparedLine, member.End); if (nextTokenIndex < preparedLine.Length && preparedLine[nextTokenIndex] == '(') continue; var insideCSharpAttributeRange = csharpAttrRangesOnLine != null && IsInsideCSharpAttributeRange(csharpAttrRangesOnLine, member.Start); - var referenceKind = TryClassifyMetadataReference("csharp", preparedLine, member.Start, insideCSharpAttributeRange) ?? "call"; + var referenceKind = TryClassifyMetadataReference("csharp", preparedLine, member.Start, insideCSharpAttributeRange) ?? "member_read"; AddReference( references, diff --git a/src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.Support.cs b/src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.Support.cs index bfb9bfa98..9feae13de 100644 --- a/src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.Support.cs +++ b/src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.Support.cs @@ -570,12 +570,31 @@ private static ( } if (IsCSharpConstMemberSymbol(symbol)) + { AddCSharpQualifiedPatternTarget( ref constantPatternMemberLookup, symbol.Name, symbol.ContainerName!, symbol.ContainerQualifiedName, allowShortNameFallback: true); + AddCSharpQualifiedEnumMemberTarget( + ref enumMemberLookup, + symbol.Name, + symbol.ContainerName!, + symbol.ContainerQualifiedName, + allowShortNameFallback: true); + continue; + } + + if (symbol.Kind is "field" or "property" && IsStaticCSharpSymbol(symbol)) + { + AddCSharpQualifiedEnumMemberTarget( + ref enumMemberLookup, + symbol.Name, + symbol.ContainerName!, + symbol.ContainerQualifiedName, + allowShortNameFallback: true); + } } return ( diff --git a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.Contracts.cs b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.Contracts.cs index a55c84aba..76035a0c5 100644 --- a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.Contracts.cs +++ b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.Contracts.cs @@ -6,7 +6,7 @@ public static partial class SymbolExtractor public const int ExpandedLanguageContractVersion = 2; public const int YamlContractVersion = 3; public const int PythonContractVersion = 2; - public const int CSharpContractVersion = 8; + public const int CSharpContractVersion = 9; public const int DockerfileContractVersion = 2; public const int MakefileContractVersion = 2; public const int StyleAndXamlContractVersion = 2; diff --git a/src/CodeIndex/Mcp/McpToolArgumentContracts.cs b/src/CodeIndex/Mcp/McpToolArgumentContracts.cs index 2147a523a..a5ffd6ad0 100644 --- a/src/CodeIndex/Mcp/McpToolArgumentContracts.cs +++ b/src/CodeIndex/Mcp/McpToolArgumentContracts.cs @@ -17,7 +17,7 @@ public partial class McpServer "search" => new HashSet(StringComparer.Ordinal) { "query", "recipe", "listRecipes", "auditScope", "limit", "lang", "snippetLines", "snippetFocus", "maxLineWidth", "rawQuery", "cursor", "path", "excludePaths", "excludeTests", "includeGenerated", "since", "noDedup", "exactSubstring", "tokenBoundary", "exact", "prefix", "requireBefore", "requireAfter", "rejectBefore", "rejectAfter", "guardWindow", "guardScope", "countOnly", "format", "project", "solution" }, "definition" => new HashSet(StringComparer.Ordinal) { "query", "kind", "lang", "limit", "visibility", "excludeVisibility", "includeBody", "lsp_compatible", "lspCompatible", "path", "excludePaths", "excludeTests", "includeGenerated", "since", "exactName", "exact", "format", "project", "solution" }, "references" => new HashSet(StringComparer.Ordinal) { "query", "kind", "lang", "limit", "offset", "maxLineWidth", "lsp_compatible", "lspCompatible", "path", "excludePaths", "excludeTests", "includeGenerated", "includeQualifiedCommonCalls", "exactName", "exact", "countOnly", "format", "project", "solution" }, - "callers" or "callees" => new HashSet(StringComparer.Ordinal) { "query", "kind", "rawKinds", "includeQualifiedCommonCalls", "rankBy", "lang", "limit", "offset", "path", "excludePaths", "excludeTests", "includeGenerated", "exactName", "exact", "countOnly", "format", "project", "solution" }, + "callers" or "callees" => new HashSet(StringComparer.Ordinal) { "query", "kind", "rawKinds", "includeQualifiedCommonCalls", "includeMemberReads", "rankBy", "lang", "limit", "offset", "path", "excludePaths", "excludeTests", "includeGenerated", "exactName", "exact", "countOnly", "format", "project", "solution" }, "symbols" => new HashSet(StringComparer.Ordinal) { "query", "names", "kind", "lang", "visibility", "excludeVisibility", "limit", "cursor", "path", "excludePaths", "excludeTests", "includeGenerated", "since", "exactName", "exact", "countOnly", "format", "project", "solution" }, "files" => new HashSet(StringComparer.Ordinal) { "query", "lang", "limit", "cursor", "path", "excludePaths", "excludeTests", "includeGenerated", "since", "orderBySize", "rawBytes", "project", "solution" }, "find_in_file" => new HashSet(StringComparer.Ordinal) { "query", "path", "limit", "lang", "excludePaths", "excludeTests", "includeGenerated", "before", "after", "snippetLines", "focusLine", "focusColumn", "maxLineWidth", "exact", "regex" }, @@ -28,7 +28,7 @@ public partial class McpServer "outline" => new HashSet(StringComparer.Ordinal) { "path" }, "batch_query" => new HashSet(StringComparer.Ordinal) { "queries", "maxResponseBytes", "estimateOnly" }, "deps" => new HashSet(StringComparer.Ordinal) { "path", "reverse", "format", "cycles", "lang", "limit", "graphBudget", "cursor", "excludePaths", "excludeTests", "includeGenerated", "project", "solution" }, - "impact_analysis" => new HashSet(StringComparer.Ordinal) { "query", "lang", "maxHops", "maxDepth", "limit", "path", "excludePaths", "excludeTests", "includeGenerated", "withPaths", "countOnly", "project", "solution" }, + "impact_analysis" => new HashSet(StringComparer.Ordinal) { "query", "lang", "maxHops", "maxDepth", "limit", "path", "excludePaths", "excludeTests", "includeGenerated", "withPaths", "includeMemberReads", "countOnly", "project", "solution" }, "languages" => new HashSet(StringComparer.Ordinal) { "indexedOnly", "capability", "extension", "alias" }, "validate" => new HashSet(StringComparer.Ordinal) { "kind", "severity", "limit", "cursor", "path", "excludePaths", "excludeTests", "countOnly", "format", "project", "solution" }, "unused_symbols" => new HashSet(StringComparer.Ordinal) { "kind", "lang", "limit", "visibility", "excludeVisibility", "path", "excludePaths", "excludeTests", "bucket", "minConfidence", "byBucket", "project", "solution" }, diff --git a/src/CodeIndex/Mcp/McpToolCatalog.cs b/src/CodeIndex/Mcp/McpToolCatalog.cs index 8932ef3a0..32cdc8b0a 100644 --- a/src/CodeIndex/Mcp/McpToolCatalog.cs +++ b/src/CodeIndex/Mcp/McpToolCatalog.cs @@ -127,6 +127,7 @@ private static JsonArray CreateToolCatalog() ["kind"] = new JsonObject { ["type"] = "string", ["description"] = "Filter by edge kind. Default results use the canonical call, instantiate, subscribe vocabulary; non-default `friend` remains available explicitly. Metadata and type-only kinds — metadata (attribute, annotation), type-position (type_reference), and JS/TS discriminant narrowing (type_tag) — are rejected here; use `references` with the desired kind instead." }, ["rawKinds"] = new JsonObject { ["type"] = "boolean", ["description"] = "Preserve raw reference kinds instead of canonical CLI grouping, matching `--raw-kinds`.", ["default"] = false }, ["includeQualifiedCommonCalls"] = new JsonObject { ["type"] = "boolean", ["description"] = "Include unresolved receiver/type-qualified C# calls with common member names. Resolved qualified calls are already included by default.", ["default"] = false }, + ["includeMemberReads"] = new JsonObject { ["type"] = "boolean", ["description"] = "Include canonical `member_read` value-read edges. Defaults to false; legacy indexes stored these reads as `call` and cannot separate them.", ["default"] = false }, ["rankBy"] = new JsonObject { ["type"] = "string", ["enum"] = new JsonArray { "weighted", "count", "kind" }, ["description"] = "Primary ranking recipe: weighted score then count (default; instantiate=3.0, call=1.0, subscribe=0.1), raw count, or kind priority then count. Only ties use exact-case/name relevance, production before test before docs path category, then stable path/location/name fields. Responses expose the complete applied precedence in rankingRecipe.", ["default"] = "weighted" }, ["lang"] = new JsonObject { ["type"] = "string", ["description"] = "Filter by language" }, ["limit"] = new JsonObject { ["type"] = "integer", ["description"] = "Max results (default: 20). Responses include `truncated`, `more_available`, and `next_offset` when more rows exist.", ["default"] = QueryCommandRunner.DefaultQueryLimit }, @@ -155,6 +156,7 @@ private static JsonArray CreateToolCatalog() ["kind"] = new JsonObject { ["type"] = "string", ["description"] = "Filter by edge kind. Default results use the canonical call, instantiate, subscribe vocabulary; non-default graph kinds remain available explicitly. Metadata and type-only kinds — metadata (attribute, annotation), type-position (type_reference), and JS/TS discriminant narrowing (type_tag) — are rejected here; use `references` with the desired kind instead." }, ["rawKinds"] = new JsonObject { ["type"] = "boolean", ["description"] = "Preserve raw reference kinds instead of canonical CLI grouping, matching `--raw-kinds`.", ["default"] = false }, ["includeQualifiedCommonCalls"] = new JsonObject { ["type"] = "boolean", ["description"] = "Include unresolved receiver/type-qualified C# calls with common member names. Resolved qualified calls are already included by default.", ["default"] = false }, + ["includeMemberReads"] = new JsonObject { ["type"] = "boolean", ["description"] = "Include canonical `member_read` value-read edges. Defaults to false; legacy indexes stored these reads as `call` and cannot separate them.", ["default"] = false }, ["rankBy"] = new JsonObject { ["type"] = "string", ["enum"] = new JsonArray { "weighted", "count", "kind" }, ["description"] = "Primary ranking recipe: weighted score then count (default; instantiate=3.0, call=1.0, subscribe=0.1), raw count, or kind priority then count. Only ties use exact-case/name relevance, production before test before docs path category, then stable path/location/name fields. Responses expose the complete applied precedence in rankingRecipe.", ["default"] = "weighted" }, ["lang"] = new JsonObject { ["type"] = "string", ["description"] = "Filter by language" }, ["limit"] = new JsonObject { ["type"] = "integer", ["description"] = "Max results (default: 20). Responses include `truncated`, `more_available`, and `next_offset` when more rows exist.", ["default"] = QueryCommandRunner.DefaultQueryLimit }, @@ -333,6 +335,7 @@ private static JsonArray CreateToolCatalog() ["excludeTests"] = new JsonObject { ["type"] = "boolean", ["description"] = "Exclude likely test files", ["default"] = false }, ["includeGenerated"] = new JsonObject { ["type"] = "boolean", ["description"] = "Include files detected as generated code", ["default"] = false }, ["withPaths"] = new JsonObject { ["type"] = "boolean", ["description"] = "When true, each caller carries a `paths` array of shortest call chains [resolvedRoot, intermediate..., callerName]; diamond convergence surfaces every shortest route (per-row cap; `pathsTruncated` flag indicates overflow).", ["default"] = false }, + ["includeMemberReads"] = new JsonObject { ["type"] = "boolean", ["description"] = "Include canonical `member_read` value-read edges in impact traversal. Defaults to false; legacy indexes stored these reads as `call` and cannot separate them.", ["default"] = false }, ["countOnly"] = new JsonObject { ["type"] = "boolean", ["description"] = "Return only count metadata and a small top-file histogram; omit caller and file-impact row payloads.", ["default"] = false } }, ["required"] = new JsonArray { "query" } diff --git a/src/CodeIndex/Mcp/McpToolHandlers.ArgumentValidation.cs b/src/CodeIndex/Mcp/McpToolHandlers.ArgumentValidation.cs index fbd7ae464..5947698a0 100644 --- a/src/CodeIndex/Mcp/McpToolHandlers.ArgumentValidation.cs +++ b/src/CodeIndex/Mcp/McpToolHandlers.ArgumentValidation.cs @@ -251,7 +251,7 @@ private static bool TryGetExpectedJsonType(string toolName, string argumentName, "lspCompatible" or "regex" or "withPaths" or "rebuild" or "dryRun" or "dry_run" or "force" or "optimize" or "reverse" or "cycles" or "config" or "logPath" or "updateCheck" or - "rawKinds" or "includeQualifiedCommonCalls" or "orderBySize" or "rawBytes" or "byBucket" or "memoryTrace" or "watch" or + "rawKinds" or "includeQualifiedCommonCalls" or "includeMemberReads" or "orderBySize" or "rawBytes" or "byBucket" or "memoryTrace" or "watch" or "estimateOnly" or "listRecipes" => "boolean", "project" or "capability" or "scopes" or "fields" or "visibility" or "excludeVisibility" or "includeSymbolKind" or "excludeSymbolKind" or "commits" or "changedBetween" or "files" or diff --git a/src/CodeIndex/Mcp/McpToolHandlers.Graph.cs b/src/CodeIndex/Mcp/McpToolHandlers.Graph.cs index 3ac37e3e5..4752199f5 100644 --- a/src/CodeIndex/Mcp/McpToolHandlers.Graph.cs +++ b/src/CodeIndex/Mcp/McpToolHandlers.Graph.cs @@ -162,20 +162,22 @@ private JsonNode ExecuteCallers(JsonNode? id, JsonNode? args) var countOnly = ReadCountOnly(args) || format == "count"; var rawKinds = args?["rawKinds"]?.GetValue() ?? false; var includeQualifiedCommonCalls = args?["includeQualifiedCommonCalls"]?.GetValue() ?? false; + var includeMemberReads = args?["includeMemberReads"]?.GetValue() ?? false; return WithDbReader(id, args, reader => { if (countOnly) { - var countOnlyTotal = reader.CountCallersTotal(query, lang, kind, pathPatterns, excludePaths, excludeTests, exact, rawKinds, includeQualifiedCommonCalls).Count; + var countOnlyTotal = reader.CountCallersTotal(query, lang, kind, pathPatterns, excludePaths, excludeTests, exact, rawKinds, includeQualifiedCommonCalls, includeMemberReads).Count; var histogramResults = countOnlyTotal > 0 - ? reader.GetCallers(query, Math.Min(countOnlyTotal, MaxLimit), lang, kind, pathPatterns, excludePaths, excludeTests, exact, rawKinds, rankMode: rankMode, includeQualifiedCommonCalls: includeQualifiedCommonCalls) + ? reader.GetCallers(query, Math.Min(countOnlyTotal, MaxLimit), lang, kind, pathPatterns, excludePaths, excludeTests, exact, rawKinds, rankMode: rankMode, includeQualifiedCommonCalls: includeQualifiedCommonCalls, includeMemberReads: includeMemberReads) : []; var countOnlyPayload = BuildCountOnlyPayload(countOnlyTotal, countOnlyTotal, truncated: false, histogramResults, result => result.Path); countOnlyPayload["query"] = query; countOnlyPayload["kind"] = kind; countOnlyPayload["rawKinds"] = rawKinds; countOnlyPayload["includeQualifiedCommonCalls"] = includeQualifiedCommonCalls; + countOnlyPayload["includeMemberReads"] = includeMemberReads; countOnlyPayload["lang"] = lang; countOnlyPayload["path"] = PathEcho(pathPatterns); countOnlyPayload["excludeTests"] = excludeTests; @@ -192,10 +194,10 @@ private JsonNode ExecuteCallers(JsonNode? id, JsonNode? args) return CreateToolResult(id, $"Counted {ConsoleUi.Counted(countOnlyTotal, "caller")}.", countOnlyPayload); } - var results = reader.GetCallers(query, FetchLimitForEnvelope(limit), lang, kind, pathPatterns, excludePaths, excludeTests, exact, rawKinds, rankMode: rankMode, offset: offset, includeQualifiedCommonCalls: includeQualifiedCommonCalls); + var results = reader.GetCallers(query, FetchLimitForEnvelope(limit), lang, kind, pathPatterns, excludePaths, excludeTests, exact, rawKinds, rankMode: rankMode, offset: offset, includeQualifiedCommonCalls: includeQualifiedCommonCalls, includeMemberReads: includeMemberReads); var truncated = TrimToRequestedLimit(results, limit); var total = truncated || offset > 0 - ? reader.CountCallersTotal(query, lang, kind, pathPatterns, excludePaths, excludeTests, exact, rawKinds, includeQualifiedCommonCalls).Count + ? reader.CountCallersTotal(query, lang, kind, pathPatterns, excludePaths, excludeTests, exact, rawKinds, includeQualifiedCommonCalls, includeMemberReads).Count : results.Count; var graphSupport = ResolveGraphSupport(reader, exact, query, lang, pathPatterns, excludePaths, excludeTests); var sqlGraphSignal = QueryCommandRunner.NarrowSqlGraphContractSignalByLanguages( @@ -206,9 +208,9 @@ private JsonNode ExecuteCallers(JsonNode? id, JsonNode? args) var exactSignal = reader.GetCallersExactQuerySignal(lang, pathPatterns, excludePaths, excludeTests, includeSqlGraphContractSignal: sqlGraphSignal.Relevant); var exactZeroHint = QueryCommandRunner.BuildExactZeroHint( exact && reader._hasReferencesTable, - () => reader.CountCallers(query, QueryCommandRunner.ExactZeroHintProbeLimit, lang, kind, pathPatterns, excludePaths, excludeTests, exact: false, rawKinds: rawKinds, includeQualifiedCommonCalls: includeQualifiedCommonCalls) > 0, - () => reader.CountCallers(query, limit, lang, kind, pathPatterns, excludePaths, excludeTests, exact: false, rawKinds: rawKinds, includeQualifiedCommonCalls: includeQualifiedCommonCalls), - () => reader.GetCallers(query, Math.Min(limit, QueryCommandRunner.ExactZeroHintSampleLimit), lang, kind, pathPatterns, excludePaths, excludeTests, exact: false, rawKinds: rawKinds, rankMode: rankMode, includeQualifiedCommonCalls: includeQualifiedCommonCalls), + () => reader.CountCallers(query, QueryCommandRunner.ExactZeroHintProbeLimit, lang, kind, pathPatterns, excludePaths, excludeTests, exact: false, rawKinds: rawKinds, includeQualifiedCommonCalls: includeQualifiedCommonCalls, includeMemberReads: includeMemberReads) > 0, + () => reader.CountCallers(query, limit, lang, kind, pathPatterns, excludePaths, excludeTests, exact: false, rawKinds: rawKinds, includeQualifiedCommonCalls: includeQualifiedCommonCalls, includeMemberReads: includeMemberReads), + () => reader.GetCallers(query, Math.Min(limit, QueryCommandRunner.ExactZeroHintSampleLimit), lang, kind, pathPatterns, excludePaths, excludeTests, exact: false, rawKinds: rawKinds, rankMode: rankMode, includeQualifiedCommonCalls: includeQualifiedCommonCalls, includeMemberReads: includeMemberReads), r => r.CalleeName); var payload = new JsonObject { @@ -216,6 +218,7 @@ private JsonNode ExecuteCallers(JsonNode? id, JsonNode? args) ["kind"] = kind, ["rawKinds"] = rawKinds, ["includeQualifiedCommonCalls"] = includeQualifiedCommonCalls, + ["includeMemberReads"] = includeMemberReads, ["lang"] = lang, ["path"] = PathEcho(pathPatterns), ["excludeTests"] = excludeTests, @@ -291,20 +294,22 @@ private JsonNode ExecuteCallees(JsonNode? id, JsonNode? args) var countOnly = ReadCountOnly(args) || format == "count"; var rawKinds = args?["rawKinds"]?.GetValue() ?? false; var includeQualifiedCommonCalls = args?["includeQualifiedCommonCalls"]?.GetValue() ?? false; + var includeMemberReads = args?["includeMemberReads"]?.GetValue() ?? false; return WithDbReader(id, args, reader => { if (countOnly) { - var countOnlyTotal = reader.CountCalleesTotal(query, lang, kind, pathPatterns, excludePaths, excludeTests, exact, rawKinds, includeQualifiedCommonCalls).Count; + var countOnlyTotal = reader.CountCalleesTotal(query, lang, kind, pathPatterns, excludePaths, excludeTests, exact, rawKinds, includeQualifiedCommonCalls, includeMemberReads).Count; var histogramResults = countOnlyTotal > 0 - ? reader.GetCallees(query, Math.Min(countOnlyTotal, MaxLimit), lang, kind, pathPatterns, excludePaths, excludeTests, exact, rawKinds, rankMode: rankMode, includeQualifiedCommonCalls: includeQualifiedCommonCalls) + ? reader.GetCallees(query, Math.Min(countOnlyTotal, MaxLimit), lang, kind, pathPatterns, excludePaths, excludeTests, exact, rawKinds, rankMode: rankMode, includeQualifiedCommonCalls: includeQualifiedCommonCalls, includeMemberReads: includeMemberReads) : []; var countOnlyPayload = BuildCountOnlyPayload(countOnlyTotal, countOnlyTotal, truncated: false, histogramResults, result => result.Path); countOnlyPayload["query"] = query; countOnlyPayload["kind"] = kind; countOnlyPayload["rawKinds"] = rawKinds; countOnlyPayload["includeQualifiedCommonCalls"] = includeQualifiedCommonCalls; + countOnlyPayload["includeMemberReads"] = includeMemberReads; countOnlyPayload["lang"] = lang; countOnlyPayload["path"] = PathEcho(pathPatterns); countOnlyPayload["excludeTests"] = excludeTests; @@ -321,10 +326,10 @@ private JsonNode ExecuteCallees(JsonNode? id, JsonNode? args) return CreateToolResult(id, $"Counted {ConsoleUi.Counted(countOnlyTotal, "callee")}.", countOnlyPayload); } - var results = reader.GetCallees(query, FetchLimitForEnvelope(limit), lang, kind, pathPatterns, excludePaths, excludeTests, exact, rawKinds, rankMode: rankMode, offset: offset, includeQualifiedCommonCalls: includeQualifiedCommonCalls); + var results = reader.GetCallees(query, FetchLimitForEnvelope(limit), lang, kind, pathPatterns, excludePaths, excludeTests, exact, rawKinds, rankMode: rankMode, offset: offset, includeQualifiedCommonCalls: includeQualifiedCommonCalls, includeMemberReads: includeMemberReads); var truncated = TrimToRequestedLimit(results, limit); var total = truncated || offset > 0 - ? reader.CountCalleesTotal(query, lang, kind, pathPatterns, excludePaths, excludeTests, exact, rawKinds, includeQualifiedCommonCalls).Count + ? reader.CountCalleesTotal(query, lang, kind, pathPatterns, excludePaths, excludeTests, exact, rawKinds, includeQualifiedCommonCalls, includeMemberReads).Count : results.Count; var graphSupport = ResolveGraphSupport(reader, exact, query, lang, pathPatterns, excludePaths, excludeTests); var sqlGraphSignal = QueryCommandRunner.NarrowSqlGraphContractSignalByLanguages( @@ -335,9 +340,9 @@ private JsonNode ExecuteCallees(JsonNode? id, JsonNode? args) var exactSignal = reader.GetCalleesExactQuerySignal(lang, pathPatterns, excludePaths, excludeTests, includeSqlGraphContractSignal: sqlGraphSignal.Relevant); var exactZeroHint = QueryCommandRunner.BuildExactZeroHint( exact && reader._hasReferencesTable, - () => reader.CountCallees(query, QueryCommandRunner.ExactZeroHintProbeLimit, lang, kind, pathPatterns, excludePaths, excludeTests, exact: false, rawKinds: rawKinds, includeQualifiedCommonCalls: includeQualifiedCommonCalls) > 0, - () => reader.CountCallees(query, limit, lang, kind, pathPatterns, excludePaths, excludeTests, exact: false, rawKinds: rawKinds, includeQualifiedCommonCalls: includeQualifiedCommonCalls), - () => reader.GetCallees(query, Math.Min(limit, QueryCommandRunner.ExactZeroHintSampleLimit), lang, kind, pathPatterns, excludePaths, excludeTests, exact: false, rawKinds: rawKinds, rankMode: rankMode, includeQualifiedCommonCalls: includeQualifiedCommonCalls), + () => reader.CountCallees(query, QueryCommandRunner.ExactZeroHintProbeLimit, lang, kind, pathPatterns, excludePaths, excludeTests, exact: false, rawKinds: rawKinds, includeQualifiedCommonCalls: includeQualifiedCommonCalls, includeMemberReads: includeMemberReads) > 0, + () => reader.CountCallees(query, limit, lang, kind, pathPatterns, excludePaths, excludeTests, exact: false, rawKinds: rawKinds, includeQualifiedCommonCalls: includeQualifiedCommonCalls, includeMemberReads: includeMemberReads), + () => reader.GetCallees(query, Math.Min(limit, QueryCommandRunner.ExactZeroHintSampleLimit), lang, kind, pathPatterns, excludePaths, excludeTests, exact: false, rawKinds: rawKinds, rankMode: rankMode, includeQualifiedCommonCalls: includeQualifiedCommonCalls, includeMemberReads: includeMemberReads), r => r.CallerName); var payload = new JsonObject { @@ -345,6 +350,7 @@ private JsonNode ExecuteCallees(JsonNode? id, JsonNode? args) ["kind"] = kind, ["rawKinds"] = rawKinds, ["includeQualifiedCommonCalls"] = includeQualifiedCommonCalls, + ["includeMemberReads"] = includeMemberReads, ["lang"] = lang, ["path"] = PathEcho(pathPatterns), ["excludeTests"] = excludeTests, diff --git a/src/CodeIndex/Mcp/McpToolHandlers.ImpactAnalysis.cs b/src/CodeIndex/Mcp/McpToolHandlers.ImpactAnalysis.cs index efbf74aa2..fa6c48a43 100644 --- a/src/CodeIndex/Mcp/McpToolHandlers.ImpactAnalysis.cs +++ b/src/CodeIndex/Mcp/McpToolHandlers.ImpactAnalysis.cs @@ -42,11 +42,12 @@ private JsonNode ExecuteImpactAnalysis(JsonNode? id, JsonNode? args) var excludePaths = ReadStringList(args, "excludePaths"); var excludeTests = args?["excludeTests"]?.GetValue() ?? false; var withPaths = args?["withPaths"]?.GetValue() ?? false; + var includeMemberReads = args?["includeMemberReads"]?.GetValue() ?? false; var countOnly = ReadCountOnly(args); return WithDbReader(id, args, reader => { - var analysis = reader.AnalyzeImpact(query, maxDepth, limit, lang, pathPatterns, excludePaths, excludeTests, withPaths); + var analysis = reader.AnalyzeImpact(query, maxDepth, limit, lang, pathPatterns, excludePaths, excludeTests, withPaths, includeMemberReads: includeMemberReads); var sqlGraphSignal = QueryCommandRunner.NarrowSqlGraphContractSignal( reader.GetSqlGraphContractSignal(lang, pathPatterns, excludePaths, excludeTests), DbReader.IsSqlLanguage(lang) @@ -84,6 +85,7 @@ private JsonNode ExecuteImpactAnalysis(JsonNode? id, JsonNode? args) ["termination_reason"] = analysis.TerminationReason, ["impact_mode"] = analysis.ImpactMode, ["heuristic"] = analysis.Heuristic, + ["includeMemberReads"] = includeMemberReads, ["top_files"] = topFiles, ["results"] = new JsonArray(), }; @@ -120,6 +122,7 @@ private JsonNode ExecuteImpactAnalysis(JsonNode? id, JsonNode? args) ["cycle_detected"] = analysis.CycleDetected, ["impact_mode"] = analysis.ImpactMode, ["heuristic"] = analysis.Heuristic, + ["includeMemberReads"] = includeMemberReads, ["callers"] = ToJsonArray(analysis.Callers), ["file_impacts"] = ToJsonArray(analysis.FileImpacts), ["definition_count"] = analysis.DefinitionCount, diff --git a/src/CodeIndex/Models/SymbolKindCatalog.cs b/src/CodeIndex/Models/SymbolKindCatalog.cs index da39e9d6e..c77aae32a 100644 --- a/src/CodeIndex/Models/SymbolKindCatalog.cs +++ b/src/CodeIndex/Models/SymbolKindCatalog.cs @@ -120,6 +120,7 @@ public static class SymbolKindCatalog "instantiate", "join_condition_reference", "lifetime_reference", + "member_read", "metadata", "project_reference", "reference", diff --git a/tests/CodeIndex.Tests/CliFlagSchemaTests.cs b/tests/CodeIndex.Tests/CliFlagSchemaTests.cs index 5fa227aac..e9568f76e 100644 --- a/tests/CodeIndex.Tests/CliFlagSchemaTests.cs +++ b/tests/CodeIndex.Tests/CliFlagSchemaTests.cs @@ -179,6 +179,20 @@ public void AuditAggregationFlags_SurfaceDocumentedRecipeGrouping_Issues4301_433 } } + [Fact] + public void MemberReadCompatibilityFlag_IsScopedToGraphTraversalCommands_Issue4894() + { + const string flag = "--include-member-reads"; + foreach (var command in new[] { "callers", "callees", "impact" }) + { + Assert.Contains(flag, CliFlagSchema.GetAcceptedFlagNamesForCommand(command)); + Assert.Contains(CliFlagSchema.GetCompletionFlagsForCommand(command), option => option.Name == flag); + } + + Assert.DoesNotContain(flag, CliFlagSchema.GetAcceptedFlagNamesForCommand("references")); + Assert.DoesNotContain(flag, CliFlagSchema.GetAcceptedFlagNamesForCommand("search")); + } + [Fact] public void UpgradeFlags_SurfaceImplementedSelectionAndJsonOptions() { diff --git a/tests/CodeIndex.Tests/DbReaderTests.cs b/tests/CodeIndex.Tests/DbReaderTests.cs index 26dc79581..2bd368cc8 100644 --- a/tests/CodeIndex.Tests/DbReaderTests.cs +++ b/tests/CodeIndex.Tests/DbReaderTests.cs @@ -941,6 +941,81 @@ public void GetCallers_SolutionProjectReference_RequiresExplicitKind_Issue3662() Assert.Equal("project_reference", caller.ReferenceKind); } + [Fact] + public void MemberReadEdges_AreOptInWhileLegacyCallRowsRemainReadable_Issue4894() + { + InsertManualReference( + "src/modern.cs", + "csharp", + "function", + "ModernReader", + "CurrentValue", + "member_read"); + InsertManualReference( + "src/legacy.cs", + "csharp", + "function", + "LegacyReader", + "LegacyValue", + "call"); + + Assert.Empty(_reader.GetCallers("CurrentValue", lang: "csharp", exact: true)); + Assert.Equal(0, _reader.CountCallers("CurrentValue", lang: "csharp", exact: true)); + Assert.Equal(0, _reader.CountCallersTotal("CurrentValue", lang: "csharp", exact: true).Count); + Assert.Empty(_reader.GetCallees("ModernReader", lang: "csharp", exact: true)); + Assert.Empty(_reader.GetTransitiveCallers("CurrentValue", maxDepth: 1, lang: "csharp").Results); + Assert.Empty(_reader.AnalyzeImpact("CurrentValue", maxDepth: 1, lang: "csharp").Callers); + + var caller = Assert.Single(_reader.GetCallers( + "CurrentValue", + lang: "csharp", + exact: true, + includeMemberReads: true)); + Assert.Equal("ModernReader", caller.CallerName); + Assert.Equal("member_read", caller.ReferenceKind); + Assert.Single(_reader.GetCallers( + "CurrentValue", + lang: "csharp", + referenceKind: "member_read", + exact: true)); + Assert.Equal( + 1, + _reader.CountCallers( + "CurrentValue", + lang: "csharp", + exact: true, + includeMemberReads: true)); + Assert.Equal( + 1, + _reader.CountCallersTotal( + "CurrentValue", + lang: "csharp", + exact: true, + includeMemberReads: true).Count); + + var callee = Assert.Single(_reader.GetCallees( + "ModernReader", + lang: "csharp", + exact: true, + includeMemberReads: true)); + Assert.Equal("CurrentValue", callee.CalleeName); + Assert.Equal("member_read", callee.ReferenceKind); + Assert.Single(_reader.GetTransitiveCallers( + "CurrentValue", + maxDepth: 1, + lang: "csharp", + includeMemberReads: true).Results); + Assert.Single(_reader.AnalyzeImpact( + "CurrentValue", + maxDepth: 1, + lang: "csharp", + includeMemberReads: true).Callers); + + var legacyCaller = Assert.Single(_reader.GetCallers("LegacyValue", lang: "csharp", exact: true)); + Assert.Equal("LegacyReader", legacyCaller.CallerName); + Assert.Equal("call", legacyCaller.ReferenceKind); + } + [Fact] public void GetCallers_RepositoryMetadataAndManifestReferencesParticipateInGraph_Issue4740() { diff --git a/tests/CodeIndex.Tests/IndexCommandRunnerFullScanTests.cs b/tests/CodeIndex.Tests/IndexCommandRunnerFullScanTests.cs index 4031b87de..68394e3bd 100644 --- a/tests/CodeIndex.Tests/IndexCommandRunnerFullScanTests.cs +++ b/tests/CodeIndex.Tests/IndexCommandRunnerFullScanTests.cs @@ -4787,7 +4787,78 @@ FROM symbol_references SymbolExtractor.CSharpContractVersion.ToString( System.Globalization.CultureInfo.InvariantCulture), versionCmd.ExecuteScalar() as string); - Assert.Equal(8, SymbolExtractor.CSharpContractVersion); + Assert.Equal(9, SymbolExtractor.CSharpContractVersion); + } + finally + { + DeleteDirectory(projectRoot); + } + } + + [Fact] + public void Run_FullScan_ReclassifiesQualifiedValueReadsFromVersion8CSharpIndex_Issue4894() + { + var projectRoot = CreateTempProject(); + try + { + File.WriteAllText( + Path.Combine(projectRoot, "Values.cs"), + """ + public static class Values + { + public const int Limit = 10; + } + + public sealed class Caller + { + public int Read() => Values.Limit; + } + """); + + Assert.Equal( + CommandExitCodes.Success, + IndexCommandRunner.Run([projectRoot, "--json"], _jsonOptions)); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + using (var conn = OpenNonPoolingConnection(dbPath)) + { + conn.Open(); + using var cmd = conn.CreateCommand(); + cmd.CommandText = $""" + UPDATE symbol_references + SET reference_kind = 'call' + WHERE symbol_name = 'Limit'; + UPDATE codeindex_meta + SET value = '8' + WHERE key = '{DbContext.GetSymbolExtractorVersionMetaKey("csharp")}'; + """; + cmd.ExecuteNonQuery(); + } + + var (exitCode, json) = RunAndCaptureJson([projectRoot, "--json"]); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal("success", json.GetProperty("status").GetString()); + Assert.Equal(0, json.GetProperty("summary").GetProperty("files_skipped").GetInt32()); + + using var verify = OpenNonPoolingConnection(dbPath); + verify.Open(); + using var referenceCmd = verify.CreateCommand(); + referenceCmd.CommandText = """ + SELECT reference_kind + FROM symbol_references + WHERE symbol_name = 'Limit' + """; + Assert.Equal("member_read", referenceCmd.ExecuteScalar() as string); + + using var versionCmd = verify.CreateCommand(); + versionCmd.CommandText = + $"SELECT value FROM codeindex_meta WHERE key = '{DbContext.GetSymbolExtractorVersionMetaKey("csharp")}'"; + Assert.Equal( + SymbolExtractor.CSharpContractVersion.ToString( + System.Globalization.CultureInfo.InvariantCulture), + versionCmd.ExecuteScalar() as string); + Assert.Equal(9, SymbolExtractor.CSharpContractVersion); } finally { diff --git a/tests/CodeIndex.Tests/McpServerToolsCallTests.cs b/tests/CodeIndex.Tests/McpServerToolsCallTests.cs index 236264dd8..aa14043ba 100644 --- a/tests/CodeIndex.Tests/McpServerToolsCallTests.cs +++ b/tests/CodeIndex.Tests/McpServerToolsCallTests.cs @@ -21,6 +21,58 @@ namespace CodeIndex.Tests; public partial class McpServerTests { + [Fact] + public void ToolsCall_MemberReadsAreOptInForGraphTraversal_Issue4894() + { + InsertIndexedFile( + "src/member-read.cs", + "csharp", + """ + public static class Values + { + public const int Limit = 10; + } + + public sealed class Reader + { + public int Read() => Values.Limit; + } + """); + + JsonNode Call(string tool, bool includeMemberReads) + { + var request = new JsonObject + { + ["jsonrpc"] = "2.0", + ["id"] = 4894, + ["method"] = "tools/call", + ["params"] = new JsonObject + { + ["name"] = tool, + ["arguments"] = new JsonObject + { + ["query"] = tool == "callees" ? "Read" : "Limit", + ["lang"] = "csharp", + ["includeMemberReads"] = includeMemberReads, + ["countOnly"] = true, + }, + }, + }; + return _server.HandleMessage(request)!["result"]!["structuredContent"]!; + } + + foreach (var tool in new[] { "callers", "callees", "impact_analysis" }) + { + var excluded = Call(tool, includeMemberReads: false); + Assert.False(excluded["includeMemberReads"]!.GetValue()); + Assert.Equal(0, excluded["count"]!.GetValue()); + + var included = Call(tool, includeMemberReads: true); + Assert.True(included["includeMemberReads"]!.GetValue()); + Assert.Equal(1, included["count"]!.GetValue()); + } + } + [Theory] [InlineData("callers", "{\"query\":\"Run\",\"countOnly\":true}")] [InlineData("callees", "{\"query\":\"Run\"}")] @@ -2150,7 +2202,7 @@ public class UsesEnum } """); - var request = JsonNode.Parse("""{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"callers","arguments":{"query":"A","lang":"csharp","exact":true}}}""")!; + var request = JsonNode.Parse("""{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"callers","arguments":{"query":"A","lang":"csharp","exact":true,"includeMemberReads":true}}}""")!; var response = _server.HandleMessage(request)!; var structured = response["result"]!["structuredContent"]!; @@ -2159,6 +2211,7 @@ public class UsesEnum Assert.True(structured["graph_supported"]!.GetValue()); Assert.Null(structured["graphDegraded"]); Assert.Null(structured["unsupportedSymbolKind"]); + Assert.True(structured["includeMemberReads"]!.GetValue()); Assert.Equal("Value", structured["results"]![0]!["callerName"]!.GetValue()); Assert.Equal("Found 1 caller.", response["result"]!["content"]![0]!["text"]!.GetValue()); } diff --git a/tests/CodeIndex.Tests/McpServerToolsListTests.cs b/tests/CodeIndex.Tests/McpServerToolsListTests.cs index f1c650819..4873d5b05 100644 --- a/tests/CodeIndex.Tests/McpServerToolsListTests.cs +++ b/tests/CodeIndex.Tests/McpServerToolsListTests.cs @@ -53,6 +53,25 @@ public void ToolsList_QualifiedCommonCallCompletenessOption_IsScopedToGraphTools } } + [Fact] + public void ToolsList_MemberReadCompatibilityOption_IsScopedToTraversalTools_Issue4894() + { + 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 toolName in new[] { "callers", "callees", "impact_analysis" }) + { + var tool = tools.First(candidate => candidate!["name"]!.GetValue() == toolName)!; + var option = tool["inputSchema"]!["properties"]!["includeMemberReads"]!; + Assert.Equal("boolean", option["type"]!.GetValue()); + Assert.False(option["default"]!.GetValue()); + } + + var references = tools.First(candidate => candidate!["name"]!.GetValue() == "references")!; + Assert.Null(references["inputSchema"]!["properties"]!["includeMemberReads"]); + } + [Fact] public void ToolsList_EachToolPublishesSchemaAndExampleContract() { diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerImpactTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerImpactTests.cs index 3701366bc..d181a43cc 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerImpactTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerImpactTests.cs @@ -110,6 +110,7 @@ public void GetTransitiveCallers_MaxDepthBoundaryProbeBudgetTerminatesStably_Iss null, false, false, + false, ]); Assert.NotNull(inspection); var type = inspection!.GetType(); diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerInspectTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerInspectTests.cs index 3ff03f960..d2d335a1e 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerInspectTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerInspectTests.cs @@ -4456,14 +4456,13 @@ class C { int N => 0; void M() { var x = global::N.Color.Red; } } using var document = ParseJsonOutput(stdout); var json = document.RootElement; var reference = Assert.Single(json.GetProperty("references").EnumerateArray()); - var caller = Assert.Single(json.GetProperty("callers").EnumerateArray()); Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); Assert.Equal("function", reference.GetProperty("container_kind").GetString()); Assert.Equal("M", reference.GetProperty("container_name").GetString()); - Assert.Equal("function", caller.GetProperty("caller_kind").GetString()); - Assert.Equal("M", caller.GetProperty("caller_name").GetString()); + Assert.Equal("member_read", reference.GetProperty("reference_kind").GetString()); + Assert.Empty(json.GetProperty("callers").EnumerateArray()); } finally { @@ -4760,7 +4759,9 @@ public int Read() Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); - Assert.Empty(json.GetProperty("references").EnumerateArray()); + var reference = Assert.Single(json.GetProperty("references").EnumerateArray()); + Assert.Equal("member_read", reference.GetProperty("reference_kind").GetString()); + Assert.Equal("Read", reference.GetProperty("container_name").GetString()); Assert.Empty(json.GetProperty("callers").EnumerateArray()); Assert.Equal("csharp", json.GetProperty("graph_language").GetString()); Assert.True(json.GetProperty("graph_supported").GetBoolean()); @@ -5188,7 +5189,7 @@ public Demo.Status Read(bool flag) Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); - Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("member_read", json.GetProperty("reference_kind").GetString()); Assert.Equal(23, json.GetProperty("line").GetInt32()); Assert.Equal("Read", json.GetProperty("container_name").GetString()); } @@ -5372,7 +5373,7 @@ public Demo.Status ReadComment(object value) Assert.Equal("primary_candidate", json.GetProperty("graph_scope").GetString()); Assert.Empty(json.GetProperty("references").EnumerateArray()); Assert.Equal(["ReadComment", "ReadRecursive"], referenceContainers); - Assert.Equal([1, 1], callerReferenceCounts); + Assert.Empty(callerReferenceCounts); Assert.Equal("csharp", json.GetProperty("graph_language").GetString()); Assert.True(json.GetProperty("graph_supported").GetBoolean()); } diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerReferencesTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerReferencesTests.cs index a17baccb2..6d7f3e24c 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerReferencesTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerReferencesTests.cs @@ -633,7 +633,7 @@ class Demo MarkGraphAndFoldReady(dbPath); var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( - ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name", "--kind", "member_read"], _jsonOptions)); Assert.Equal(CommandExitCodes.Success, exitCode); @@ -643,12 +643,12 @@ class Demo Assert.Single(references); var reference = references[0]; Assert.Equal("Red", reference.GetProperty("symbol_name").GetString()); - Assert.Equal("call", reference.GetProperty("reference_kind").GetString()); + Assert.Equal("member_read", reference.GetProperty("reference_kind").GetString()); Assert.Equal("Match", reference.GetProperty("container_name").GetString()); Assert.Contains("value is Color.Red or Color.Blue;", reference.GetProperty("context").GetString()); var (countExitCode, countStdout, countStderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( - ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name", "--count"], + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name", "--kind", "member_read", "--count"], _jsonOptions)); Assert.Equal(CommandExitCodes.Success, countExitCode); @@ -1518,7 +1518,7 @@ class C { int N => 0; void M() { var x = global::N.Color.Red; } } MarkGraphAndFoldReady(dbPath); var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunCallers( - ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name", "--include-member-reads"], _jsonOptions)); using var document = ParseJsonOutput(stdout); @@ -2409,7 +2409,7 @@ public void Use() Assert.Equal(string.Empty, stderr); Assert.Equal("src/cases.cs", json.GetProperty("path").GetString()); Assert.Equal("A", json.GetProperty("symbol_name").GetString()); - Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("member_read", json.GetProperty("reference_kind").GetString()); Assert.Equal("function", json.GetProperty("container_kind").GetString()); Assert.Equal("Use", json.GetProperty("container_name").GetString()); Assert.True(json.GetProperty("exact_index_available").GetBoolean()); @@ -2582,7 +2582,7 @@ public class UsesEnum MarkGraphAndFoldReady(dbPath); var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunCallers( - ["A", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + ["A", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name", "--include-member-reads"], _jsonOptions)); using var document = ParseJsonOutput(stdout); @@ -3103,7 +3103,7 @@ public Demo.Status Read() MarkGraphAndFoldReady(dbPath); var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( - ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name", "--kind", "member_read"], _jsonOptions)); using var document = ParseJsonOutput(stdout); @@ -3111,7 +3111,7 @@ public Demo.Status Read() Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); - Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("member_read", json.GetProperty("reference_kind").GetString()); Assert.Equal(35, json.GetProperty("line").GetInt32()); Assert.Equal("Read", json.GetProperty("container_name").GetString()); } @@ -3122,7 +3122,7 @@ public Demo.Status Read() } [Fact] - public void RunReferences_ExactJson_CSharpLaterSiblingAliasRebindingDoesNotStealEarlierEnumScope() + public void RunReferences_ExactJson_CSharpSiblingAliasMemberReadsRemainDeterministic_Issue4894() { var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_alias_rebinding"); try @@ -3172,7 +3172,7 @@ public int Read() MarkGraphAndFoldReady(dbPath); var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( - ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name", "--kind", "member_read"], _jsonOptions)); using var document = ParseJsonOutput(stdout); @@ -3180,8 +3180,8 @@ public int Read() Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); - Assert.Equal("call", json.GetProperty("reference_kind").GetString()); - Assert.Equal(22, json.GetProperty("line").GetInt32()); + Assert.Equal("member_read", json.GetProperty("reference_kind").GetString()); + Assert.Equal(35, json.GetProperty("line").GetInt32()); Assert.Equal("Read", json.GetProperty("container_name").GetString()); } finally @@ -3232,7 +3232,7 @@ public static Demo.Status Read() Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); - Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("member_read", json.GetProperty("reference_kind").GetString()); Assert.Equal(19, json.GetProperty("line").GetInt32()); Assert.Equal("Read", json.GetProperty("container_name").GetString()); } @@ -3287,7 +3287,7 @@ public Demo.Status Value Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); - Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("member_read", json.GetProperty("reference_kind").GetString()); Assert.Equal("Value", json.GetProperty("container_name").GetString()); Assert.Equal("property", json.GetProperty("container_kind").GetString()); } @@ -3404,7 +3404,7 @@ public Demo.Status Read() Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); - Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("member_read", json.GetProperty("reference_kind").GetString()); Assert.Equal("Read", json.GetProperty("container_name").GetString()); } finally @@ -3464,7 +3464,7 @@ public Status Read() Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); - Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("member_read", json.GetProperty("reference_kind").GetString()); Assert.Equal("Read", json.GetProperty("container_name").GetString()); } finally @@ -3527,7 +3527,7 @@ public Status Read(bool flag) Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); - Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("member_read", json.GetProperty("reference_kind").GetString()); Assert.Equal("Read", json.GetProperty("container_name").GetString()); } finally @@ -6113,7 +6113,7 @@ void AssertSingleReference(string path, int? expectedLine) Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); - Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("member_read", json.GetProperty("reference_kind").GetString()); Assert.Equal("Read", json.GetProperty("container_name").GetString()); if (expectedLine.HasValue) Assert.Equal(expectedLine.Value, json.GetProperty("line").GetInt32()); @@ -6203,7 +6203,7 @@ void AssertSingleReference(string path, int? expectedLine) Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); - Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("member_read", json.GetProperty("reference_kind").GetString()); Assert.Equal("Read", json.GetProperty("container_name").GetString()); if (expectedLine.HasValue) Assert.Equal(expectedLine.Value, json.GetProperty("line").GetInt32()); @@ -6317,7 +6317,7 @@ void AssertSingleReference(string path, int? expectedLine) Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); - Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("member_read", json.GetProperty("reference_kind").GetString()); Assert.Equal("Read", json.GetProperty("container_name").GetString()); if (expectedLine.HasValue) Assert.Equal(expectedLine.Value, json.GetProperty("line").GetInt32()); @@ -6483,7 +6483,7 @@ void AssertSingleReference(string path, int expectedLine) Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); - Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("member_read", json.GetProperty("reference_kind").GetString()); Assert.Equal(expectedLine, json.GetProperty("line").GetInt32()); Assert.Equal("Read", json.GetProperty("container_name").GetString()); } @@ -6542,7 +6542,7 @@ public RealNs.Status Read(object[] values) Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); - Assert.Equal("call", first.RootElement.GetProperty("reference_kind").GetString()); + Assert.Equal("member_read", first.RootElement.GetProperty("reference_kind").GetString()); Assert.Equal([19, 22], lines); } finally @@ -6599,7 +6599,7 @@ public RealNs.Status Read(object[] values) Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); - Assert.Equal("call", first.RootElement.GetProperty("reference_kind").GetString()); + Assert.Equal("member_read", first.RootElement.GetProperty("reference_kind").GetString()); Assert.Equal([19, 22], lines); } finally @@ -6655,7 +6655,7 @@ public Demo.Status Read(object value) Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); - Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("member_read", json.GetProperty("reference_kind").GetString()); Assert.Equal(24, json.GetProperty("line").GetInt32()); Assert.Equal("Read", json.GetProperty("container_name").GetString()); } @@ -6707,7 +6707,7 @@ public Demo.Status Read(object value) Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); - Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("member_read", json.GetProperty("reference_kind").GetString()); Assert.Equal(19, json.GetProperty("line").GetInt32()); Assert.Equal("Read", json.GetProperty("container_name").GetString()); } @@ -6762,7 +6762,7 @@ public Demo.Status Read(object value) Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); - Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("member_read", json.GetProperty("reference_kind").GetString()); Assert.Equal(22, json.GetProperty("line").GetInt32()); Assert.Equal("Read", json.GetProperty("container_name").GetString()); } @@ -6820,7 +6820,7 @@ public Demo.Status Read(object value) Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); - Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("member_read", json.GetProperty("reference_kind").GetString()); Assert.Equal(25, json.GetProperty("line").GetInt32()); Assert.Equal("Read", json.GetProperty("container_name").GetString()); } @@ -6941,7 +6941,7 @@ from Status in items Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); - Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("member_read", json.GetProperty("reference_kind").GetString()); Assert.Equal(28, json.GetProperty("line").GetInt32()); Assert.Equal("Read", json.GetProperty("container_name").GetString()); } @@ -7148,7 +7148,7 @@ public Status Read(IEnumerable items) Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); - Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("member_read", json.GetProperty("reference_kind").GetString()); Assert.Equal(28, json.GetProperty("line").GetInt32()); Assert.Equal("Read", json.GetProperty("container_name").GetString()); } @@ -7265,7 +7265,7 @@ public Demo.Status Read(object value) Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); - Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("member_read", json.GetProperty("reference_kind").GetString()); Assert.Equal(24, json.GetProperty("line").GetInt32()); Assert.Equal("Read", json.GetProperty("container_name").GetString()); } @@ -7344,7 +7344,7 @@ public Demo.Status ReadRecursive(object value) Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); - Assert.Equal("call", firstJson.GetProperty("reference_kind").GetString()); + Assert.Equal("member_read", firstJson.GetProperty("reference_kind").GetString()); Assert.Equal([83, 30], rowsByContainer["ReadMultiLineComment"]); Assert.Equal([64, 30], rowsByContainer["ReadGuard"]); Assert.Equal([30], rowsByContainer["ReadRecursive"]); @@ -7403,7 +7403,7 @@ public RealNs.Status Read(object[] values) Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); - Assert.Equal("call", first.RootElement.GetProperty("reference_kind").GetString()); + Assert.Equal("member_read", first.RootElement.GetProperty("reference_kind").GetString()); Assert.Equal([19, 22], lines); } finally @@ -7460,7 +7460,7 @@ public sealed class Uses Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); - Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("member_read", json.GetProperty("reference_kind").GetString()); Assert.Equal(25, json.GetProperty("line").GetInt32()); Assert.Equal("Read", json.GetProperty("container_name").GetString()); } @@ -7512,7 +7512,7 @@ public Demo.Status Read() Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); - Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("member_read", json.GetProperty("reference_kind").GetString()); Assert.Equal(19, json.GetProperty("line").GetInt32()); Assert.Equal("Read", json.GetProperty("container_name").GetString()); } @@ -7557,7 +7557,7 @@ void M() Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); - Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("member_read", json.GetProperty("reference_kind").GetString()); Assert.Equal(12, json.GetProperty("line").GetInt32()); Assert.Equal("M", json.GetProperty("container_name").GetString()); } @@ -7591,7 +7591,7 @@ class C { int N => 0; void M() { var x = global::N.Color.Red; } } Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); - Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("member_read", json.GetProperty("reference_kind").GetString()); Assert.Equal("function", json.GetProperty("container_kind").GetString()); Assert.Equal("M", json.GetProperty("container_name").GetString()); } @@ -7647,7 +7647,7 @@ Demo.Color M() Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); - Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("member_read", json.GetProperty("reference_kind").GetString()); Assert.Equal(23, json.GetProperty("line").GetInt32()); Assert.Equal("M", json.GetProperty("container_name").GetString()); } @@ -8485,7 +8485,7 @@ void Run(object value) } [ProductionRuntimeFact] - public void RunReferences_ExactJson_CSharpQualifiedConstantPatternSameFileEnumMemberSitesStaySuppressed() + public void RunReferences_ExactJson_CSharpQualifiedConstantPatternSameFileEnumMemberSitesUseMemberRead_Issue4894() { var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_qualified_constant_pattern_same_file_enum_member_sites_suppressed"); try @@ -8519,30 +8519,40 @@ void Run(object value) var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); - var (referencesExitCode, referencesStdout, referencesStderr) = RunReferencesInProcess("Red", dbPath, "csharp"); + var (referencesExitCode, referencesStdout, referencesStderr) = RunReferencesInProcess( + "Red", dbPath, "csharp", true, "--kind", "member_read"); using var referencesDocument = ParseJsonOutput(referencesStdout); var (countExitCode, countStdout, countStderr) = RunReferencesInProcess( - "Red", dbPath, "csharp", true, "--count"); + "Red", dbPath, "csharp", true, "--kind", "member_read", "--count"); using var countDocument = ParseJsonOutput(countStdout); var (callersExitCode, callersStdout, callersStderr) = RunCallersInProcess("Red", dbPath, "csharp"); using var callersDocument = ParseJsonOutput(callersStdout); + var (includedCallersExitCode, includedCallersStdout, includedCallersStderr) = RunCallersInProcess( + "Red", dbPath, "csharp", true, "--include-member-reads"); + using var includedCallersDocument = ParseJsonOutput(includedCallersStdout); Assert.Equal(CommandExitCodes.Success, indexExitCode); Assert.Equal(string.Empty, indexStderr); Assert.Equal(CommandExitCodes.Success, referencesExitCode); Assert.Equal(string.Empty, referencesStderr); - Assert.Equal(0, referencesDocument.RootElement.GetProperty("count").GetInt32()); + Assert.Equal("member_read", referencesDocument.RootElement.GetProperty("reference_kind").GetString()); + Assert.Equal("Run", referencesDocument.RootElement.GetProperty("container_name").GetString()); Assert.Equal(CommandExitCodes.Success, countExitCode); Assert.Equal(string.Empty, countStderr); - Assert.Equal(0, countDocument.RootElement.GetProperty("count").GetInt32()); + Assert.Equal(1, countDocument.RootElement.GetProperty("count").GetInt32()); Assert.Equal(CommandExitCodes.Success, callersExitCode); Assert.Equal(string.Empty, callersStderr); Assert.Equal(0, callersDocument.RootElement.GetProperty("count").GetInt32()); + + Assert.Equal(CommandExitCodes.Success, includedCallersExitCode); + Assert.Equal(string.Empty, includedCallersStderr); + Assert.Equal("member_read", includedCallersDocument.RootElement.GetProperty("reference_kind").GetString()); + Assert.Equal("Run", includedCallersDocument.RootElement.GetProperty("caller_name").GetString()); } finally { @@ -8653,7 +8663,7 @@ class Demo Assert.Equal(CommandExitCodes.Success, redExitCode); Assert.Equal(string.Empty, redStderr); - Assert.Equal("call", redJson.GetProperty("reference_kind").GetString()); + Assert.Equal("member_read", redJson.GetProperty("reference_kind").GetString()); } finally { diff --git a/tests/CodeIndex.Tests/ReferenceExtractorCSharpTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorCSharpTests.cs index 465d7f732..89446c16f 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorCSharpTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorCSharpTests.cs @@ -1479,12 +1479,12 @@ public void Use() var aRefs = references.Where(reference => reference.SymbolName == "A").OrderBy(reference => reference.Line).ToList(); Assert.Equal(2, aRefs.Count); - Assert.All(aRefs, reference => Assert.Equal("call", reference.ReferenceKind)); + Assert.All(aRefs, reference => Assert.Equal("member_read", reference.ReferenceKind)); Assert.Contains(aRefs, reference => reference.ContainerKind == "property" && reference.ContainerName == "Value"); Assert.Contains(aRefs, reference => reference.ContainerKind == "function" && reference.ContainerName == "GetValue"); var noneRef = Assert.Single(references.Where(reference => reference.SymbolName == "None")); - Assert.Equal("call", noneRef.ReferenceKind); + Assert.Equal("member_read", noneRef.ReferenceKind); Assert.Equal("function", noneRef.ContainerKind); Assert.Equal("Use", noneRef.ContainerName); } @@ -1525,7 +1525,7 @@ public class Foo } [Fact] - public void Extract_CsharpNonEnumQualifiedMemberAccess_DoesNotBecomeEnumMemberReference() + public void Extract_CsharpNonEnumQualifiedStaticMemberAccess_BecomesMemberRead_Issue4894() { const string content = """ namespace Demo; @@ -1552,11 +1552,14 @@ public int Read() var symbols = SymbolExtractor.Extract(1, "csharp", content); var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); - Assert.DoesNotContain(references, reference => reference.SymbolName == "A" && reference.ReferenceKind == "call"); + Assert.Contains(references, reference => + reference.SymbolName == "A" + && reference.ReferenceKind == "member_read" + && reference.ContainerName == "Read"); } [Fact] - public void Extract_CsharpQualifiedEnumMemberAccess_WithConflictingNonEnumType_DoesNotLeakAcrossNamespaces() + public void Extract_CsharpQualifiedMemberRead_WithConflictingNonEnumType_UsesNearestType_Issue4894() { const string content = """ namespace A; @@ -1585,7 +1588,10 @@ public int Read() var symbols = SymbolExtractor.Extract(1, "csharp", content); var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); - Assert.DoesNotContain(references, reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "call"); + var ready = Assert.Single(references.Where(reference => + reference.SymbolName == "Ready" + && reference.ReferenceKind == "member_read")); + Assert.Equal("Read", ready.ContainerName); } [Fact] @@ -1632,9 +1638,11 @@ public A.Status ReadEnum() var symbols = SymbolExtractor.Extract(1, "csharp", content); var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); - var ready = Assert.Single(references.Where(reference => reference.SymbolName == "Ready")); - Assert.Equal("call", ready.ReferenceKind); - Assert.Equal("ReadEnum", ready.ContainerName); + var ready = references.Where(reference => + reference.SymbolName == "Ready" + && reference.ReferenceKind == "member_read").ToList(); + Assert.Equal(2, ready.Count); + Assert.Equal(["ReadEnum", "ReadNonEnum"], ready.Select(reference => reference.ContainerName).Order().ToArray()); } [Fact] @@ -1715,10 +1723,9 @@ public Demo.Status Read() var symbols = SymbolExtractor.Extract(1, "csharp", content); var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); - var readyRefs = references.Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "call").ToList(); - var readyRef = Assert.Single(readyRefs); - Assert.Equal(35, readyRef.Line); - Assert.Equal("Read", readyRef.ContainerName); + var readyRefs = references.Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read").ToList(); + Assert.Equal([22, 35], readyRefs.Select(reference => reference.Line).Order().ToArray()); + Assert.All(readyRefs, readyRef => Assert.Equal("Read", readyRef.ContainerName)); } [Fact] @@ -1768,10 +1775,9 @@ public int Read() var symbols = SymbolExtractor.Extract(1, "csharp", content); var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); - var readyRefs = references.Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "call").ToList(); - var readyRef = Assert.Single(readyRefs); - Assert.Equal(22, readyRef.Line); - Assert.Equal("Read", readyRef.ContainerName); + var readyRefs = references.Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read").ToList(); + Assert.Equal([22, 35], readyRefs.Select(reference => reference.Line).Order().ToArray()); + Assert.All(readyRefs, readyRef => Assert.Equal("Read", readyRef.ContainerName)); } [Fact] @@ -1836,7 +1842,7 @@ public static int ReadStatic() var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); var readyRef = Assert.Single(references.Where(reference => - reference.SymbolName == "Ready" && reference.ReferenceKind == "call")); + reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read")); Assert.Equal("ReadInstanceFromStatic", readyRef.ContainerName); } @@ -1950,7 +1956,7 @@ public Demo.Status SplitValue var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); var readyRefs = references - .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "call") + .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read") .OrderBy(reference => reference.ContainerName) .ToArray(); @@ -1985,7 +1991,7 @@ void M() var symbols = SymbolExtractor.Extract(1, "csharp", content); var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); - var redRef = Assert.Single(references.Where(reference => reference.SymbolName == "Red" && reference.ReferenceKind == "call")); + var redRef = Assert.Single(references.Where(reference => reference.SymbolName == "Red" && reference.ReferenceKind == "member_read")); Assert.Equal(12, redRef.Line); Assert.Equal("M", redRef.ContainerName); } @@ -2024,7 +2030,7 @@ Demo.Color M() var symbols = SymbolExtractor.Extract(1, "csharp", content); var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); - var redRef = Assert.Single(references.Where(reference => reference.SymbolName == "Red" && reference.ReferenceKind == "call")); + var redRef = Assert.Single(references.Where(reference => reference.SymbolName == "Red" && reference.ReferenceKind == "member_read")); Assert.Equal(23, redRef.Line); Assert.Equal("M", redRef.ContainerName); } @@ -2054,7 +2060,9 @@ void M() var symbols = SymbolExtractor.Extract(1, "csharp", content); var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); - Assert.DoesNotContain(references, reference => reference.SymbolName == "Red" && reference.ReferenceKind == "call"); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "Red" + && reference.ReferenceKind is "call" or "member_read"); } [Fact] @@ -2140,7 +2148,7 @@ public Demo.Status ReadOutVar() var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); var readyRefs = references - .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "call") + .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read") .Select(reference => reference.ContainerName) .OrderBy(name => name) .ToArray(); @@ -2207,7 +2215,7 @@ public int ReadParameter(Holder Status) var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); var readyRefs = references - .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "call") + .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read") .Select(reference => reference.ContainerName) .OrderBy(name => name) .ToArray(); @@ -2266,7 +2274,7 @@ public Demo.Status ReadArgument(IEnumerable items) var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); var readyRefs = references - .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "call") + .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read") .Select(reference => reference.ContainerName) .OrderBy(name => name) .ToArray(); @@ -2328,7 +2336,7 @@ public IEnumerable ReadObject(IEnumerable items) var symbols = SymbolExtractor.Extract(1, "csharp", content); var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); - Assert.DoesNotContain(references, reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "call"); + Assert.DoesNotContain(references, reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read"); } [Fact] @@ -2373,7 +2381,7 @@ public Demo.Status ReadGroup(IEnumerable items) var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); var readyRefs = references - .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "call") + .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read") .Select(reference => reference.ContainerName) .OrderBy(name => name) .ToArray(); @@ -2429,7 +2437,7 @@ public IEnumerable ReadSpaced(IEnumerable items) var symbols = SymbolExtractor.Extract(1, "csharp", content); var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); - Assert.DoesNotContain(references, reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "call"); + Assert.DoesNotContain(references, reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read"); } [Fact] @@ -2466,7 +2474,7 @@ orderby select(items), items.Count() var symbols = SymbolExtractor.Extract(1, "csharp", content); var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); - Assert.DoesNotContain(references, reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "call"); + Assert.DoesNotContain(references, reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read"); } [Fact] @@ -2519,7 +2527,7 @@ public IEnumerable ReadBang(IEnumerable items) var symbols = SymbolExtractor.Extract(1, "csharp", content); var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); - Assert.DoesNotContain(references, reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "call"); + Assert.DoesNotContain(references, reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read"); } [Fact] @@ -2565,7 +2573,7 @@ public async Task> ReadCommentSeparated(IEnumerable ite var symbols = SymbolExtractor.Extract(1, "csharp", content); var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); - Assert.DoesNotContain(references, reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "call"); + Assert.DoesNotContain(references, reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read"); } [Fact] @@ -2617,7 +2625,7 @@ public Status ReadIncrement(IEnumerable items, int counter) var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); var readyRefs = references - .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "call") + .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read") .Select(reference => reference.ContainerName) .OrderBy(name => name) .ToArray(); @@ -2667,7 +2675,7 @@ public IEnumerable ReadGroup(IEnumerable items) var symbols = SymbolExtractor.Extract(1, "csharp", content); var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); - Assert.DoesNotContain(references, reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "call"); + Assert.DoesNotContain(references, reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read"); } [Fact] @@ -2706,7 +2714,7 @@ orderby items.Count() > var symbols = SymbolExtractor.Extract(1, "csharp", content); var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); - Assert.DoesNotContain(references, reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "call"); + Assert.DoesNotContain(references, reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read"); } [Fact] @@ -2745,7 +2753,7 @@ public IEnumerable Read(IEnumerable items) var symbols = SymbolExtractor.Extract(1, "csharp", content); var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); - Assert.DoesNotContain(references, reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "call"); + Assert.DoesNotContain(references, reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read"); } [Fact] @@ -2799,7 +2807,7 @@ public Demo.Status ReadArray(IEnumerable items, object value) var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); var readyRefs = references - .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "call") + .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read") .Select(reference => reference.ContainerName) .OrderBy(name => name) .ToArray(); @@ -2880,7 +2888,7 @@ public Demo.Status ReadAlias(IEnumerable items) var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); var readyRefs = references - .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "call") + .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read") .Select(reference => reference.ContainerName) .OrderBy(name => name) .ToArray(); @@ -2931,7 +2939,7 @@ public Demo.Status ReadCoalesce(IEnumerable items, int? left, int right) var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); var readyRefs = references - .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "call") + .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read") .Select(reference => reference.ContainerName) .OrderBy(name => name) .ToArray(); @@ -2973,7 +2981,7 @@ public Demo.Status Read(IEnumerable items) var symbols = SymbolExtractor.Extract(1, "csharp", content); var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); - var readyRefs = references.Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "call").ToList(); + var readyRefs = references.Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read").ToList(); Assert.Equal(2, readyRefs.Count); Assert.All(readyRefs, readyRef => Assert.Equal("Read", readyRef.ContainerName)); } @@ -3022,7 +3030,7 @@ public Demo.Status ReadLocal(IEnumerable items) var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); var readyRefs = references - .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "call") + .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read") .Select(reference => reference.ContainerName) .OrderBy(name => name) .ToArray(); @@ -3076,7 +3084,7 @@ public Demo.Status ReadGenericClose(IEnumerable items) var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); var readyRefs = references - .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "call") + .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read") .Select(reference => reference.ContainerName) .OrderBy(name => name) .ToArray(); @@ -3133,7 +3141,7 @@ public Demo.Status ReadTrailing(IEnumerable items, IEnumerable othe var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); var readyRef = Assert.Single(references.Where(reference => - reference.SymbolName == "Ready" && reference.ReferenceKind == "call")); + reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read")); Assert.Equal("ReadTrailing", readyRef.ContainerName); } @@ -3187,7 +3195,7 @@ public IEnumerable ReadTuple(IEnumerable items) var symbols = SymbolExtractor.Extract(1, "csharp", content); var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); - Assert.DoesNotContain(references, reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "call"); + Assert.DoesNotContain(references, reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read"); } [Fact] @@ -3230,7 +3238,7 @@ from Status in items var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); var readyRefs = references - .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "call") + .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read") .OrderBy(reference => reference.Line) .ToList(); @@ -3276,7 +3284,7 @@ public int ReadWithoutDesignation(IEnumerable items) var symbols = SymbolExtractor.Extract(1, "csharp", content); var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); - Assert.DoesNotContain(references, reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "call"); + Assert.DoesNotContain(references, reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read"); } [Fact] @@ -3331,7 +3339,7 @@ public Status ReadLater(IEnumerable items) var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); var readyRef = Assert.Single(references.Where(reference => - reference.SymbolName == "Ready" && reference.ReferenceKind == "call")); + reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read")); Assert.Equal("ReadLater", readyRef.ContainerName); Assert.Contains("Status.Ready", readyRef.Context, StringComparison.Ordinal); } @@ -3375,7 +3383,7 @@ public Status Read(IEnumerable items) Assert.Contains(references, reference => reference.SymbolName == "Ready" - && reference.ReferenceKind == "call" + && reference.ReferenceKind == "member_read" && reference.Line == 26 && reference.Context.Contains("Status.Ready", StringComparison.Ordinal)); } @@ -3417,7 +3425,7 @@ public object Read(IEnumerable items) var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); var readyRefs = references - .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "call") + .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read") .ToList(); Assert.Single(readyRefs); @@ -3463,7 +3471,7 @@ public object Read(IEnumerable items) var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); var readyRefs = references - .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "call") + .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read") .ToList(); Assert.Single(readyRefs); @@ -3521,7 +3529,7 @@ public Demo.Status ReadElse(IEnumerable items, bool flag) var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); var readyRefs = references - .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "call") + .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read") .Select(reference => reference.ContainerName) .OrderBy(name => name) .ToArray(); @@ -3570,7 +3578,7 @@ public void AfterBlock() var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); var readyRefs = references - .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "call") + .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read") .GroupBy(reference => reference.ContainerName) .ToDictionary(group => group.Key!, group => group.Count()); @@ -3635,7 +3643,7 @@ public Demo.Status ReadMultiLineWhile(object value) var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); var readyRefs = references - .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "call") + .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read") .Select(reference => reference.ContainerName) .OrderBy(name => name) .ToArray(); @@ -3697,7 +3705,7 @@ public RealNs.Status ReadStatic(object[] values) var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); var readyRefs = references - .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "call") + .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read") .GroupBy(reference => reference.ContainerName) .ToDictionary(group => group.Key!, group => group.Count()); @@ -3784,7 +3792,7 @@ public Demo.Status ReadRecursiveCase(object value) var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); var readyRefs = references - .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "call") + .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read") .Select(reference => reference.ContainerName) .OrderBy(name => name) .ToArray(); @@ -3860,7 +3868,7 @@ public Demo.Status ReadMultiLineComment(object value) var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); var readyRefs = references - .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "call") + .Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read") .GroupBy(reference => reference.ContainerName) .ToDictionary(group => group.Key!, group => group.OrderBy(reference => reference.Line).ToArray()); @@ -3907,7 +3915,7 @@ public sealed class Uses var symbols = SymbolExtractor.Extract(1, "csharp", content); var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); - var readyRefs = references.Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "call").ToList(); + var readyRefs = references.Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read").ToList(); var readyRef = Assert.Single(readyRefs); Assert.Equal(25, readyRef.Line); Assert.Equal("Read", readyRef.ContainerName); @@ -3943,7 +3951,7 @@ public Demo.Status Read() var symbols = SymbolExtractor.Extract(1, "csharp", content); var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); - var ready = Assert.Single(references.Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "call")); + var ready = Assert.Single(references.Where(reference => reference.SymbolName == "Ready" && reference.ReferenceKind == "member_read")); Assert.Equal(19, ready.Line); Assert.Equal("Read", ready.ContainerName); } diff --git a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs index f6f14bc37..f203ba380 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs @@ -13829,8 +13829,9 @@ bool Run(Color value) references.Count(r => r.SymbolName == "Color" && r.ReferenceKind == "type_reference" && r.ContainerName == "Run")); Assert.DoesNotContain(references, r => r.SymbolName == "Red" && r.ReferenceKind == "type_reference"); Assert.DoesNotContain(references, r => r.SymbolName == "Blue" && r.ReferenceKind == "type_reference"); - Assert.Contains(references, r => r.SymbolName == "Red" && r.ReferenceKind == "call"); - Assert.Contains(references, r => r.SymbolName == "Blue" && r.ReferenceKind == "call"); + Assert.Contains(references, r => r.SymbolName == "Red" && r.ReferenceKind == "member_read"); + Assert.Contains(references, r => r.SymbolName == "Blue" && r.ReferenceKind == "member_read"); + Assert.DoesNotContain(references, r => (r.SymbolName is "Red" or "Blue") && r.ReferenceKind == "call"); } [Fact] @@ -13881,6 +13882,8 @@ public void Run(object value, Color color) Assert.DoesNotContain(references, r => r.SymbolName == "Red" && r.ReferenceKind == "type_reference"); Assert.DoesNotContain(references, r => r.SymbolName == "Blue" && r.ReferenceKind == "call"); Assert.DoesNotContain(references, r => r.SymbolName == "Blue" && r.ReferenceKind == "type_reference"); + Assert.Equal(2, references.Count(r => r.SymbolName == "Red" && r.ReferenceKind == "member_read")); + Assert.Equal(2, references.Count(r => r.SymbolName == "Blue" && r.ReferenceKind == "member_read")); Assert.Equal( 0, @@ -13922,7 +13925,39 @@ void Run(int value) Assert.DoesNotContain(references, r => r.SymbolName == "ErrorCodes" && r.ReferenceKind == "type_reference"); Assert.DoesNotContain(references, r => r.SymbolName == "NotFound" && r.ReferenceKind == "type_reference"); Assert.DoesNotContain(references, r => r.SymbolName == "Forbidden" && r.ReferenceKind == "type_reference"); - Assert.DoesNotContain(references, r => r.ContainerName == "Run"); + Assert.Contains(references, r => r.SymbolName == "NotFound" && r.ReferenceKind == "member_read" && r.ContainerName == "Run"); + Assert.Contains(references, r => r.SymbolName == "Forbidden" && r.ReferenceKind == "member_read" && r.ContainerName == "Run"); + Assert.DoesNotContain(references, r => r.ContainerName == "Run" && r.ReferenceKind == "call"); + } + + [Fact] + public void Extract_CsharpQualifiedValueReads_UseMemberReadWhileInvocationsRemainCalls_Issue4894() + { + const string content = """ + namespace Probe; + + static class Values + { + public const int Constant = 1; + public static readonly int Readonly = 2; + public static int Property => 3; + public static int Method() => 4; + } + + class Demo + { + int Run() => Values.Constant + Values.Readonly + Values.Property + Values.Method(); + } + """; + + var symbols = SymbolExtractor.Extract(1, "csharp", content); + var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); + + Assert.Contains(references, r => r.SymbolName == "Constant" && r.ReferenceKind == "member_read" && r.ContainerName == "Run"); + Assert.Contains(references, r => r.SymbolName == "Readonly" && r.ReferenceKind == "member_read" && r.ContainerName == "Run"); + Assert.Contains(references, r => r.SymbolName == "Property" && r.ReferenceKind == "member_read" && r.ContainerName == "Run"); + Assert.Contains(references, r => r.SymbolName == "Method" && r.ReferenceKind == "call" && r.ContainerName == "Run"); + Assert.DoesNotContain(references, r => (r.SymbolName is "Constant" or "Readonly" or "Property") && r.ReferenceKind == "call"); } [Fact] From 02d08f930c8604f622e561aa15d939eefa6e5fd4 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 31 Jul 2026 01:16:55 +0900 Subject: [PATCH 2/5] Fix cross-file member-read resolution (#4894) --- TESTING_GUIDE.md | 4 +- changelog.d/unreleased/4894.added.md | 8 +- ...ommandRunner.FullScan.ExtractionWorkers.cs | 3 +- ...xCommandRunner.FullScan.FilePersistence.cs | 4 +- ...dexCommandRunner.Update.FilePersistence.cs | 4 +- .../Database/DbWriter.CSharpContracts.cs | 68 ++++++++++++++++ .../Indexer/CSharpStaticInterfacePrepass.cs | 29 ++++++- ...arpReferenceExtractor.QualifiedPatterns.cs | 8 ++ .../CSharpReferenceExtractor.Support.cs | 29 ++++--- .../References/ReferenceExtractionContext.cs | 1 + .../ReferenceExtractor.CoreExtraction.cs | 52 ++++++++++-- .../ReferenceExtractor.PrimaryConstructors.cs | 7 ++ .../Indexer/References/ReferenceExtractor.cs | 13 ++- .../Indexer/Scanning/FileContentLoader.cs | 2 +- .../Mcp/McpToolHandlers.Indexing.Execution.cs | 3 +- tests/CodeIndex.Tests/DatabaseTests.cs | 81 +++++++++++++++++++ tests/CodeIndex.Tests/FileIndexerTests.cs | 4 +- .../IndexCommandRunnerFullScanTests.cs | 51 ++++++++++-- .../ReferenceExtractorTests.cs | 69 ++++++++++++++++ 19 files changed, 402 insertions(+), 38 deletions(-) diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md index 4e835363b..1752ca2c5 100644 --- a/TESTING_GUIDE.md +++ b/TESTING_GUIDE.md @@ -102,7 +102,7 @@ Use `docs/test-doc-maintenance-plan.md` before moving oversized suites or adding `ReferenceExtractorTests.ExtractSymbolsAndReferences(...)` owns the common symbol-then-reference extraction setup for tests that need both lists; use it instead of repeating the two extractor calls when the fixture does not need a specialized path or workspace symbol setup, and discard the symbol tuple element with `_` instead of keeping an unused `symbols` local when the test only asserts references. C# named-argument coverage keeps syntax discrimination in `ReferenceExtractorCSharpTests` and persisted query/dependency behavior in `QueryCommandRunnerIssue4833Tests`; preserve positional and reordered arguments, attributes, constructors, nested and multiline calls, expression-side, named-`out`, explicitly typed lambda/anonymous-method, and typed LINQ range-variable type references, property-subpattern types, and negative controls for aliases, labels, nullable types, and ternaries (#4833; regressions #106 and #122). C# qualified common-call coverage keeps static BCL, instance, LINQ extension, alias-qualified, current-instance, and unresolved-receiver cases together. Assert that extraction retains every row, default bare-name references/callers/callees and hotspot counts retain resolved evidence while excluding unresolved noise, the completeness option restores that noise deterministically, and dependency queries remain identity-scoped (#4867). - C# member-read coverage keeps enum and const patterns, ordinary qualified constants, static readonly fields, static properties, and a true method invocation together. Assert `member_read` extraction, default callers/callees/impact exclusion, explicit compatibility inclusion, and legacy `call` readability in coordinated extractor and `DbReaderTests` fixtures (#4894). + C# member-read coverage keeps enum and const patterns, ordinary qualified constants, static readonly fields, static properties, cross-file targets, callable-name collisions, and a true method invocation together. Assert `member_read` extraction without a duplicate `call`, default callers/callees/impact exclusion, explicit compatibility inclusion, and legacy `call` readability in coordinated extractor, full-scan, and `DbReaderTests` fixtures (#4894). Crystal, Groovy, Tcl, Prolog, and `ambiguous_pl` graph fixtures keep imports, parenthesized calls, conservative same-file command/predicate calls, caller containers, and keyword false-positive controls independently diagnosable; capability tests must keep their advertised symbol/reference/graph flags aligned with those extractor fixtures (#4746). Database status tests must also pin stale and current extractor-version stamps for those languages so pre-graph rows cannot report authoritative graph readiness. Database page-attribution coverage keeps empty/schema-only, WAL-visible overflow, database-truncating WAL, and a connection-pinned read snapshot after a newer WAL commit, cancellation of both `dbstat` aggregation and WAL validation, the bounded large-page-count rejection, reconciliation, corrupt-file rejection, main/WAL/SHM separation, the 20-object and 128-character support-safe output bounds, explicit unavailable/not-requested values, and unchanged `total_changes()` / `PRAGMA query_only` in coordinated `DbReaderTests` fixtures. @@ -1045,7 +1045,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests" `ReferenceExtractorTests.ExtractSymbolsAndReferences(...)` は symbol 抽出から reference 抽出までの共通 setup を所有します。fixture が特殊な path や workspace symbol setup を必要としない場合は 2 つの extractor 呼び出しを繰り返さずこの helper を使い、reference だけを検証するテストでは未使用の `symbols` local を残さず symbol 側を `_` で捨ててください。 C# named-argument の coverage は構文の判別を `ReferenceExtractorCSharpTests`、永続化後の query / dependency 動作を `QueryCommandRunnerIssue4833Tests` に保持します。positional argument との混在、並べ替え、attribute、constructor、nested / multiline call、value 式側、named `out` declaration、明示型 lambda / anonymous method、および型付き LINQ range variable の type reference、property subpattern の型、ならびに alias、label、nullable type、ternary に対する負例を維持してください(#4833、回帰 #106 / #122)。 C# の修飾付き一般名 call の coverage は、static BCL、instance、LINQ extension、alias 修飾、current instance、未解決 receiver の各 case を同じ fixture に維持します。extraction が全 row を保持すること、無修飾名による references / callers / callees と hotspot count の既定動作が解決済み evidence を維持しつつ未解決 noise を除外すること、completeness option がその noise を決定的に復元すること、dependency query が identity scope のままであることを検証してください(#4867)。 - C# member-read coverage は enum / const pattern、通常の修飾付き定数、static readonly field、static property、真の method invocation を同じ fixture に維持します。連携する extractor / `DbReaderTests` fixture で `member_read` 抽出、既定 callers / callees / impact からの除外、明示 compatibility option による復元、legacy `call` row の読み取りを検証してください(#4894)。 + C# member-read coverage は enum / const pattern、通常の修飾付き定数、static readonly field、static property、cross-file target、callable 名の衝突、真の method invocation を同じ fixture に維持します。連携する extractor / full-scan / `DbReaderTests` fixture で、重複 `call` を伴わない `member_read` 抽出、既定 callers / callees / impact からの除外、明示 compatibility option による復元、legacy `call` row の読み取りを検証してください(#4894)。 Crystal、Groovy、Tcl、Prolog、`ambiguous_pl` の graph fixture では、import、括弧付き call、同一ファイルに限定した保守的な command / predicate call、caller container、keyword の false-positive control を個別に診断可能な状態で維持し、capability test の symbol / reference / graph 広告を extractor fixture と一致させてください(#4746)。 さらに database status test でこれらの言語の古い extractor-version stamp と現行 stamp を固定し、graph 対応前の row が authoritative な graph readiness を報告できないことを検証してください。 database page-attribution coverage では、empty / schema-only、WAL から可視な overflow、database を縮小する WAL、後続 WAL commit 後も connection に固定された read snapshot の各 case、`dbstat` 集約と WAL 検証のキャンセル、large page count の上限付き拒否、再照合、破損 file の拒否、main/WAL/SHM の分離、20 object / 128文字の support-safe 出力上限、明示的な unavailable / not-requested 値、`total_changes()` / `PRAGMA query_only` が不変であることを、連携した `DbReaderTests` fixture で維持してください。 diff --git a/changelog.d/unreleased/4894.added.md b/changelog.d/unreleased/4894.added.md index 2abdec024..f074704b7 100644 --- a/changelog.d/unreleased/4894.added.md +++ b/changelog.d/unreleased/4894.added.md @@ -4,11 +4,15 @@ issues: - 4894 affected: - src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.QualifiedPatterns.cs + - src/CodeIndex/Indexer/References/ReferenceExtractor.CoreExtraction.cs + - src/CodeIndex/Indexer/CSharpStaticInterfacePrepass.cs + - src/CodeIndex/Database/DbWriter.CSharpContracts.cs - src/CodeIndex/Database/DbReader.GraphQueries.cs - src/CodeIndex/Cli/CliFlagSchema.cs - src/CodeIndex/Mcp/McpToolCatalog.cs - src/CodeIndex/Models/SymbolKindCatalog.cs - tests/CodeIndex.Tests/ReferenceExtractorTests.cs + - tests/CodeIndex.Tests/IndexCommandRunnerFullScanTests.cs - tests/CodeIndex.Tests/DbReaderTests.cs - USER_GUIDE.md - DEVELOPER_GUIDE.md @@ -17,8 +21,8 @@ affected: ## English -- **Added canonical `member_read` references for non-invoking C# member/value reads (#4894)** — new indexes distinguish qualified enum constants, constants, static readonly fields, static properties, and constant-pattern reads from true `call` edges. Callers, callees, and impact exclude these reads by default and expose explicit CLI `--include-member-reads` / MCP `includeMemberReads` compatibility controls; legacy indexes remain readable with their historical `call` rows. +- **Added canonical `member_read` references for non-invoking C# member/value reads (#4894)** — new indexes distinguish qualified enum constants, constants, static readonly fields, static properties, and constant-pattern reads from true `call` edges, including cross-file targets and callable-name collisions. Callers, callees, and impact exclude these reads by default and expose explicit CLI `--include-member-reads` / MCP `includeMemberReads` compatibility controls; legacy indexes remain readable with their historical `call` rows. ## 日本語 -- **呼び出しを伴わない C# member / value read 向けに canonical `member_read` reference を追加しました (#4894)** — 新しい index は修飾付き enum 定数、定数、static readonly field、static property、constant-pattern read を真の `call` edge と区別します。callers、callees、impact は既定でこれらを除外し、CLI `--include-member-reads` / MCP `includeMemberReads` の明示 compatibility control で復元できます。legacy index は従来の `call` row のまま引き続き読み取れます。 +- **呼び出しを伴わない C# member / value read 向けに canonical `member_read` reference を追加しました (#4894)** — 新しい index は cross-file target や callable 名の衝突を含む修飾付き enum 定数、定数、static readonly field、static property、constant-pattern read を真の `call` edge と区別します。callers、callees、impact は既定でこれらを除外し、CLI `--include-member-reads` / MCP `includeMemberReads` の明示 compatibility control で復元できます。legacy index は従来の `call` row のまま引き続き読み取れます。 diff --git a/src/CodeIndex/Cli/IndexCommandRunner.FullScan.ExtractionWorkers.cs b/src/CodeIndex/Cli/IndexCommandRunner.FullScan.ExtractionWorkers.cs index d47024b60..ec04a4c1c 100644 --- a/src/CodeIndex/Cli/IndexCommandRunner.FullScan.ExtractionWorkers.cs +++ b/src/CodeIndex/Cli/IndexCommandRunner.FullScan.ExtractionWorkers.cs @@ -201,7 +201,8 @@ private static Task[] StartFullScanExtractionWorkers( maxReferenceCount: options.MaxReferencesPerFile + 1, conflictMarkerLine: loaded.ConflictMarkerLine, workspaceRoot: projectRoot, - csharpStaticInterfaceMemberLookups: csharpWorkspace.StaticInterfaceMemberLookups); + csharpStaticInterfaceMemberLookups: csharpWorkspace.StaticInterfaceMemberLookups, + csharpQualifiedPatternLookups: csharpWorkspace.QualifiedPatternLookups); references = referenceExtraction.References; referenceRegexTimeoutIssue = BuildRegexTimeoutIssue(record.Path, regexTimeouts); } diff --git a/src/CodeIndex/Cli/IndexCommandRunner.FullScan.FilePersistence.cs b/src/CodeIndex/Cli/IndexCommandRunner.FullScan.FilePersistence.cs index 058383c00..1c2ac61b2 100644 --- a/src/CodeIndex/Cli/IndexCommandRunner.FullScan.FilePersistence.cs +++ b/src/CodeIndex/Cli/IndexCommandRunner.FullScan.FilePersistence.cs @@ -247,7 +247,9 @@ private static FullScanFilePersistenceResult PersistFullScanFile( conflictMarkerLine: item.ConflictMarkerLine, workspaceRoot: context.ProjectRoot, csharpStaticInterfaceMemberLookups: - context.CSharpWorkspace.StaticInterfaceMemberLookups); + context.CSharpWorkspace.StaticInterfaceMemberLookups, + csharpQualifiedPatternLookups: + context.CSharpWorkspace.QualifiedPatternLookups); references = referenceExtraction.References; regexTimeoutIssue = BuildRegexTimeoutIssue(record.Path, regexTimeouts); } diff --git a/src/CodeIndex/Cli/IndexCommandRunner.Update.FilePersistence.cs b/src/CodeIndex/Cli/IndexCommandRunner.Update.FilePersistence.cs index f84717c26..65aa6bf4b 100644 --- a/src/CodeIndex/Cli/IndexCommandRunner.Update.FilePersistence.cs +++ b/src/CodeIndex/Cli/IndexCommandRunner.Update.FilePersistence.cs @@ -252,7 +252,9 @@ private static UpdateFilePersistenceResult PersistUpdateFile( conflictMarkerLine: loaded.ConflictMarkerLine, workspaceRoot: context.ProjectRoot, csharpStaticInterfaceMemberLookups: - context.CSharpWorkspace.StaticInterfaceMemberLookups); + context.CSharpWorkspace.StaticInterfaceMemberLookups, + csharpQualifiedPatternLookups: + context.CSharpWorkspace.QualifiedPatternLookups); references = referenceExtraction.References; referenceRegexTimeoutIssue = BuildRegexTimeoutIssue(record.Path, regexTimeouts); diff --git a/src/CodeIndex/Database/DbWriter.CSharpContracts.cs b/src/CodeIndex/Database/DbWriter.CSharpContracts.cs index 142f1154c..0d9c94300 100644 --- a/src/CodeIndex/Database/DbWriter.CSharpContracts.cs +++ b/src/CodeIndex/Database/DbWriter.CSharpContracts.cs @@ -67,6 +67,21 @@ CROSS JOIN symbols s INDEXED BY idx_symbols_file_kind WHERE f.lang = 'csharp' AND " + CSharpStaticInterfaceContractMemberPredicateSql; + internal const string CSharpMemberReadTargetWorkspaceSql = @" + SELECT " + CSharpContractWorkspaceProjectionSql + @" + FROM files f INDEXED BY idx_files_lang + CROSS JOIN symbols s INDEXED BY idx_symbols_file_kind + ON s.file_id = f.id + WHERE f.lang = 'csharp' + AND ( + (s.kind = 'enum' AND s.container_kind = 'enum') + OR ( + s.kind IN ('field', 'property') + AND s.container_kind IN ('class', 'struct') + AND (s.signature LIKE '%static%' OR s.signature LIKE '%const%') + ) + )"; + internal bool? GetCSharpStaticInterfaceSourceEvidence() { var raw = GetMetaString(DbContext.CSharpStaticInterfaceSourceEvidenceMetaKey); @@ -226,6 +241,12 @@ internal List LoadCSharpStaticInterfaceContractSymbols( cancellationToken); } + AppendCSharpMemberReadTargetSymbols( + symbols, + excludedPaths, + excludedExistingFileIds, + isExistingSymbolPathExcluded, + cancellationToken); cancellationToken.ThrowIfCancellationRequested(); CSharpContractWorkspaceReadStatsForTesting?.Invoke( new CSharpContractWorkspaceReadStats( @@ -237,6 +258,53 @@ internal List LoadCSharpStaticInterfaceContractSymbols( return symbols; } + private void AppendCSharpMemberReadTargetSymbols( + List symbols, + IReadOnlySet? excludedPaths, + IReadOnlyList? excludedExistingFileIds, + Func? isExistingSymbolPathExcluded, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var cmd = RentCommand(CSharpMemberReadTargetWorkspaceSql, static _ => { }); + try + { + using var cancellationRegistration = RegisterSqliteInterrupt(cancellationToken); + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + cancellationToken.ThrowIfCancellationRequested(); + var fileId = reader.GetInt64(1); + if (FilePurgePlan.ContainsSortedFileId(excludedExistingFileIds, fileId)) + continue; + + var path = reader.GetString(0); + if (excludedPaths?.Contains(path) == true + || isExistingSymbolPathExcluded?.Invoke(path) == true) + { + continue; + } + + var symbol = ReadCSharpContractWorkspaceSymbol(reader); + if (ReferenceExtractor.IsCSharpQualifiedMemberReadTargetSymbol(symbol)) + symbols.Add(symbol); + } + + cancellationToken.ThrowIfCancellationRequested(); + } + catch (SqliteException ex) when (IsSqliteInterruptCancellation(ex, cancellationToken)) + { + throw new OperationCanceledException( + "C# member-read target workspace read was interrupted.", + ex, + cancellationToken); + } + finally + { + ReleaseCommand(cmd); + } + } + internal static string BuildCSharpStaticInterfaceDeclarationWorkspaceSql(int batchCount) { SqliteDynamicSql.EnsureParameterBudget( diff --git a/src/CodeIndex/Indexer/CSharpStaticInterfacePrepass.cs b/src/CodeIndex/Indexer/CSharpStaticInterfacePrepass.cs index 3d9aaef63..e5b3e4ac3 100644 --- a/src/CodeIndex/Indexer/CSharpStaticInterfacePrepass.cs +++ b/src/CodeIndex/Indexer/CSharpStaticInterfacePrepass.cs @@ -98,7 +98,7 @@ internal static CSharpStaticInterfaceWorkspaceSymbols BuildWorkspaceSymbols( target.FilePath, target.RelativePath, cancellationToken); - if (content is not null && MayContainCSharpStaticInterfaceContract(content)) + if (content is not null && MayContainCSharpWorkspaceReferenceTargets(content)) extractedByCandidate[candidateIndex] = SymbolExtractor.Extract( 0, "csharp", @@ -164,7 +164,8 @@ internal static CSharpStaticInterfaceWorkspaceSymbols BuildWorkspaceSymbols( ReferenceExtractor.BuildCSharpStaticInterfaceMemberLookups(symbols), hasSourceStaticInterfaceContracts, sourceEvidenceComplete != 0, - incompletePaths); + incompletePaths, + ReferenceExtractor.BuildCSharpQualifiedPatternLookups(symbols)); } internal static CSharpStaticInterfaceWorkspaceSymbols BuildWorkspaceSymbols( @@ -222,6 +223,14 @@ internal static bool MayContainCSharpStaticInterfaceContract(string content) return CSharpCodeMayContainStaticInterfaceContract(contentSpan); } + internal static bool MayContainCSharpWorkspaceReferenceTargets(string content) + { + var contentSpan = content.AsSpan(); + return ContainsCSharpWord(contentSpan, "enum") + || ContainsCSharpWord(contentSpan, "const") + || ContainsCSharpWord(contentSpan, "static"); + } + internal static bool RawBytesMayContainCSharpStaticInterfaceContract(byte[] bytes) { var span = bytes.AsSpan(); @@ -257,9 +266,12 @@ internal sealed class RawByteContractProbe private bool _hasStatic; private bool _hasAbstract; private bool _hasVirtual; + private bool _hasEnum; + private bool _hasConst; private bool _mayContainUtf16; internal bool MayContainContractCandidate => _hasInterface && _hasStatic && (_hasAbstract || _hasVirtual); + internal bool MayContainWorkspaceCandidate => _hasStatic || _hasEnum || _hasConst; internal bool AppendAndCheck(ReadOnlySpan bytes) { @@ -267,6 +279,12 @@ internal bool AppendAndCheck(ReadOnlySpan bytes) return MayContainContractCandidate; } + internal bool AppendAndCheckWorkspaceCandidate(ReadOnlySpan bytes) + { + Append(bytes); + return MayContainWorkspaceCandidate; + } + internal void Append(ReadOnlySpan bytes) { if (bytes.Length == 0 || MayContainContractCandidate) @@ -298,6 +316,10 @@ private void Scan(ReadOnlySpan bytes) _hasInterface = ContainsAsciiTokenInCommonEncodings(bytes, CSharpInterfaceKeywordBytes, _mayContainUtf16); if (!_hasStatic) _hasStatic = ContainsAsciiTokenInCommonEncodings(bytes, CSharpStaticKeywordBytes, _mayContainUtf16); + if (!_hasEnum) + _hasEnum = ContainsAsciiTokenInCommonEncodings(bytes, "enum"u8, _mayContainUtf16); + if (!_hasConst) + _hasConst = ContainsAsciiTokenInCommonEncodings(bytes, "const"u8, _mayContainUtf16); if (!_hasAbstract && !_hasVirtual) { _hasAbstract = ContainsAsciiTokenInCommonEncodings(bytes, CSharpAbstractKeywordBytes, _mayContainUtf16); @@ -1013,4 +1035,5 @@ internal sealed record CSharpStaticInterfaceWorkspaceSymbols( ReferenceExtractor.CSharpStaticInterfaceMemberLookups? StaticInterfaceMemberLookups = null, bool HasSourceStaticInterfaceContracts = false, bool SourceContractEvidenceComplete = true, - IReadOnlyList? IncompleteSourcePaths = null); + IReadOnlyList? IncompleteSourcePaths = null, + ReferenceExtractor.CSharpQualifiedPatternLookups? QualifiedPatternLookups = null); diff --git a/src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.QualifiedPatterns.cs b/src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.QualifiedPatterns.cs index 5ac1871b1..03f1a883b 100644 --- a/src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.QualifiedPatterns.cs +++ b/src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.QualifiedPatterns.cs @@ -66,6 +66,8 @@ internal static void EmitCSharpQualifiedEnumMemberReferences( var nextTokenIndex = SkipWhitespace(preparedLine, member.End); if (nextTokenIndex < preparedLine.Length && preparedLine[nextTokenIndex] == '(') continue; + if (IsCSharpSimpleAssignmentTarget(preparedLine, nextTokenIndex)) + continue; var insideCSharpAttributeRange = csharpAttrRangesOnLine != null && IsInsideCSharpAttributeRange(csharpAttrRangesOnLine, member.Start); @@ -84,6 +86,12 @@ internal static void EmitCSharpQualifiedEnumMemberReferences( } } + private static bool IsCSharpSimpleAssignmentTarget(string preparedLine, int nextTokenIndex) + => nextTokenIndex < preparedLine.Length + && preparedLine[nextTokenIndex] == '=' + && (nextTokenIndex + 1 >= preparedLine.Length + || preparedLine[nextTokenIndex + 1] is not ('=' or '>')); + private static bool IsCSharpQualifiedConstantPatternReferenceSite( string preparedLine, (IReadOnlyList<(int Start, int End)> Segments, int NextIndex, bool LastSeparatorWasDot, bool HasLeadingGlobalQualifier) parsed) diff --git a/src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.Support.cs b/src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.Support.cs index 9feae13de..08d411093 100644 --- a/src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.Support.cs +++ b/src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.Support.cs @@ -24,6 +24,10 @@ internal sealed record CSharpFunctionValueReceiverNameRecord(string Name, int Sc new Dictionary>(StringComparer.Ordinal); private static readonly IReadOnlyDictionary> EmptyCSharpQualifiedPatternLookup = new Dictionary>(StringComparer.Ordinal); + internal sealed record CSharpQualifiedPatternLookups( + IReadOnlyDictionary> EnumMemberLookup, + IReadOnlyDictionary> ConstantPatternMemberLookup, + IReadOnlyDictionary> TypePatternLookup); private static ( IReadOnlyList Aliases, @@ -519,17 +523,17 @@ private static void AddCSharpContainingTypeValueReceiverName( names.InstanceNames.Add(symbol.Name); } - private static ( - IReadOnlyDictionary> EnumMemberLookup, - IReadOnlyDictionary> ConstantPatternMemberLookup, - IReadOnlyDictionary> TypePatternLookup) BuildCSharpQualifiedPatternLookups( - string language, + internal static CSharpQualifiedPatternLookups BuildCSharpQualifiedPatternLookups( + IReadOnlyList symbols) + { + var typeNameSets = BuildCSharpTypeNameSets("csharp", symbols); + return BuildCSharpQualifiedPatternLookups(symbols, typeNameSets.NonEnumTypeNames); + } + + private static CSharpQualifiedPatternLookups BuildCSharpQualifiedPatternLookups( IReadOnlyList symbols, IReadOnlySet conflictingNonEnumTypeNames) { - if (language != "csharp") - return (EmptyCSharpQualifiedEnumMemberLookup, EmptyCSharpQualifiedPatternLookup, EmptyCSharpQualifiedPatternLookup); - Dictionary>? enumMemberLookup = null; Dictionary>? constantPatternMemberLookup = null; Dictionary>? typePatternLookup = null; @@ -597,7 +601,7 @@ private static ( } } - return ( + return new CSharpQualifiedPatternLookups( enumMemberLookup ?? EmptyCSharpQualifiedEnumMemberLookup, constantPatternMemberLookup ?? EmptyCSharpQualifiedPatternLookup, typePatternLookup ?? EmptyCSharpQualifiedPatternLookup); @@ -655,6 +659,13 @@ private static void AddCSharpQualifiedPatternTarget( targets.Add((containerName, qualifiedContainerName, allowShortNameFallback)); } + internal static bool IsCSharpQualifiedMemberReadTargetSymbol(SymbolRecord symbol) + => symbol.Kind == "enum" && symbol.ContainerKind == "enum" + || IsCSharpConstMemberSymbol(symbol) + || symbol.ContainerKind is "class" or "struct" + && symbol.Kind is "field" or "property" + && IsStaticCSharpSymbol(symbol); + private static bool IsCSharpConstMemberSymbol(SymbolRecord symbol) { if (symbol.ContainerKind is not ("class" or "struct")) diff --git a/src/CodeIndex/Indexer/References/ReferenceExtractionContext.cs b/src/CodeIndex/Indexer/References/ReferenceExtractionContext.cs index 0b0849f6e..f55f9fce8 100644 --- a/src/CodeIndex/Indexer/References/ReferenceExtractionContext.cs +++ b/src/CodeIndex/Indexer/References/ReferenceExtractionContext.cs @@ -22,4 +22,5 @@ public sealed record ReferenceExtractionContext( int? ConflictMarkerLine = null) { internal ReferenceExtractor.CSharpStaticInterfaceMemberLookups? CSharpStaticInterfaceMemberLookups { get; init; } + internal ReferenceExtractor.CSharpQualifiedPatternLookups? CSharpQualifiedPatternLookups { get; init; } } diff --git a/src/CodeIndex/Indexer/References/ReferenceExtractor.CoreExtraction.cs b/src/CodeIndex/Indexer/References/ReferenceExtractor.CoreExtraction.cs index edf3bfed8..7639cf846 100644 --- a/src/CodeIndex/Indexer/References/ReferenceExtractor.CoreExtraction.cs +++ b/src/CodeIndex/Indexer/References/ReferenceExtractor.CoreExtraction.cs @@ -143,16 +143,26 @@ internal static List ExtractCore( KnownTypeNames: EmptyCSharpStringSet, NonEnumTypeNames: EmptyCSharpStringSet); var csharpKnownTypeNames = csharpTypeNameSets.KnownTypeNames; - var csharpQualifiedPatternLookups = language == "csharp" + var localCSharpQualifiedPatternLookups = language == "csharp" ? BuildCSharpQualifiedPatternLookups( - language, symbols, csharpTypeNameSets.NonEnumTypeNames) - : ( - EnumMemberLookup: EmptyCSharpQualifiedEnumMemberLookup, - ConstantPatternMemberLookup: - EmptyCSharpQualifiedPatternLookup, - TypePatternLookup: EmptyCSharpQualifiedPatternLookup); + : null; + var workspaceCSharpQualifiedPatternLookups = language == "csharp" + ? request.CSharpQualifiedPatternLookups + ?? (workspaceSymbols is { Count: > 0 } + ? BuildCSharpQualifiedPatternLookups(workspaceSymbols) + : localCSharpQualifiedPatternLookups) + : null; + var csharpQualifiedPatternLookups = language == "csharp" + ? new CSharpQualifiedPatternLookups( + workspaceCSharpQualifiedPatternLookups!.EnumMemberLookup, + workspaceCSharpQualifiedPatternLookups.ConstantPatternMemberLookup, + localCSharpQualifiedPatternLookups!.TypePatternLookup) + : new CSharpQualifiedPatternLookups( + EmptyCSharpQualifiedEnumMemberLookup, + EmptyCSharpQualifiedPatternLookup, + EmptyCSharpQualifiedPatternLookup); var csharpQualifiedEnumMemberLookup = csharpQualifiedPatternLookups.EnumMemberLookup; var csharpQualifiedConstantPatternMemberLookup = @@ -350,6 +360,7 @@ internal static List ExtractCore( preparedLines, references, lookups); + RemoveCSharpCallsDuplicatedByMemberReads(references); } lookups.ApplyCSharpUsingAliasReferenceNames(references); @@ -363,6 +374,33 @@ internal static List ExtractCore( return references; } + private static void RemoveCSharpCallsDuplicatedByMemberReads( + List references) + { + var memberReadSites = references + .Where(reference => reference.ReferenceKind == "member_read") + .Select(reference => ( + reference.FileId, + reference.Line, + reference.Column, + reference.SymbolName, + reference.ContainerKind, + reference.ContainerName)) + .ToHashSet(); + if (memberReadSites.Count == 0) + return; + + references.RemoveAll(reference => + reference.ReferenceKind == "call" + && memberReadSites.Contains(( + reference.FileId, + reference.Line, + reference.Column, + reference.SymbolName, + reference.ContainerKind, + reference.ContainerName))); + } + private static void RewriteCSharpPropertyReceiverReferences( IReadOnlyList preparedLines, List references, diff --git a/src/CodeIndex/Indexer/References/ReferenceExtractor.PrimaryConstructors.cs b/src/CodeIndex/Indexer/References/ReferenceExtractor.PrimaryConstructors.cs index 0cf75a6a1..e488a16c2 100644 --- a/src/CodeIndex/Indexer/References/ReferenceExtractor.PrimaryConstructors.cs +++ b/src/CodeIndex/Indexer/References/ReferenceExtractor.PrimaryConstructors.cs @@ -96,6 +96,13 @@ private static void EmitMethodGroupReferences( var name = language == "csharp" ? NormalizeCSharpIdentifier(rawName) : rawName; if (!callableDefinitionNames.Contains(name)) continue; + if (language == "csharp" + && IsCSharpSimpleAssignmentTarget( + preparedLine, + SkipWhitespace(preparedLine, nameGroup.Index + nameGroup.Length))) + { + continue; + } var container = resolveContainerForColumn(nameGroup.Index); AddChainReference( diff --git a/src/CodeIndex/Indexer/References/ReferenceExtractor.cs b/src/CodeIndex/Indexer/References/ReferenceExtractor.cs index 267c8e4f6..57401a6e4 100644 --- a/src/CodeIndex/Indexer/References/ReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/References/ReferenceExtractor.cs @@ -115,7 +115,8 @@ public static ReferenceExtractionResult ExtractDetailed( cancellationToken, maxReferenceCount, workspaceRoot: null, - csharpStaticInterfaceMemberLookups: null); + csharpStaticInterfaceMemberLookups: null, + csharpQualifiedPatternLookups: null); internal static ReferenceExtractionResult ExtractDetailedNormalized( long fileId, @@ -129,7 +130,8 @@ internal static ReferenceExtractionResult ExtractDetailedNormalized( int? maxReferenceCount = null, int? conflictMarkerLine = null, string? workspaceRoot = null, - CSharpStaticInterfaceMemberLookups? csharpStaticInterfaceMemberLookups = null) + CSharpStaticInterfaceMemberLookups? csharpStaticInterfaceMemberLookups = null, + CSharpQualifiedPatternLookups? csharpQualifiedPatternLookups = null) => ExtractDetailedCore( fileId, lang, @@ -143,7 +145,8 @@ internal static ReferenceExtractionResult ExtractDetailedNormalized( cancellationToken, maxReferenceCount, workspaceRoot, - csharpStaticInterfaceMemberLookups); + csharpStaticInterfaceMemberLookups, + csharpQualifiedPatternLookups); private static ReferenceExtractionResult ExtractDetailedCore( long fileId, @@ -158,7 +161,8 @@ private static ReferenceExtractionResult ExtractDetailedCore( CancellationToken cancellationToken, int? maxReferenceCount, string? workspaceRoot, - CSharpStaticInterfaceMemberLookups? csharpStaticInterfaceMemberLookups) + CSharpStaticInterfaceMemberLookups? csharpStaticInterfaceMemberLookups, + CSharpQualifiedPatternLookups? csharpQualifiedPatternLookups) { cancellationToken.ThrowIfCancellationRequested(); var requestedLanguage = lang; @@ -214,6 +218,7 @@ void ReportDiagnostic(ReferenceExtractionDiagnostic diagnostic) conflictMarkerLine) { CSharpStaticInterfaceMemberLookups = csharpStaticInterfaceMemberLookups, + CSharpQualifiedPatternLookups = csharpQualifiedPatternLookups, }; var builtInReferences = extractor.Extract(extractionContext); return new ReferenceExtractionResult( diff --git a/src/CodeIndex/Indexer/Scanning/FileContentLoader.cs b/src/CodeIndex/Indexer/Scanning/FileContentLoader.cs index ee8ce6049..00710b3b6 100644 --- a/src/CodeIndex/Indexer/Scanning/FileContentLoader.cs +++ b/src/CodeIndex/Indexer/Scanning/FileContentLoader.cs @@ -132,7 +132,7 @@ internal string LoadNormalizedContentForPrepass( stream, initialLength, normalizedRelativePath, - probe.AppendAndCheck, + probe.AppendAndCheckWorkspaceCandidate, cancellationToken); if (rawCandidate) { diff --git a/src/CodeIndex/Mcp/McpToolHandlers.Indexing.Execution.cs b/src/CodeIndex/Mcp/McpToolHandlers.Indexing.Execution.cs index 71109e320..2f5c97204 100644 --- a/src/CodeIndex/Mcp/McpToolHandlers.Indexing.Execution.cs +++ b/src/CodeIndex/Mcp/McpToolHandlers.Indexing.Execution.cs @@ -1550,7 +1550,8 @@ await EmitProgressNotificationAsync( maxReferenceCount: maxReferencesPerFile + 1, conflictMarkerLine: loaded.ConflictMarkerLine, workspaceRoot: projectPath, - csharpStaticInterfaceMemberLookups: csharpWorkspace.StaticInterfaceMemberLookups); + csharpStaticInterfaceMemberLookups: csharpWorkspace.StaticInterfaceMemberLookups, + csharpQualifiedPatternLookups: csharpWorkspace.QualifiedPatternLookups); references = referenceExtraction.References; regexTimeoutIssue = IndexCommandRunner.BuildRegexTimeoutIssue(record.Path, regexTimeouts); } diff --git a/tests/CodeIndex.Tests/DatabaseTests.cs b/tests/CodeIndex.Tests/DatabaseTests.cs index 134ecd3d1..681378795 100644 --- a/tests/CodeIndex.Tests/DatabaseTests.cs +++ b/tests/CodeIndex.Tests/DatabaseTests.cs @@ -4493,6 +4493,17 @@ public void CSharpContractWorkspaceQueries_UseFileKindThenBoundedInterfaceNamePl detail.Contains("SCAN f", StringComparison.Ordinal) || detail.Contains("SCAN s", StringComparison.Ordinal)); + var memberReadPlan = ReadQueryPlanDetails( + _db.Connection, + DbWriter.CSharpMemberReadTargetWorkspaceSql); + Assert.Contains(memberReadPlan, detail => + detail.Contains("SEARCH f USING INDEX idx_files_lang", StringComparison.Ordinal)); + Assert.Contains(memberReadPlan, detail => + detail.Contains("SEARCH s USING INDEX idx_symbols_file_kind", StringComparison.Ordinal)); + Assert.DoesNotContain(memberReadPlan, detail => + detail.Contains("SCAN f", StringComparison.Ordinal) + || detail.Contains("SCAN s", StringComparison.Ordinal)); + var interfacePlan = ReadQueryPlanDetails( _db.Connection, DbWriter.BuildCSharpStaticInterfaceDeclarationWorkspaceSql(batchCount: 1), @@ -4625,6 +4636,76 @@ public void LoadCSharpContractWorkspace_MaterializesOnlyCandidatesAndMatchingInt } } + [Fact] + public void LoadCSharpWorkspace_IncludesPersistedMemberReadTargets_Issue4894() + { + var fileId = UpsertTestFile("src/Values.cs", "member-read-targets"); + _writer.InsertSymbols( + [ + new SymbolRecord + { + FileId = fileId, + Kind = "enum", + Name = "Ready", + Line = 2, + Signature = "Ready", + ContainerKind = "enum", + ContainerName = "State", + ContainerQualifiedName = "Demo.State", + }, + new SymbolRecord + { + FileId = fileId, + Kind = "field", + Name = "Limit", + Line = 7, + Signature = "public const int Limit = 10;", + ContainerKind = "class", + ContainerName = "Values", + ContainerQualifiedName = "Demo.Values", + }, + new SymbolRecord + { + FileId = fileId, + Kind = "field", + Name = "Other", + Line = 8, + Signature = "public static readonly int Other = 20;", + ContainerKind = "class", + ContainerName = "Values", + ContainerQualifiedName = "Demo.Values", + }, + new SymbolRecord + { + FileId = fileId, + Kind = "property", + Name = "Property", + Line = 9, + Signature = "public static int Property => 30;", + ContainerKind = "class", + ContainerName = "Values", + ContainerQualifiedName = "Demo.Values", + }, + new SymbolRecord + { + FileId = fileId, + Kind = "field", + Name = "Instance", + Line = 10, + Signature = "public int Instance;", + ContainerKind = "class", + ContainerName = "Values", + ContainerQualifiedName = "Demo.Values", + }, + ]); + + var loaded = _writer.LoadCSharpStaticInterfaceContractSymbols(); + + Assert.Equal( + ["Limit", "Other", "Property", "Ready"], + loaded.Select(symbol => symbol.Name).Order(StringComparer.Ordinal).ToArray()); + } + [Fact] public void TryMigrateForRead_CreatesReferenceCompositeIndexesForGraphLookups() { diff --git a/tests/CodeIndex.Tests/FileIndexerTests.cs b/tests/CodeIndex.Tests/FileIndexerTests.cs index 819853c29..bc384dcc1 100644 --- a/tests/CodeIndex.Tests/FileIndexerTests.cs +++ b/tests/CodeIndex.Tests/FileIndexerTests.cs @@ -1848,6 +1848,7 @@ public void RawFileMayContainCSharpStaticInterfaceContract_OverExplicitMaxFileBy [Theory] [InlineData("raw-negative", false, false)] + [InlineData("workspace-member", true, false)] [InlineData("semantic-negative", true, false)] [InlineData("contract", true, true)] public void LoadCSharpStaticInterfaceCandidateContentForPrepass_ProbeShapesUseOneAuthorizedBoundedSnapshot( @@ -1859,7 +1860,8 @@ public void LoadCSharpStaticInterfaceCandidateContentForPrepass_ProbeShapesUseOn var filler = new string('x', 128 * 1024); var source = shape switch { - "raw-negative" => $"public class C {{ static int M() => 0; {filler} }}", + "raw-negative" => $"public class C {{ int M() => 0; {filler} }}", + "workspace-member" => $"public class C {{ static int M() => 0; {filler} }}", "semantic-negative" => $"public class C {{ const string S = \"interface I {{ static abstract int M(); }}\"; {filler} }}", "contract" => $"public interface I {{ static abstract int M(); {filler} }}", _ => throw new ArgumentOutOfRangeException(nameof(shape), shape, null), diff --git a/tests/CodeIndex.Tests/IndexCommandRunnerFullScanTests.cs b/tests/CodeIndex.Tests/IndexCommandRunnerFullScanTests.cs index 68394e3bd..604a19141 100644 --- a/tests/CodeIndex.Tests/IndexCommandRunnerFullScanTests.cs +++ b/tests/CodeIndex.Tests/IndexCommandRunnerFullScanTests.cs @@ -4750,6 +4750,25 @@ public class Caller IndexCommandRunner.Run([projectRoot, "--json"], _jsonOptions)); var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + File.AppendAllText( + Path.Combine(projectRoot, "Caller.cs"), + $"{Environment.NewLine}// Force a reader-only refresh.{Environment.NewLine}"); + Assert.Equal( + CommandExitCodes.Success, + IndexCommandRunner.Run([projectRoot, "--json"], _jsonOptions)); + using (var incrementalVerify = OpenNonPoolingConnection(dbPath)) + { + incrementalVerify.Open(); + using var incrementalReferenceCmd = incrementalVerify.CreateCommand(); + incrementalReferenceCmd.CommandText = """ + SELECT COUNT(*) + FROM symbol_references + WHERE symbol_name IN ('Limit', 'Other', 'Property') + AND reference_kind = 'member_read' + """; + Assert.Equal(3L, incrementalReferenceCmd.ExecuteScalar()); + } + using (var conn = OpenNonPoolingConnection(dbPath)) { conn.Open(); @@ -4807,11 +4826,19 @@ public void Run_FullScan_ReclassifiesQualifiedValueReadsFromVersion8CSharpIndex_ public static class Values { public const int Limit = 10; + public static readonly int Other = 20; + public static int Property => 30; } + """); + File.WriteAllText( + Path.Combine(projectRoot, "Caller.cs"), + """ public sealed class Caller { - public int Read() => Values.Limit; + private static int Limit() => 0; + + public int Read() => Values.Limit + Values.Other + Values.Property; } """); @@ -4827,7 +4854,7 @@ public sealed class Caller cmd.CommandText = $""" UPDATE symbol_references SET reference_kind = 'call' - WHERE symbol_name = 'Limit'; + WHERE symbol_name IN ('Limit', 'Other', 'Property'); UPDATE codeindex_meta SET value = '8' WHERE key = '{DbContext.GetSymbolExtractorVersionMetaKey("csharp")}'; @@ -4845,11 +4872,25 @@ UPDATE codeindex_meta verify.Open(); using var referenceCmd = verify.CreateCommand(); referenceCmd.CommandText = """ - SELECT reference_kind + SELECT symbol_name, reference_kind FROM symbol_references - WHERE symbol_name = 'Limit' + WHERE symbol_name IN ('Limit', 'Other', 'Property') + ORDER BY symbol_name """; - Assert.Equal("member_read", referenceCmd.ExecuteScalar() as string); + using (var reader = referenceCmd.ExecuteReader()) + { + var actual = new List<(string Name, string Kind)>(); + while (reader.Read()) + actual.Add((reader.GetString(0), reader.GetString(1))); + + Assert.Equal( + [ + ("Limit", "member_read"), + ("Other", "member_read"), + ("Property", "member_read"), + ], + actual); + } using var versionCmd = verify.CreateCommand(); versionCmd.CommandText = diff --git a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs index f203ba380..4955ec884 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs @@ -13941,12 +13941,29 @@ static class Values public const int Constant = 1; public static readonly int Readonly = 2; public static int Property => 3; + public static int Mutable; + public static int Settable { get; set; } public static int Method() => 4; } + static class CallableNameCollisions + { + public static int Constant() => 0; + public static int Readonly() => 0; + public static int Property() => 0; + public static int Mutable() => 0; + public static int Settable() => 0; + } + class Demo { int Run() => Values.Constant + Values.Readonly + Values.Property + Values.Method(); + + void Write() + { + Values.Mutable = 5; + Values.Settable = 6; + } } """; @@ -13958,6 +13975,58 @@ class Demo Assert.Contains(references, r => r.SymbolName == "Property" && r.ReferenceKind == "member_read" && r.ContainerName == "Run"); Assert.Contains(references, r => r.SymbolName == "Method" && r.ReferenceKind == "call" && r.ContainerName == "Run"); Assert.DoesNotContain(references, r => (r.SymbolName is "Constant" or "Readonly" or "Property") && r.ReferenceKind == "call"); + Assert.DoesNotContain(references, r => + (r.SymbolName is "Mutable" or "Settable") + && (r.ReferenceKind is "member_read" or "call") + && r.ContainerName == "Write"); + } + + [Fact] + public void Extract_CsharpQualifiedValueReads_ResolveWorkspaceTargets_Issue4894() + { + const string definitions = """ + namespace Probe; + + static class Values + { + public const int Limit = 10; + public static readonly int Other = 20; + public static int Property => 30; + } + """; + const string reader = """ + namespace Probe; + + class Reader + { + private static int Limit() => 0; + private static int Other() => 0; + private static int Property() => 0; + + int Read() => Values.Limit + Values.Other + Values.Property; + } + """; + + var symbols = SymbolExtractor.Extract(1, "csharp", reader); + var workspaceSymbols = SymbolExtractor.Extract(2, "csharp", definitions); + var references = ReferenceExtractor.Extract( + 1, + "csharp", + reader, + symbols, + "Reader.cs", + workspaceSymbols); + + Assert.Equal( + ["Limit", "Other", "Property"], + references + .Where(reference => reference.ReferenceKind == "member_read") + .Select(reference => reference.SymbolName) + .Order(StringComparer.Ordinal) + .ToArray()); + Assert.DoesNotContain(references, reference => + reference.ReferenceKind == "call" + && (reference.SymbolName is "Limit" or "Other" or "Property")); } [Fact] From 0e7d90f5ffed3ddcd65dd00758fdc6091afbba87 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 31 Jul 2026 02:03:15 +0900 Subject: [PATCH 3/5] Fix scoped member-read indexing (#4894) --- changelog.d/unreleased/4894.added.md | 7 +- ...xCommandRunner.FullScan.CSharpPreflight.cs | 4 +- ...xCommandRunner.FullScan.TargetSelection.cs | 4 +- ...dexCommandRunner.Update.CSharpPreflight.cs | 45 ++++- .../Database/DbWriter.CSharpContracts.cs | 155 ++++++++++++++-- .../Indexer/CSharpStaticInterfacePrepass.cs | 12 +- .../CSharpReferenceExtractor.Support.cs | 4 +- .../Mcp/McpToolHandlers.Indexing.Execution.cs | 3 +- tests/CodeIndex.Tests/DatabaseTests.cs | 15 +- .../IndexCommandRunnerFullScanTests.cs | 170 ++++++++++++++++++ 10 files changed, 392 insertions(+), 27 deletions(-) diff --git a/changelog.d/unreleased/4894.added.md b/changelog.d/unreleased/4894.added.md index f074704b7..3b363c3cb 100644 --- a/changelog.d/unreleased/4894.added.md +++ b/changelog.d/unreleased/4894.added.md @@ -8,8 +8,11 @@ affected: - src/CodeIndex/Indexer/CSharpStaticInterfacePrepass.cs - src/CodeIndex/Database/DbWriter.CSharpContracts.cs - src/CodeIndex/Database/DbReader.GraphQueries.cs + - src/CodeIndex/Cli/IndexCommandRunner.Update.CSharpPreflight.cs + - src/CodeIndex/Cli/IndexCommandRunner.FullScan.CSharpPreflight.cs - src/CodeIndex/Cli/CliFlagSchema.cs - src/CodeIndex/Mcp/McpToolCatalog.cs + - src/CodeIndex/Mcp/McpToolHandlers.Indexing.Execution.cs - src/CodeIndex/Models/SymbolKindCatalog.cs - tests/CodeIndex.Tests/ReferenceExtractorTests.cs - tests/CodeIndex.Tests/IndexCommandRunnerFullScanTests.cs @@ -21,8 +24,8 @@ affected: ## English -- **Added canonical `member_read` references for non-invoking C# member/value reads (#4894)** — new indexes distinguish qualified enum constants, constants, static readonly fields, static properties, and constant-pattern reads from true `call` edges, including cross-file targets and callable-name collisions. Callers, callees, and impact exclude these reads by default and expose explicit CLI `--include-member-reads` / MCP `includeMemberReads` compatibility controls; legacy indexes remain readable with their historical `call` rows. +- **Added canonical `member_read` references for non-invoking C# member/value reads (#4894)** — new indexes distinguish qualified enum constants, constants, static readonly fields, static properties, and constant-pattern reads from true `call` edges, including cross-file targets, scoped/watch-style incremental updates, and callable-name collisions. Callers, callees, and impact exclude these reads by default and expose explicit CLI `--include-member-reads` / MCP `includeMemberReads` compatibility controls; legacy indexes remain readable with their historical `call` rows. ## 日本語 -- **呼び出しを伴わない C# member / value read 向けに canonical `member_read` reference を追加しました (#4894)** — 新しい index は cross-file target や callable 名の衝突を含む修飾付き enum 定数、定数、static readonly field、static property、constant-pattern read を真の `call` edge と区別します。callers、callees、impact は既定でこれらを除外し、CLI `--include-member-reads` / MCP `includeMemberReads` の明示 compatibility control で復元できます。legacy index は従来の `call` row のまま引き続き読み取れます。 +- **呼び出しを伴わない C# member / value read 向けに canonical `member_read` reference を追加しました (#4894)** — 新しい index は cross-file target、scoped / watch-style incremental update、callable 名の衝突を含む修飾付き enum 定数、定数、static readonly field、static property、constant-pattern read を真の `call` edge と区別します。callers、callees、impact は既定でこれらを除外し、CLI `--include-member-reads` / MCP `includeMemberReads` の明示 compatibility control で復元できます。legacy index は従来の `call` row のまま引き続き読み取れます。 diff --git a/src/CodeIndex/Cli/IndexCommandRunner.FullScan.CSharpPreflight.cs b/src/CodeIndex/Cli/IndexCommandRunner.FullScan.CSharpPreflight.cs index b978c8ccf..ef9935fbe 100644 --- a/src/CodeIndex/Cli/IndexCommandRunner.FullScan.CSharpPreflight.cs +++ b/src/CodeIndex/Cli/IndexCommandRunner.FullScan.CSharpPreflight.cs @@ -194,7 +194,9 @@ bool CanReuseCSharpPrepassTargetWithoutRead( forceFullCSharpRefreshFromInvalidatedNoOp = csharpWorkspaceMaterialized && (context.PriorCSharpStaticInterfaceSourceEvidence == true - || csharpWorkspace.HasStaticInterfaceContracts); + || csharpWorkspace.HasStaticInterfaceContracts + || csharpWorkspace + .RequiresMemberReadReferenceRefresh); } if (!options.SymbolsOnly diff --git a/src/CodeIndex/Cli/IndexCommandRunner.FullScan.TargetSelection.cs b/src/CodeIndex/Cli/IndexCommandRunner.FullScan.TargetSelection.cs index 5dd5bbabe..dd9345451 100644 --- a/src/CodeIndex/Cli/IndexCommandRunner.FullScan.TargetSelection.cs +++ b/src/CodeIndex/Cli/IndexCommandRunner.FullScan.TargetSelection.cs @@ -328,7 +328,9 @@ private static void RebuildInvalidatedFullScanCSharpNoOp( var requiresFullCSharpRefresh = context.PriorCSharpStaticInterfaceSourceEvidence == true - || state.CSharpWorkspace.HasStaticInterfaceContracts; + || state.CSharpWorkspace.HasStaticInterfaceContracts + || state.CSharpWorkspace + .RequiresMemberReadReferenceRefresh; state.ForceFullCSharpRefreshFromInvalidatedNoOp = requiresFullCSharpRefresh; state.CSharpSourceEvidenceForStamp = diff --git a/src/CodeIndex/Cli/IndexCommandRunner.Update.CSharpPreflight.cs b/src/CodeIndex/Cli/IndexCommandRunner.Update.CSharpPreflight.cs index ffc98bacb..efe43278b 100644 --- a/src/CodeIndex/Cli/IndexCommandRunner.Update.CSharpPreflight.cs +++ b/src/CodeIndex/Cli/IndexCommandRunner.Update.CSharpPreflight.cs @@ -130,7 +130,8 @@ private static UpdateCSharpPreflightResult PrepareUpdateCSharpWorkspace( StopIndexJsonPhaseHeartbeat(heartbeat); } - if (state.CSharpWorkspace.HasStaticInterfaceContracts) + if (state.CSharpWorkspace.HasStaticInterfaceContracts + || state.CSharpWorkspace.RequiresMemberReadReferenceRefresh) ExpandUpdateCSharpWorkspace(context, state); return new UpdateCSharpPreflightResult( @@ -164,6 +165,16 @@ private static void BuildInitialUpdateCSharpWorkspace( context.PriorCSharpStaticInterfaceSourceEvidence == null || !context.PriorFilterRetainedCSharpContractMembers, cancellationToken); + var transitionedPathHadMemberReadTarget = + transitionedPaths is { Count: > 0 } + && writer.HasCSharpMemberReadTargetSymbolsInPaths( + transitionedPaths, + cancellationToken); + var scopedCleanupHadMemberReadTarget = + context.ScopedCleanupPlan.FileIds.Count > 0 + && writer.HasCSharpMemberReadTargetSymbolsInFileIds( + context.ScopedCleanupPlan.FileIds, + cancellationToken); state.CSharpTargetAffected = state.CSharpPrepassTargets.Count > 0 || transitionedPathWasCSharp || context.ScopedCleanupHadCSharp; @@ -176,13 +187,24 @@ private static void BuildInitialUpdateCSharpWorkspace( persistedContractEvidence; if (state.CSharpPrepassTargets.Count == 0 - && !persistedContractEvidence) + && !persistedContractEvidence + && !transitionedPathHadMemberReadTarget + && !scopedCleanupHadMemberReadTarget) { state.CSharpWorkspace = new CSharpStaticInterfaceWorkspaceSymbols( [], transitionedPathHadContract); } + else if (transitionedPathHadMemberReadTarget + || scopedCleanupHadMemberReadTarget) + { + state.CSharpWorkspace = + new CSharpStaticInterfaceWorkspaceSymbols( + [], + false, + RequiresMemberReadReferenceRefresh: true); + } else if (persistedContractEvidence) { // Persisted contracts already require the complete C# update set. Defer @@ -241,7 +263,13 @@ private static void BuildInitialUpdateCSharpWorkspaceSnapshot( context.Writer, context.Indexer, state.CSharpPrepassTargets, - includeExistingSymbols: false, + includeExistingSymbols: true, + excludedExistingFileIds: + context.ScopedCleanupPlan.FileIds, + isExistingSymbolPathExcluded: path => + state + .ExistingCSharpPathsNowUnsupportedOrNonCSharp? + .Contains(path) == true, parallelism: context.Options.Parallelism, cancellationToken: context.CancellationToken); if (!CSharpStaticInterfacePrepass.TryValidateFileStatSnapshots( @@ -426,6 +454,17 @@ private static void BuildExpandedUpdateCSharpWorkspace( state.CSharpSourceEvidenceForStamp = state.CSharpWorkspace.HasSourceStaticInterfaceContracts; state.CSharpSourceEvidenceCompleteForStamp = true; + if (state.CSharpWorkspace.RequiresMemberReadReferenceRefresh) + { + // A target-set change requires every C# consumer to be re-extracted + // against the new lookup, including otherwise reusable files. + // target集合の変更時は、通常なら再利用可能なfileも含め、全C# consumerを + // 新しいlookupで再抽出する。 + state.CSharpWorkspace = state.CSharpWorkspace with + { + HasStaticInterfaceContracts = true, + }; + } // Persisted positive/legacy evidence remains conservative until every C# file // has been refreshed successfully. Even when the new source snapshot is diff --git a/src/CodeIndex/Database/DbWriter.CSharpContracts.cs b/src/CodeIndex/Database/DbWriter.CSharpContracts.cs index 0d9c94300..8323e6c30 100644 --- a/src/CodeIndex/Database/DbWriter.CSharpContracts.cs +++ b/src/CodeIndex/Database/DbWriter.CSharpContracts.cs @@ -67,21 +67,24 @@ CROSS JOIN symbols s INDEXED BY idx_symbols_file_kind WHERE f.lang = 'csharp' AND " + CSharpStaticInterfaceContractMemberPredicateSql; - internal const string CSharpMemberReadTargetWorkspaceSql = @" - SELECT " + CSharpContractWorkspaceProjectionSql + @" - FROM files f INDEXED BY idx_files_lang - CROSS JOIN symbols s INDEXED BY idx_symbols_file_kind - ON s.file_id = f.id - WHERE f.lang = 'csharp' - AND ( + private const string CSharpMemberReadTargetPredicateSql = @" + ( (s.kind = 'enum' AND s.container_kind = 'enum') OR ( s.kind IN ('field', 'property') - AND s.container_kind IN ('class', 'struct') + AND s.container_kind IN ('class', 'struct', 'interface') AND (s.signature LIKE '%static%' OR s.signature LIKE '%const%') ) )"; + internal const string CSharpMemberReadTargetWorkspaceSql = @" + SELECT " + CSharpContractWorkspaceProjectionSql + @" + FROM files f INDEXED BY idx_files_lang + CROSS JOIN symbols s INDEXED BY idx_symbols_file_kind + ON s.file_id = f.id + WHERE f.lang = 'csharp' + AND " + CSharpMemberReadTargetPredicateSql; + internal bool? GetCSharpStaticInterfaceSourceEvidence() { var raw = GetMetaString(DbContext.CSharpStaticInterfaceSourceEvidenceMetaKey); @@ -103,7 +106,8 @@ internal List LoadCSharpStaticInterfaceContractSymbols( excludedPaths, excludedExistingFileIds: null, isExistingSymbolPathExcluded: null, - out excludedPathsHaveContracts); + out excludedPathsHaveContracts, + out _); internal List LoadCSharpStaticInterfaceContractSymbols( IReadOnlySet? excludedPaths, @@ -113,16 +117,33 @@ internal List LoadCSharpStaticInterfaceContractSymbols( excludedPaths, excludedExistingFileIds, isExistingSymbolPathExcluded: null, - out excludedPathsHaveContracts); + out excludedPathsHaveContracts, + out _); + + internal List LoadCSharpStaticInterfaceContractSymbols( + IReadOnlySet? excludedPaths, + IReadOnlyList? excludedExistingFileIds, + Func? isExistingSymbolPathExcluded, + out bool excludedPathsHaveContracts, + CancellationToken cancellationToken = default) + => LoadCSharpStaticInterfaceContractSymbols( + excludedPaths, + excludedExistingFileIds, + isExistingSymbolPathExcluded, + out excludedPathsHaveContracts, + out _, + cancellationToken); internal List LoadCSharpStaticInterfaceContractSymbols( IReadOnlySet? excludedPaths, IReadOnlyList? excludedExistingFileIds, Func? isExistingSymbolPathExcluded, out bool excludedPathsHaveContracts, + out bool excludedPathsHaveMemberReadTargets, CancellationToken cancellationToken = default) { excludedPathsHaveContracts = false; + excludedPathsHaveMemberReadTargets = false; var symbols = new List(); var retainedContractContainerNames = new HashSet(StringComparer.Ordinal); var memberCandidateRowsRead = 0; @@ -246,6 +267,7 @@ internal List LoadCSharpStaticInterfaceContractSymbols( excludedPaths, excludedExistingFileIds, isExistingSymbolPathExcluded, + out excludedPathsHaveMemberReadTargets, cancellationToken); cancellationToken.ThrowIfCancellationRequested(); CSharpContractWorkspaceReadStatsForTesting?.Invoke( @@ -263,8 +285,10 @@ private void AppendCSharpMemberReadTargetSymbols( IReadOnlySet? excludedPaths, IReadOnlyList? excludedExistingFileIds, Func? isExistingSymbolPathExcluded, + out bool excludedPathsHaveMemberReadTargets, CancellationToken cancellationToken) { + excludedPathsHaveMemberReadTargets = false; cancellationToken.ThrowIfCancellationRequested(); var cmd = RentCommand(CSharpMemberReadTargetWorkspaceSql, static _ => { }); try @@ -274,20 +298,26 @@ private void AppendCSharpMemberReadTargetSymbols( while (reader.Read()) { cancellationToken.ThrowIfCancellationRequested(); + var symbol = ReadCSharpContractWorkspaceSymbol(reader); + if (!ReferenceExtractor.IsCSharpQualifiedMemberReadTargetSymbol(symbol)) + continue; + var fileId = reader.GetInt64(1); if (FilePurgePlan.ContainsSortedFileId(excludedExistingFileIds, fileId)) + { + excludedPathsHaveMemberReadTargets = true; continue; + } var path = reader.GetString(0); if (excludedPaths?.Contains(path) == true || isExistingSymbolPathExcluded?.Invoke(path) == true) { + excludedPathsHaveMemberReadTargets = true; continue; } - var symbol = ReadCSharpContractWorkspaceSymbol(reader); - if (ReferenceExtractor.IsCSharpQualifiedMemberReadTargetSymbol(symbol)) - symbols.Add(symbol); + symbols.Add(symbol); } cancellationToken.ThrowIfCancellationRequested(); @@ -305,6 +335,105 @@ private void AppendCSharpMemberReadTargetSymbols( } } + internal bool HasCSharpMemberReadTargetSymbolsInFileIds( + IReadOnlyList sortedFileIds, + CancellationToken cancellationToken = default) + { + if (sortedFileIds.Count == 0) + return false; + + cancellationToken.ThrowIfCancellationRequested(); + for (var offset = 0; offset < sortedFileIds.Count; offset += DeleteFilesBatchSize) + { + cancellationToken.ThrowIfCancellationRequested(); + var batchCount = Math.Min(DeleteFilesBatchSize, sortedFileIds.Count - offset); + SqliteDynamicSql.EnsureParameterBudget(batchCount, "C# member-read target file-id preflight batch"); + var parameterList = SqliteDynamicSql.BuildParameterList("fileId", batchCount); + var sql = @" + SELECT " + CSharpContractWorkspaceProjectionSql + @" + FROM files f + CROSS JOIN symbols s INDEXED BY idx_symbols_file_kind + ON s.file_id = f.id + WHERE f.lang = 'csharp' + AND s.file_id IN (" + parameterList + @") + AND " + CSharpMemberReadTargetPredicateSql; + using var cmd = _conn.CreateCommand(); + cmd.Transaction = _activeTransaction; + cmd.CommandText = sql; + for (var parameterIndex = 0; parameterIndex < batchCount; parameterIndex++) + { + cmd.Parameters.Add( + SqliteDynamicSql.BuildParameterName("fileId", parameterIndex), + SqliteType.Integer).Value = sortedFileIds[offset + parameterIndex]; + } + + using var cancellationRegistration = RegisterSqliteInterrupt(cancellationToken); + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + cancellationToken.ThrowIfCancellationRequested(); + if (ReferenceExtractor.IsCSharpQualifiedMemberReadTargetSymbol( + ReadCSharpContractWorkspaceSymbol(reader))) + { + return true; + } + } + } + + cancellationToken.ThrowIfCancellationRequested(); + return false; + } + + internal bool HasCSharpMemberReadTargetSymbolsInPaths( + IReadOnlySet paths, + CancellationToken cancellationToken = default) + { + if (paths.Count == 0) + return false; + + cancellationToken.ThrowIfCancellationRequested(); + var pathArray = paths.ToArray(); + for (var offset = 0; offset < pathArray.Length; offset += DeleteFilesBatchSize) + { + cancellationToken.ThrowIfCancellationRequested(); + var batchCount = Math.Min(DeleteFilesBatchSize, pathArray.Length - offset); + SqliteDynamicSql.EnsureParameterBudget(batchCount, "C# member-read target path preflight batch"); + var parameterList = SqliteDynamicSql.BuildParameterList("path", batchCount); + var sql = @" + SELECT " + CSharpContractWorkspaceProjectionSql + @" + FROM files f + CROSS JOIN symbols s INDEXED BY idx_symbols_file_kind + ON s.file_id = f.id + WHERE f.path IN (" + parameterList + @") + AND f.lang = 'csharp' + AND " + CSharpMemberReadTargetPredicateSql; + using var cmd = _conn.CreateCommand(); + cmd.Transaction = _activeTransaction; + cmd.CommandText = sql; + for (var parameterIndex = 0; parameterIndex < batchCount; parameterIndex++) + { + cmd.Parameters.Add( + SqliteDynamicSql.BuildParameterName("path", parameterIndex), + SqliteType.Text).Value = pathArray[offset + parameterIndex]; + } + + using var cancellationRegistration = RegisterSqliteInterrupt(cancellationToken); + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + cancellationToken.ThrowIfCancellationRequested(); + if (ReferenceExtractor.IsCSharpQualifiedMemberReadTargetSymbol( + ReadCSharpContractWorkspaceSymbol(reader))) + { + return true; + } + } + } + + cancellationToken.ThrowIfCancellationRequested(); + return false; + } + internal static string BuildCSharpStaticInterfaceDeclarationWorkspaceSql(int batchCount) { SqliteDynamicSql.EnsureParameterBudget( diff --git a/src/CodeIndex/Indexer/CSharpStaticInterfacePrepass.cs b/src/CodeIndex/Indexer/CSharpStaticInterfacePrepass.cs index e5b3e4ac3..32fda36f1 100644 --- a/src/CodeIndex/Indexer/CSharpStaticInterfacePrepass.cs +++ b/src/CodeIndex/Indexer/CSharpStaticInterfacePrepass.cs @@ -145,16 +145,22 @@ internal static CSharpStaticInterfaceWorkspaceSymbols BuildWorkspaceSymbols( var hasSourceStaticInterfaceContracts = HasCSharpStaticInterfaceContractSymbol(pendingSymbols); var hadPendingContracts = false; + var hadPendingMemberReadTargets = false; var symbols = includeExistingSymbols ? writer.LoadCSharpStaticInterfaceContractSymbols( pendingPaths!, excludedExistingFileIds, isExistingSymbolPathExcluded, out hadPendingContracts, + out hadPendingMemberReadTargets, cancellationToken) : []; symbols.AddRange(pendingSymbols); var hasStaticInterfaceContracts = HasCSharpStaticInterfaceContractSymbol(symbols) || hadPendingContracts; + var requiresMemberReadReferenceRefresh = + hadPendingMemberReadTargets + || pendingSymbols.Any( + ReferenceExtractor.IsCSharpQualifiedMemberReadTargetSymbol); IReadOnlyList incompletePaths = firstIncompleteSourcePath == null ? [] : [firstIncompleteSourcePath]; @@ -165,7 +171,8 @@ internal static CSharpStaticInterfaceWorkspaceSymbols BuildWorkspaceSymbols( hasSourceStaticInterfaceContracts, sourceEvidenceComplete != 0, incompletePaths, - ReferenceExtractor.BuildCSharpQualifiedPatternLookups(symbols)); + ReferenceExtractor.BuildCSharpQualifiedPatternLookups(symbols), + requiresMemberReadReferenceRefresh); } internal static CSharpStaticInterfaceWorkspaceSymbols BuildWorkspaceSymbols( @@ -1036,4 +1043,5 @@ internal sealed record CSharpStaticInterfaceWorkspaceSymbols( bool HasSourceStaticInterfaceContracts = false, bool SourceContractEvidenceComplete = true, IReadOnlyList? IncompleteSourcePaths = null, - ReferenceExtractor.CSharpQualifiedPatternLookups? QualifiedPatternLookups = null); + ReferenceExtractor.CSharpQualifiedPatternLookups? QualifiedPatternLookups = null, + bool RequiresMemberReadReferenceRefresh = false); diff --git a/src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.Support.cs b/src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.Support.cs index 08d411093..b3b8e5312 100644 --- a/src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.Support.cs +++ b/src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.Support.cs @@ -662,13 +662,13 @@ private static void AddCSharpQualifiedPatternTarget( internal static bool IsCSharpQualifiedMemberReadTargetSymbol(SymbolRecord symbol) => symbol.Kind == "enum" && symbol.ContainerKind == "enum" || IsCSharpConstMemberSymbol(symbol) - || symbol.ContainerKind is "class" or "struct" + || symbol.ContainerKind is "class" or "struct" or "interface" && symbol.Kind is "field" or "property" && IsStaticCSharpSymbol(symbol); private static bool IsCSharpConstMemberSymbol(SymbolRecord symbol) { - if (symbol.ContainerKind is not ("class" or "struct")) + if (symbol.ContainerKind is not ("class" or "struct" or "interface")) return false; if (string.IsNullOrWhiteSpace(symbol.Signature)) return false; diff --git a/src/CodeIndex/Mcp/McpToolHandlers.Indexing.Execution.cs b/src/CodeIndex/Mcp/McpToolHandlers.Indexing.Execution.cs index 2f5c97204..07d299d1d 100644 --- a/src/CodeIndex/Mcp/McpToolHandlers.Indexing.Execution.cs +++ b/src/CodeIndex/Mcp/McpToolHandlers.Indexing.Execution.cs @@ -718,7 +718,8 @@ CSharpStaticInterfaceWorkspaceSymbols BuildStableCSharpWorkspace( cancellationToken: requestToken)); forceFullCSharpRefreshFromInvalidatedNoOp = indexSnapshot.CSharpStaticInterfaceSourceEvidence == true - || csharpWorkspace.HasStaticInterfaceContracts; + || csharpWorkspace.HasStaticInterfaceContracts + || csharpWorkspace.RequiresMemberReadReferenceRefresh; } if (!csharpWorkspace.SourceContractEvidenceComplete) { diff --git a/tests/CodeIndex.Tests/DatabaseTests.cs b/tests/CodeIndex.Tests/DatabaseTests.cs index 681378795..ca71deb4e 100644 --- a/tests/CodeIndex.Tests/DatabaseTests.cs +++ b/tests/CodeIndex.Tests/DatabaseTests.cs @@ -4687,11 +4687,22 @@ public void LoadCSharpWorkspace_IncludesPersistedMemberReadTargets_Issue4894() ContainerQualifiedName = "Demo.Values", }, new SymbolRecord + { + FileId = fileId, + Kind = "property", + Name = "InterfaceProperty", + Line = 10, + Signature = "public static int InterfaceProperty => 40;", + ContainerKind = "interface", + ContainerName = "IValues", + ContainerQualifiedName = "Demo.IValues", + }, + new SymbolRecord { FileId = fileId, Kind = "field", Name = "Instance", - Line = 10, + Line = 11, Signature = "public int Instance;", ContainerKind = "class", ContainerName = "Values", @@ -4702,7 +4713,7 @@ public void LoadCSharpWorkspace_IncludesPersistedMemberReadTargets_Issue4894() var loaded = _writer.LoadCSharpStaticInterfaceContractSymbols(); Assert.Equal( - ["Limit", "Other", "Property", "Ready"], + ["InterfaceProperty", "Limit", "Other", "Property", "Ready"], loaded.Select(symbol => symbol.Name).Order(StringComparer.Ordinal).ToArray()); } diff --git a/tests/CodeIndex.Tests/IndexCommandRunnerFullScanTests.cs b/tests/CodeIndex.Tests/IndexCommandRunnerFullScanTests.cs index 604a19141..5ccaf2885 100644 --- a/tests/CodeIndex.Tests/IndexCommandRunnerFullScanTests.cs +++ b/tests/CodeIndex.Tests/IndexCommandRunnerFullScanTests.cs @@ -4814,6 +4814,176 @@ FROM symbol_references } } + [Fact] + public void Run_ScopedUpdate_LoadsPersistedMemberReadTargetsAndRefreshesConsumers_Issue4894() + { + var projectRoot = CreateTempProject(); + try + { + var valuesPath = Path.Combine(projectRoot, "Values.cs"); + var callerPath = Path.Combine(projectRoot, "Caller.cs"); + File.WriteAllText( + valuesPath, + """ + public static class Values + { + } + """); + File.WriteAllText( + callerPath, + """ + public sealed class Caller + { + public int Read() => Values.Limit; + } + """); + + Assert.Equal( + CommandExitCodes.Success, + IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + + var dbPath = Path.Combine( + projectRoot, + ".cdidx", + "codeindex.db"); + Assert.Equal(0L, CountMemberReadReferences(dbPath, "Limit")); + + File.WriteAllText( + valuesPath, + """ + public static class Values + { + public const int Limit = 10; + } + """); + File.SetLastWriteTimeUtc( + valuesPath, + DateTime.UtcNow.AddSeconds(2)); + + var (addExitCode, addJson) = RunAndCaptureJson( + [ + projectRoot, + "--files", + valuesPath, + "--json", + "--quiet", + ]); + + Assert.Equal(CommandExitCodes.Success, addExitCode); + Assert.Equal("success", addJson.GetProperty("status").GetString()); + Assert.Equal(1L, CountMemberReadReferences(dbPath, "Limit")); + + File.WriteAllText( + valuesPath, + """ + public static class Values + { + } + """); + File.SetLastWriteTimeUtc( + valuesPath, + DateTime.UtcNow.AddSeconds(4)); + + Assert.Equal( + CommandExitCodes.Success, + IndexCommandRunner.Run( + [ + projectRoot, + "--files", + valuesPath, + "--json", + "--quiet", + ], + _jsonOptions)); + Assert.Equal(0L, CountMemberReadReferences(dbPath, "Limit")); + + File.WriteAllText( + callerPath, + """ + public sealed class Caller + { + public int Read() => + Values.Limit + Values.Other + Values.Property; + } + """); + File.WriteAllText( + valuesPath, + """ + public static class Values + { + public const int Limit = 10; + public static readonly int Other = 20; + public static int Property => 30; + } + """); + File.SetLastWriteTimeUtc( + valuesPath, + DateTime.UtcNow.AddSeconds(6)); + Assert.Equal( + CommandExitCodes.Success, + IndexCommandRunner.Run( + [ + projectRoot, + "--files", + valuesPath, + "--json", + "--quiet", + ], + _jsonOptions)); + + File.WriteAllText( + callerPath, + """ + public sealed class Caller + { + public int Read() => + Values.Limit + Values.Other + Values.Property + 1; + } + """); + File.SetLastWriteTimeUtc( + callerPath, + DateTime.UtcNow.AddSeconds(8)); + + Assert.Equal( + CommandExitCodes.Success, + IndexCommandRunner.Run( + [ + projectRoot, + "--files", + callerPath, + "--json", + "--quiet", + ], + _jsonOptions)); + Assert.Equal(1L, CountMemberReadReferences(dbPath, "Limit")); + Assert.Equal(1L, CountMemberReadReferences(dbPath, "Other")); + Assert.Equal(1L, CountMemberReadReferences(dbPath, "Property")); + } + finally + { + DeleteDirectory(projectRoot); + } + + static long CountMemberReadReferences( + string dbPath, + string symbolName) + { + using var connection = OpenNonPoolingConnection(dbPath); + connection.Open(); + using var command = connection.CreateCommand(); + command.CommandText = """ + SELECT COUNT(*) + FROM symbol_references + WHERE symbol_name = $symbol_name + AND reference_kind = 'member_read' + """; + command.Parameters.AddWithValue("$symbol_name", symbolName); + return (long)command.ExecuteScalar()!; + } + } + [Fact] public void Run_FullScan_ReclassifiesQualifiedValueReadsFromVersion8CSharpIndex_Issue4894() { From 5296a54c2e2dc3a2568a79c9ec49e81881791952 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 31 Jul 2026 02:13:50 +0900 Subject: [PATCH 4/5] Correct member-read regression placement (#4894) --- .../IndexCommandRunnerFullScanTests.cs | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/tests/CodeIndex.Tests/IndexCommandRunnerFullScanTests.cs b/tests/CodeIndex.Tests/IndexCommandRunnerFullScanTests.cs index 5ccaf2885..c53a5c740 100644 --- a/tests/CodeIndex.Tests/IndexCommandRunnerFullScanTests.cs +++ b/tests/CodeIndex.Tests/IndexCommandRunnerFullScanTests.cs @@ -4750,25 +4750,6 @@ public class Caller IndexCommandRunner.Run([projectRoot, "--json"], _jsonOptions)); var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); - File.AppendAllText( - Path.Combine(projectRoot, "Caller.cs"), - $"{Environment.NewLine}// Force a reader-only refresh.{Environment.NewLine}"); - Assert.Equal( - CommandExitCodes.Success, - IndexCommandRunner.Run([projectRoot, "--json"], _jsonOptions)); - using (var incrementalVerify = OpenNonPoolingConnection(dbPath)) - { - incrementalVerify.Open(); - using var incrementalReferenceCmd = incrementalVerify.CreateCommand(); - incrementalReferenceCmd.CommandText = """ - SELECT COUNT(*) - FROM symbol_references - WHERE symbol_name IN ('Limit', 'Other', 'Property') - AND reference_kind = 'member_read' - """; - Assert.Equal(3L, incrementalReferenceCmd.ExecuteScalar()); - } - using (var conn = OpenNonPoolingConnection(dbPath)) { conn.Open(); From 33869af460d7281ae573865edad738a28a1f161d Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 31 Jul 2026 02:38:54 +0900 Subject: [PATCH 5/5] Preserve scoped C# preflight performance (#4894) --- changelog.d/unreleased/4894.added.md | 2 + ...dexCommandRunner.Update.CSharpPreflight.cs | 15 +++++- .../Indexer/CSharpStaticInterfacePrepass.cs | 49 ++++++++++++++++--- .../Indexer/Scanning/FileContentLoader.cs | 6 ++- .../Scanning/FileIndexer.RecordLoading.cs | 12 +++++ .../IndexCommandRunnerFullScanTests.cs | 3 ++ 6 files changed, 76 insertions(+), 11 deletions(-) diff --git a/changelog.d/unreleased/4894.added.md b/changelog.d/unreleased/4894.added.md index 3b363c3cb..1ddfb6d2c 100644 --- a/changelog.d/unreleased/4894.added.md +++ b/changelog.d/unreleased/4894.added.md @@ -6,6 +6,8 @@ affected: - src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.QualifiedPatterns.cs - src/CodeIndex/Indexer/References/ReferenceExtractor.CoreExtraction.cs - src/CodeIndex/Indexer/CSharpStaticInterfacePrepass.cs + - src/CodeIndex/Indexer/Scanning/FileContentLoader.cs + - src/CodeIndex/Indexer/Scanning/FileIndexer.RecordLoading.cs - src/CodeIndex/Database/DbWriter.CSharpContracts.cs - src/CodeIndex/Database/DbReader.GraphQueries.cs - src/CodeIndex/Cli/IndexCommandRunner.Update.CSharpPreflight.cs diff --git a/src/CodeIndex/Cli/IndexCommandRunner.Update.CSharpPreflight.cs b/src/CodeIndex/Cli/IndexCommandRunner.Update.CSharpPreflight.cs index efe43278b..ded37587f 100644 --- a/src/CodeIndex/Cli/IndexCommandRunner.Update.CSharpPreflight.cs +++ b/src/CodeIndex/Cli/IndexCommandRunner.Update.CSharpPreflight.cs @@ -175,6 +175,13 @@ private static void BuildInitialUpdateCSharpWorkspace( && writer.HasCSharpMemberReadTargetSymbolsInFileIds( context.ScopedCleanupPlan.FileIds, cancellationToken); + var changedCSharpPaths = state.CSharpPrepassTargets + .Select(target => target.IndexPath) + .ToHashSet(StringComparer.Ordinal); + var changedPathHadMemberReadTarget = + writer.HasCSharpMemberReadTargetSymbolsInPaths( + changedCSharpPaths, + cancellationToken); state.CSharpTargetAffected = state.CSharpPrepassTargets.Count > 0 || transitionedPathWasCSharp || context.ScopedCleanupHadCSharp; @@ -189,7 +196,8 @@ private static void BuildInitialUpdateCSharpWorkspace( if (state.CSharpPrepassTargets.Count == 0 && !persistedContractEvidence && !transitionedPathHadMemberReadTarget - && !scopedCleanupHadMemberReadTarget) + && !scopedCleanupHadMemberReadTarget + && !changedPathHadMemberReadTarget) { state.CSharpWorkspace = new CSharpStaticInterfaceWorkspaceSymbols( @@ -197,7 +205,8 @@ private static void BuildInitialUpdateCSharpWorkspace( transitionedPathHadContract); } else if (transitionedPathHadMemberReadTarget - || scopedCleanupHadMemberReadTarget) + || scopedCleanupHadMemberReadTarget + || changedPathHadMemberReadTarget) { state.CSharpWorkspace = new CSharpStaticInterfaceWorkspaceSymbols( @@ -270,6 +279,8 @@ private static void BuildInitialUpdateCSharpWorkspaceSnapshot( state .ExistingCSharpPathsNowUnsupportedOrNonCSharp? .Contains(path) == true, + loadExistingSymbolsOnlyForPendingQualifiedMemberAccess: + true, parallelism: context.Options.Parallelism, cancellationToken: context.CancellationToken); if (!CSharpStaticInterfacePrepass.TryValidateFileStatSnapshots( diff --git a/src/CodeIndex/Indexer/CSharpStaticInterfacePrepass.cs b/src/CodeIndex/Indexer/CSharpStaticInterfacePrepass.cs index 32fda36f1..0951a1575 100644 --- a/src/CodeIndex/Indexer/CSharpStaticInterfacePrepass.cs +++ b/src/CodeIndex/Indexer/CSharpStaticInterfacePrepass.cs @@ -24,6 +24,7 @@ internal static CSharpStaticInterfaceWorkspaceSymbols BuildWorkspaceSymbols( int parallelism = 1, IReadOnlyList? excludedExistingFileIds = null, Func? isExistingSymbolPathExcluded = null, + bool loadExistingSymbolsOnlyForPendingQualifiedMemberAccess = false, CancellationToken cancellationToken = default) { var targetCount = fileTargets.TryGetNonEnumeratedCount(out var count) ? count : 0; @@ -79,6 +80,7 @@ internal static CSharpStaticInterfaceWorkspaceSymbols BuildWorkspaceSymbols( var extractedByCandidate = new List?[candidates.Count]; var sourceEvidenceComplete = 1; + var hasPendingQualifiedMemberAccessCandidate = 0; string? firstIncompleteSourcePath = null; var parallelOptions = new ParallelOptions { @@ -97,14 +99,29 @@ internal static CSharpStaticInterfaceWorkspaceSymbols BuildWorkspaceSymbols( var content = indexer.LoadCSharpStaticInterfaceCandidateContentForPrepass( target.FilePath, target.RelativePath, + includeQualifiedMemberAccessCandidate: + loadExistingSymbolsOnlyForPendingQualifiedMemberAccess, cancellationToken); - if (content is not null && MayContainCSharpWorkspaceReferenceTargets(content)) - extractedByCandidate[candidateIndex] = SymbolExtractor.Extract( - 0, - "csharp", - content, - target.IndexPath, - cancellationToken: cancellationToken); + if (content is not null) + { + if (content.AsSpan().IndexOf('.') >= 0) + { + Interlocked.Exchange( + ref hasPendingQualifiedMemberAccessCandidate, + 1); + } + + if (MayContainCSharpWorkspaceReferenceTargets(content)) + { + extractedByCandidate[candidateIndex] = + SymbolExtractor.Extract( + 0, + "csharp", + content, + target.IndexPath, + cancellationToken: cancellationToken); + } + } } catch (Exception ex) when (ex is FileIndexer.BinaryFileSkippedException or FileIndexer.FileTooLargeSkippedException) @@ -146,7 +163,10 @@ internal static CSharpStaticInterfaceWorkspaceSymbols BuildWorkspaceSymbols( var hasSourceStaticInterfaceContracts = HasCSharpStaticInterfaceContractSymbol(pendingSymbols); var hadPendingContracts = false; var hadPendingMemberReadTargets = false; - var symbols = includeExistingSymbols + var shouldLoadExistingSymbols = includeExistingSymbols + && (!loadExistingSymbolsOnlyForPendingQualifiedMemberAccess + || hasPendingQualifiedMemberAccessCandidate != 0); + var symbols = shouldLoadExistingSymbols ? writer.LoadCSharpStaticInterfaceContractSymbols( pendingPaths!, excludedExistingFileIds, @@ -195,6 +215,7 @@ internal static CSharpStaticInterfaceWorkspaceSymbols BuildWorkspaceSymbols( reportCandidateFile: null, parallelism: 1, excludedExistingFileIds: null, + loadExistingSymbolsOnlyForPendingQualifiedMemberAccess: false, cancellationToken: cancellationToken); } @@ -275,10 +296,13 @@ internal sealed class RawByteContractProbe private bool _hasVirtual; private bool _hasEnum; private bool _hasConst; + private bool _hasDot; private bool _mayContainUtf16; internal bool MayContainContractCandidate => _hasInterface && _hasStatic && (_hasAbstract || _hasVirtual); internal bool MayContainWorkspaceCandidate => _hasStatic || _hasEnum || _hasConst; + internal bool MayContainWorkspaceOrQualifiedMemberAccessCandidate => + MayContainWorkspaceCandidate || _hasDot; internal bool AppendAndCheck(ReadOnlySpan bytes) { @@ -292,6 +316,13 @@ internal bool AppendAndCheckWorkspaceCandidate(ReadOnlySpan bytes) return MayContainWorkspaceCandidate; } + internal bool AppendAndCheckWorkspaceOrQualifiedMemberAccessCandidate( + ReadOnlySpan bytes) + { + Append(bytes); + return MayContainWorkspaceOrQualifiedMemberAccessCandidate; + } + internal void Append(ReadOnlySpan bytes) { if (bytes.Length == 0 || MayContainContractCandidate) @@ -318,6 +349,8 @@ private void Scan(ReadOnlySpan bytes) { if (!_mayContainUtf16 && bytes.IndexOf((byte)0) >= 0) _mayContainUtf16 = true; + if (!_hasDot && bytes.IndexOf((byte)'.') >= 0) + _hasDot = true; if (!_hasInterface) _hasInterface = ContainsAsciiTokenInCommonEncodings(bytes, CSharpInterfaceKeywordBytes, _mayContainUtf16); diff --git a/src/CodeIndex/Indexer/Scanning/FileContentLoader.cs b/src/CodeIndex/Indexer/Scanning/FileContentLoader.cs index 00710b3b6..ffaf001e6 100644 --- a/src/CodeIndex/Indexer/Scanning/FileContentLoader.cs +++ b/src/CodeIndex/Indexer/Scanning/FileContentLoader.cs @@ -107,6 +107,7 @@ internal string LoadNormalizedContentForPrepass( string normalizedRelativePath, string relativePath, bool retryOnMutation, + bool includeQualifiedMemberAccessCandidate, CancellationToken cancellationToken) { var readPath = _resolveFileReadPath(absolutePath); @@ -132,7 +133,10 @@ internal string LoadNormalizedContentForPrepass( stream, initialLength, normalizedRelativePath, - probe.AppendAndCheckWorkspaceCandidate, + includeQualifiedMemberAccessCandidate + ? probe + .AppendAndCheckWorkspaceOrQualifiedMemberAccessCandidate + : probe.AppendAndCheckWorkspaceCandidate, cancellationToken); if (rawCandidate) { diff --git a/src/CodeIndex/Indexer/Scanning/FileIndexer.RecordLoading.cs b/src/CodeIndex/Indexer/Scanning/FileIndexer.RecordLoading.cs index 943f22867..6385c68e5 100644 --- a/src/CodeIndex/Indexer/Scanning/FileIndexer.RecordLoading.cs +++ b/src/CodeIndex/Indexer/Scanning/FileIndexer.RecordLoading.cs @@ -116,6 +116,17 @@ internal string LoadNormalizedContentForPrepass(string absolutePath, string rela string absolutePath, string relativePath, CancellationToken cancellationToken = default) + => LoadCSharpStaticInterfaceCandidateContentForPrepass( + absolutePath, + relativePath, + includeQualifiedMemberAccessCandidate: false, + cancellationToken); + + internal string? LoadCSharpStaticInterfaceCandidateContentForPrepass( + string absolutePath, + string relativePath, + bool includeQualifiedMemberAccessCandidate, + CancellationToken cancellationToken = default) { var normalizedRelativePath = NormalizeIndexPath(relativePath); for (var attempt = 0; ; attempt++) @@ -134,6 +145,7 @@ internal string LoadNormalizedContentForPrepass(string absolutePath, string rela normalizedRelativePath, relativePath, retryOnMutation: attempt == 0, + includeQualifiedMemberAccessCandidate, cancellationToken); if (!requiresRetry) return content; diff --git a/tests/CodeIndex.Tests/IndexCommandRunnerFullScanTests.cs b/tests/CodeIndex.Tests/IndexCommandRunnerFullScanTests.cs index c53a5c740..c1fc4ec86 100644 --- a/tests/CodeIndex.Tests/IndexCommandRunnerFullScanTests.cs +++ b/tests/CodeIndex.Tests/IndexCommandRunnerFullScanTests.cs @@ -4913,6 +4913,9 @@ public static class Values "--quiet", ], _jsonOptions)); + Assert.Equal(1L, CountMemberReadReferences(dbPath, "Limit")); + Assert.Equal(1L, CountMemberReadReferences(dbPath, "Other")); + Assert.Equal(1L, CountMemberReadReferences(dbPath, "Property")); File.WriteAllText( callerPath,