diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md index 90f39433c2..cc40480399 100644 --- a/TESTING_GUIDE.md +++ b/TESTING_GUIDE.md @@ -34,14 +34,16 @@ The test project mirrors the production areas closely. - `ChunkSplitterTests.cs`, `SymbolExtractorTests.cs`, `ReferenceExtractorTests.cs`, `SearchSnippetFormatterTests.cs`, `DbPathResolverTests.cs`, `ConsoleUiTests.cs` Pure or mostly pure behavior tests with in-memory inputs. +- `SymbolExtractor*Tests.cs` and `ReferenceExtractor*Tests.cs` + Extractor coverage is split by language or feature area with partial test classes, while shared helpers remain on the root `SymbolExtractorTests` / `ReferenceExtractorTests` parts. - `FileIndexerTests.cs` File scanning, language detection, and record-building behavior, including extensionless shebang detection's 256-byte first-line cap, binary/NUL-byte rejection, and Windows-only >=260-character path walker/purge coverage. - `DatabaseTests.cs`, `DbReaderTests.cs` SQLite schema, write paths, migrations, and query behavior. - `LegacySchemaMigrationTests.cs` End-to-end upgrade path: seeds a pre-column legacy DB, opens it through `TryMigrateForRead`, and exercises the read paths that touch nullable symbol ordinals (outline, symbol search, nearby, unused, analyze bundle) to lock in the real-world failure mode behind #58 / #49. -- `IndexCommandRunnerTests.cs`, `QueryCommandRunnerTests.cs`, `ProgramCliTests.cs`, `InstallScriptTests.cs` - CLI parsing, command execution, and installer behavior. `ProgramCliTests.cs` covers top-level entrypoint behavior that must be exercised through a subprocess, while `InstallScriptTests.cs` runs focused bash snippets against `install.sh` in library mode to lock in release-installer regressions without performing real network installs. +- `IndexCommandRunnerTests.cs`, `QueryCommandRunner*Tests.cs`, `ProgramCliTests.cs`, `InstallScriptTests.cs` + CLI parsing, command execution, and installer behavior. Query command coverage is split by command family with partial `QueryCommandRunnerTests` classes so shared console and fixture helpers stay centralized. `ProgramCliTests.cs` covers top-level entrypoint behavior that must be exercised through a subprocess, while `InstallScriptTests.cs` runs focused bash snippets against `install.sh` in library mode to lock in release-installer regressions without performing real network installs. - `IndexCommandRunnerTests.Run_CancelDuringFreshIndex_ReturnsInterruptedJson`, `Run_CancelDuringDryRunScan_ReturnsInterruptedJson`, and `Run_CancelBeforeFreshScan_ReturnsInterruptedJson` exercise the same in-process cancellation paths used after Ctrl-C/SIGINT wiring, including scan-time cancellation, so interrupted index runs keep returning the canonical JSON error contract. - `SymbolExtractorTests.Extract_CSharp_InstallScriptFixture_CompletesWithinPracticalBudget` @@ -50,7 +52,6 @@ The test project mirrors the production areas closely. publishes a trimmed RID-specific CLI and runs whichever entry point the SDK emits (`cdidx.dll` through `dotnet` or the native `cdidx`/`cdidx.exe` apphost). It is reported as skipped on macOS arm64 while SDK/ILLink can crash before exercising `cdidx` (#2586). Do not assume every SDK/runtime pair writes a `cdidx.dll` into self-contained publish output. - `QueryCommandRunnerTests.RunPublishedTrimmedCli_SearchSupportsCSharpRazorAliases` uses the same trimmed RID-specific publish path to verify C# Razor language aliases. It is also reported as skipped on macOS arm64 because the SDK/ILLink crash happens before the test reaches `cdidx`. - RID-specific publish helpers must route `NuGetLockFilePath` into the temporary publish output so local test runs do not add runtime sections to source `packages.lock.json` files (#2918). - `McpServerTests.cs` MCP JSON-RPC behavior and tool outputs. - `HttpMcpTransportTests.cs` @@ -235,14 +236,16 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests" - `ChunkSplitterTests.cs`、`SymbolExtractorTests.cs`、`ReferenceExtractorTests.cs`、`SearchSnippetFormatterTests.cs`、`DbPathResolverTests.cs`、`ConsoleUiTests.cs` インメモリ入力中心の、純粋またはほぼ純粋な振る舞いのテスト。 +- `SymbolExtractor*Tests.cs` と `ReferenceExtractor*Tests.cs` + extractor のカバレッジは言語または機能領域ごとの partial test class に分割し、共有 helper は root 側の `SymbolExtractorTests` / `ReferenceExtractorTests` に残します。 - `FileIndexerTests.cs` ファイル走査、言語判定、レコード構築のテスト。拡張子なし shebang 判定の「先頭物理行 256 byte 上限」、binary/NUL byte 除外、Windows 専用の 260 文字以上 path walker/purge カバレッジも含みます。 - `DatabaseTests.cs`、`DbReaderTests.cs` SQLite スキーマ、書き込み経路、マイグレーション、クエリ挙動のテスト。 - `LegacySchemaMigrationTests.cs` エンドツーエンドのアップグレード経路: カラム追加前のレガシー DB を用意し、`TryMigrateForRead` 経由で開いてから NULL になりうるシンボル列を触る read path(outline、シンボル検索、近傍、unused、analyze バンドル)を一通り叩き、#58 / #49 の実機失敗モードを固定する。 -- `IndexCommandRunnerTests.cs`、`QueryCommandRunnerTests.cs`、`ProgramCliTests.cs`、`InstallScriptTests.cs` - CLI の引数解析、コマンド実行、installer 挙動のテスト。`ProgramCliTests.cs` はグローバル引数の解釈や完全な CLI 起動フローのように subprocess 経由で確認すべき Program エントリポイント挙動を扱い、`InstallScriptTests.cs` は `install.sh` を library mode で source した bash snippet を実行して、実ネットワーク install を行わずに release installer の回帰を固定する。 +- `IndexCommandRunnerTests.cs`、`QueryCommandRunner*Tests.cs`、`ProgramCliTests.cs`、`InstallScriptTests.cs` + CLI の引数解析、コマンド実行、installer 挙動のテスト。Query command coverage は command family ごとの partial `QueryCommandRunnerTests` class に分割し、共有 console / fixture helper は一箇所に保ちます。`ProgramCliTests.cs` はグローバル引数の解釈や完全な CLI 起動フローのように subprocess 経由で確認すべき Program エントリポイント挙動を扱い、`InstallScriptTests.cs` は `install.sh` を library mode で source した bash snippet を実行して、実ネットワーク install を行わずに release installer の回帰を固定する。 - `IndexCommandRunnerTests.Run_CancelDuringFreshIndex_ReturnsInterruptedJson`、`Run_CancelDuringDryRunScan_ReturnsInterruptedJson`、`Run_CancelBeforeFreshScan_ReturnsInterruptedJson` Ctrl-C/SIGINT 配線後に使われる in-process cancellation 経路を、scan 中のキャンセルも含めて検証し、interrupted index run が標準の JSON error contract を返し続けることを固定する。 - `SymbolExtractorTests.Extract_CSharp_InstallScriptFixture_CompletesWithinPracticalBudget` @@ -251,7 +254,6 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests" は trimmed な RID 固有 CLI を publish し、SDK が生成した entry point(`dotnet` 経由の `cdidx.dll`、または native の `cdidx`/`cdidx.exe` apphost)を実行します。self-contained publish output に常に `cdidx.dll` が出るとは仮定しないでください。 - `QueryCommandRunnerTests.RunPublishedTrimmedCli_SearchSupportsCSharpRazorAliases` は同じ trimmed RID 固有 publish 経路で C# Razor の言語 alias を検証します。このテストも macOS arm64 では、`cdidx` に到達する前に SDK/ILLink がクラッシュし得るため skipped として報告されます。 - RID 固有 publish helper は `NuGetLockFilePath` を一時 publish 出力へ向け、ローカル test run が source の `packages.lock.json` に runtime section を追加しないようにしてください (#2918)。 - `McpServerTests.cs` MCP の JSON-RPC 挙動とツール出力のテスト。 - `GitHelperTests.cs` diff --git a/changelog.d/unreleased/2817.internal.md b/changelog.d/unreleased/2817.internal.md new file mode 100644 index 0000000000..854b9a9ddd --- /dev/null +++ b/changelog.d/unreleased/2817.internal.md @@ -0,0 +1,18 @@ +--- +category: internal +issues: + - 2817 +affected: + - tests/CodeIndex.Tests/QueryCommandRunnerTests.cs + - tests/CodeIndex.Tests/ReferenceExtractorTests.cs + - tests/CodeIndex.Tests/SymbolExtractorTests.cs + - TESTING_GUIDE.md +--- + +## English + +- **Oversized extractor and query command tests were split by feature area (#2817)** — large test classes now use partial class companions grouped by query command family and extractor language area, reducing noise in navigation and review. + +## 日本語 + +- **巨大化していた extractor / query command テストを機能領域別に分割しました (#2817)** — 大きなテストクラスを query command family と extractor の言語領域ごとの partial class に分け、ナビゲーションとレビュー時のノイズを減らしました。 diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerBoundsTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerBoundsTests.cs new file mode 100644 index 0000000000..10ff1b11b2 --- /dev/null +++ b/tests/CodeIndex.Tests/QueryCommandRunnerBoundsTests.cs @@ -0,0 +1,135 @@ +using CodeIndex.Cli; + +namespace CodeIndex.Tests; + +public partial class QueryCommandRunnerTests +{ + [Theory] + [InlineData("--visibility")] + [InlineData("--exclude-visibility")] + public void ParseArgs_VisibilityFiltersRejectOverlongCsv_Issue2912(string optionName) + { + var tooLong = new string('p', QueryCommandRunner.MaxVisibilityFilterCsvLength + 1); + + var options = QueryCommandRunner.ParseArgs( + ["RunSearch", optionName, tooLong], + jsonDefault: false, + allowNamedQuery: true); + + Assert.Contains($"{optionName} value is too long", options.ParseError); + Assert.Empty(options.VisibilityFilters); + Assert.Empty(options.ExcludeVisibilityFilters); + } + + [Theory] + [InlineData("--visibility")] + [InlineData("--exclude-visibility")] + public void ParseArgs_VisibilityFiltersRejectTooManyCsvEntries_Issue2912(string optionName) + { + var tooMany = string.Join(',', Enumerable.Repeat("public", QueryCommandRunner.MaxVisibilityFilterCsvEntries + 1)); + + var options = QueryCommandRunner.ParseArgs( + ["RunSearch", optionName, tooMany], + jsonDefault: false, + allowNamedQuery: true); + + Assert.Contains($"{optionName} accepts at most", options.ParseError); + Assert.Empty(options.VisibilityFilters); + Assert.Empty(options.ExcludeVisibilityFilters); + } + + [Fact] + public void ParseArgs_StatusCheckScopesRejectsOverlongCsv_Issue2913() + { + var tooLong = new string('w', QueryCommandRunner.MaxStatusCheckScopesCsvLength + 1); + + var options = QueryCommandRunner.ParseArgs( + [$"--check={tooLong}"], + jsonDefault: false, + allowStatusCheck: true); + + Assert.True(options.CheckWorkspace); + Assert.Contains("--check value is too long", options.ParseError); + Assert.Null(options.StatusCheckScopes); + } + + [Fact] + public void ParseArgs_StatusCheckScopesRejectsTooManyCsvEntries_Issue2913() + { + var tooMany = string.Join(',', Enumerable.Repeat("workspace", QueryCommandRunner.MaxStatusCheckScopesCsvEntries + 1)); + + var options = QueryCommandRunner.ParseArgs( + [$"--check={tooMany}"], + jsonDefault: false, + allowStatusCheck: true); + + Assert.True(options.CheckWorkspace); + Assert.Contains("--check accepts at most", options.ParseError); + Assert.Null(options.StatusCheckScopes); + } + + [Theory] + [InlineData("--path")] + [InlineData("--exclude-path")] + public void ParseArgs_PathFiltersRejectTooManyValues_Issue2911(string optionName) + { + var args = new List { "RunSearch" }; + for (var i = 0; i <= QueryCommandRunner.MaxQueryPathFilterCount; i++) + { + args.Add(optionName); + args.Add("src/**"); + } + + var options = QueryCommandRunner.ParseArgs( + args.ToArray(), + jsonDefault: false, + allowNamedQuery: true); + + Assert.Contains($"{optionName} accepts at most", options.ParseError); + } + + [Theory] + [InlineData("--path")] + [InlineData("--exclude-path")] + public void ParseArgs_PathFiltersRejectOverlongPattern_Issue2911(string optionName) + { + var tooLong = new string('a', QueryCommandRunner.MaxQueryPathFilterLength + 1); + + var options = QueryCommandRunner.ParseArgs( + ["RunSearch", optionName, tooLong], + jsonDefault: false, + allowNamedQuery: true); + + Assert.Contains($"{optionName} value is too long", options.ParseError); + } + + [Fact] + public void ParseArgs_MapSectionsRejectsOverlongCsv_Issue2914() + { + var tooLong = new string('t', QueryCommandRunner.MaxMapSectionsCsvLength + 1); + + var options = QueryCommandRunner.ParseArgs( + ["--sections", tooLong], + jsonDefault: false, + validateDefaultSnippetLines: false, + validateDefaultMaxLineWidth: false); + + Assert.Contains("--sections value is too long", options.ParseError); + Assert.Empty(options.MapSections!); + } + + [Fact] + public void ParseArgs_MapSectionsRejectsTooManyCsvEntries_Issue2914() + { + var tooMany = string.Join(',', Enumerable.Repeat("tree", QueryCommandRunner.MaxMapSectionsCsvEntries + 1)); + + var options = QueryCommandRunner.ParseArgs( + ["--sections", tooMany], + jsonDefault: false, + validateDefaultSnippetLines: false, + validateDefaultMaxLineWidth: false); + + Assert.Contains("--sections accepts at most", options.ParseError); + Assert.Empty(options.MapSections!); + } +} diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerFilesTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerFilesTests.cs new file mode 100644 index 0000000000..bf2732ee09 --- /dev/null +++ b/tests/CodeIndex.Tests/QueryCommandRunnerFilesTests.cs @@ -0,0 +1,2039 @@ +using System.Text.Json; +using CodeIndex.Cli; +using CodeIndex.Database; +using CodeIndex.Models; +using Microsoft.Data.Sqlite; + +namespace CodeIndex.Tests; + +public partial class QueryCommandRunnerTests +{ + [Fact] + public void RunFiles_MissingSinceValueShowsPerFlagHint_Issue1507() + { + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunFiles(["--since"], _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("Error: --since requires a value.", stderr); + Assert.Contains("Hint: pass an ISO 8601 datetime", stderr); + Assert.Contains("--since 2024-01-01", stderr); + } + + [Fact] + public void RunFiles_PathFilterAcceptsLeadingDashValue() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_files_path_leading_dash"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFiles( + ["--db", dbPath, "--path", "-foo", "--count"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal("0", stdout.Trim()); + Assert.Equal(string.Empty, stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunFiles_ExcludePathFilterAcceptsLeadingDashValue() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_files_exclude_path_leading_dash"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFiles( + ["--db", dbPath, "--exclude-path", "-foo", "--count"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal("0", stdout.Trim()); + Assert.Equal(string.Empty, stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunFiles_PathFilterAcceptsRecognizedOptionTokenViaInlineValue() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_files_path_inline_recognized_option"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/--json-dir/Demo.cs", + "csharp", + "class Demo {}\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFiles( + [$"--db={dbPath}", "--path=--json-dir", "--count", "--json"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + using var document = ParseJsonOutput(stdout); + Assert.Equal(1, document.RootElement.GetProperty("count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunFiles_ExcludePathFilterAcceptsRecognizedOptionTokenViaInlineValue() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_files_exclude_path_inline_recognized_option"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/--count-dir/Demo.cs", + "csharp", + "class Demo {}\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFiles( + [$"--db={dbPath}", "--exclude-path=--count-dir", "--count", "--json"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + using var document = ParseJsonOutput(stdout); + Assert.Equal(0, document.RootElement.GetProperty("count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunStatus_Json_ReportsHotspotFamilyTrustSignals() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_status_hotspots_family_signal"); + try + { + var dbPath = CreateHotspotFamilyFixtureDb(projectRoot, markHotspotFamilyReady: false); + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.False(json.GetProperty("hotspot_family_ready").GetBoolean()); + Assert.Contains("csharp", json.GetProperty("hotspot_family_degraded_reason").GetString()); + Assert.Contains("DEGRADED", json.GetProperty("summary").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunStatus_Json_ReportsFoldOnlyRemediationHint() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_status_fold_only_json"); + try + { + var sourcePath = Path.Combine(projectRoot, "app.cs"); + File.WriteAllText(sourcePath, "public class App { public void Run() { } }\n"); + + Assert.Equal(CommandExitCodes.Success, IndexCommandRunner.Run([projectRoot, "--json", "--quiet"], _jsonOptions)); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + using (var db = new DbContext(dbPath)) + { + using var cmd = db.Connection.CreateCommand(); + cmd.CommandText = "UPDATE codeindex_meta SET value = '0' WHERE key = 'fold_key_version'"; + cmd.ExecuteNonQuery(); + } + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.False(json.GetProperty("fold_ready").GetBoolean()); + Assert.Equal("stale_fold_key_version", json.GetProperty("fold_ready_reason").GetString()); + Assert.Contains("older fold-key version", json.GetProperty("degraded_reason").GetString()); + Assert.Contains("cdidx backfill-fold --db", json.GetProperty("recommended_action").GetString()); + Assert.Contains("--rebuild", json.GetProperty("alternative_action").GetString()); + Assert.Contains("DEGRADED", json.GetProperty("summary").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunStatus_Json_ReportsWorktreeHeadChangedWhenIndexedHeadDiffersFromRuntime() + { + // #1512: after `git worktree add` / `git switch` inside a worktree, the runtime HEAD + // diverges from the HEAD captured at index time. status JSON must surface that so MCP / + // automation clients can warn before issuing further queries against a stale index. + // #1512: worktree branch / HEAD 切替後、runtime HEAD は index 時点の HEAD と乖離する。 + // status JSON でこれを surface し、後続クエリ前に stale を検知可能にする。 + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_status_worktree_head_changed_json"); + try + { + TestProjectHelper.InitializeGitRepo(projectRoot); + File.WriteAllText(Path.Combine(projectRoot, "app.cs"), "public class App {}\n"); + TestProjectHelper.RunGit(projectRoot, "add", "app.cs"); + TestProjectHelper.RunGit(projectRoot, "commit", "-m", "initial"); + + Assert.Equal(CommandExitCodes.Success, IndexCommandRunner.Run([projectRoot, "--json", "--quiet"], _jsonOptions)); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var staleHead = new string('b', 40); + using (var db = new DbContext(dbPath)) + { + var writer = new DbWriter(db.Connection); + writer.SetMeta(DbContext.IndexedHeadCommitMetaKey, staleHead); + } + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(staleHead, json.GetProperty("indexed_head_commit").GetString()); + Assert.True(json.GetProperty("worktree_head_changed").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunStatus_Json_ReportsWorktreeHeadChangedWhenBranchBecomesDetachedAtSameCommit() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_status_detached_head_changed_json"); + try + { + TestProjectHelper.InitializeGitRepo(projectRoot); + File.WriteAllText(Path.Combine(projectRoot, "app.cs"), "public class App {}\n"); + TestProjectHelper.RunGit(projectRoot, "add", "app.cs"); + TestProjectHelper.RunGit(projectRoot, "commit", "-m", "initial"); + + Assert.Equal(CommandExitCodes.Success, IndexCommandRunner.Run([projectRoot, "--json", "--quiet"], _jsonOptions)); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var indexedHead = TestProjectHelper.RunGit(projectRoot, "rev-parse", "HEAD").Trim(); + var indexedBranch = TestProjectHelper.RunGit(projectRoot, "rev-parse", "--abbrev-ref", "HEAD").Trim(); + TestProjectHelper.RunGit(projectRoot, "checkout", "--detach", indexedHead); + + using (var db = new DbContext(dbPath)) + { + var writer = new DbWriter(db.Connection); + writer.SetMeta(DbContext.IndexedHeadCommitMetaKey, indexedHead); + writer.SetMeta(DbContext.IndexedHeadCommitBranchMetaKey, indexedBranch); + } + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(indexedHead, json.GetProperty("indexed_head_commit").GetString()); + Assert.True(json.GetProperty("worktree_head_changed").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunStatus_Json_ReportsDetachedHeadAfterPartialUpdateAtSameCommit() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_status_detached_head_after_partial_json"); + try + { + TestProjectHelper.InitializeGitRepo(projectRoot); + File.WriteAllText(Path.Combine(projectRoot, "app.cs"), "public class App {}\n"); + TestProjectHelper.RunGit(projectRoot, "add", "app.cs"); + TestProjectHelper.RunGit(projectRoot, "commit", "-m", "initial"); + + Assert.Equal(CommandExitCodes.Success, IndexCommandRunner.Run([projectRoot, "--json", "--quiet"], _jsonOptions)); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var indexedHead = TestProjectHelper.RunGit(projectRoot, "rev-parse", "HEAD").Trim(); + TestProjectHelper.RunGit(projectRoot, "checkout", "--detach", indexedHead); + File.WriteAllText(Path.Combine(projectRoot, "app.cs"), "public class App { public void Run() {} }\n"); + + Assert.Equal(CommandExitCodes.Success, IndexCommandRunner.Run([projectRoot, "--files", "app.cs", "--json", "--quiet"], _jsonOptions)); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(indexedHead, json.GetProperty("indexed_head_commit").GetString()); + Assert.True(json.GetProperty("worktree_head_changed").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunStatus_HumanOutput_WarnsWhenWorktreeHeadHasSwitchedSinceIndex() + { + // #1512: surface a `WARN` line plus an Indexed-HEAD echo and a ready-to-run reindex + // command when the runtime HEAD no longer matches the HEAD captured at index time. + // #1512: index 時点と runtime の HEAD が異なるとき、`WARN` 行と Indexed-HEAD・再 index コマンド + // を出力する。 + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_status_worktree_head_changed_human"); + try + { + TestProjectHelper.InitializeGitRepo(projectRoot); + File.WriteAllText(Path.Combine(projectRoot, "app.cs"), "public class App {}\n"); + TestProjectHelper.RunGit(projectRoot, "add", "app.cs"); + TestProjectHelper.RunGit(projectRoot, "commit", "-m", "initial"); + + Assert.Equal(CommandExitCodes.Success, IndexCommandRunner.Run([projectRoot, "--json", "--quiet"], _jsonOptions)); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var staleHead = new string('c', 40); + using (var db = new DbContext(dbPath)) + { + var writer = new DbWriter(db.Connection); + writer.SetMeta(DbContext.IndexedHeadCommitMetaKey, staleHead); + } + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", dbPath], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Contains("WARN : worktree HEAD changed since the index was built", stdout); + Assert.Contains(staleHead[..12], stdout); + Assert.Contains("cdidx index", stdout); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunStatus_HumanOutput_WarnsWhenOnlyFoldReadinessIsDegraded() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_status_fold_only_human"); + try + { + var sourcePath = Path.Combine(projectRoot, "app.cs"); + File.WriteAllText(sourcePath, "public class App { public void Run() { } }\n"); + + Assert.Equal(CommandExitCodes.Success, IndexCommandRunner.Run([projectRoot, "--json", "--quiet"], _jsonOptions)); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + using (var db = new DbContext(dbPath)) + { + using var cmd = db.Connection.CreateCommand(); + cmd.CommandText = "UPDATE codeindex_meta SET value = '0' WHERE key = 'fold_key_version'"; + cmd.ExecuteNonQuery(); + } + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", dbPath], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Contains("older fold-key version", stdout); + Assert.Contains("Hint : run `cdidx backfill-fold --db", stdout); + Assert.Contains("Hint : or run `cdidx index", stdout); + Assert.Contains("--rebuild", stdout); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunStatus_Json_ReadOnlyUriFoldRemediationUsesWritableDbPath() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_status_fold_only_uri"); + try + { + var sourcePath = Path.Combine(projectRoot, "app.cs"); + File.WriteAllText(sourcePath, "public class App { public void Run() { } }\n"); + + Assert.Equal(CommandExitCodes.Success, IndexCommandRunner.Run([projectRoot, "--json", "--quiet"], _jsonOptions)); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + using (var db = new DbContext(dbPath)) + { + using var cmd = db.Connection.CreateCommand(); + cmd.CommandText = "UPDATE codeindex_meta SET value = '0' WHERE key = 'fold_key_version'"; + cmd.ExecuteNonQuery(); + + using var checkpoint = db.Connection.CreateCommand(); + checkpoint.CommandText = "PRAGMA wal_checkpoint(TRUNCATE);"; + checkpoint.ExecuteNonQuery(); + } + SqliteConnection.ClearAllPools(); + + var readOnlyUri = new Uri(dbPath).AbsoluteUri + "?immutable=1"; + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", readOnlyUri, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("stale_fold_key_version", json.GetProperty("fold_ready_reason").GetString()); + Assert.Contains(dbPath, json.GetProperty("recommended_action").GetString()); + Assert.Contains(dbPath, json.GetProperty("alternative_action").GetString()); + Assert.DoesNotContain("immutable=1", json.GetProperty("recommended_action").GetString()); + Assert.DoesNotContain("immutable=1", json.GetProperty("alternative_action").GetString()); + Assert.DoesNotContain("file:", json.GetProperty("recommended_action").GetString()); + Assert.DoesNotContain("file:", json.GetProperty("alternative_action").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + SqliteConnection.ClearAllPools(); + } + } + + [Fact] + public void RunStatus_Json_RelativeReadOnlyUriFoldRemediationUsesWorkingDirectoryDbPath() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_status_fold_only_relative_uri_json"); + try + { + var sourcePath = Path.Combine(projectRoot, "app.cs"); + File.WriteAllText(sourcePath, "public class App { public void Run() { } }\n"); + + Assert.Equal(CommandExitCodes.Success, IndexCommandRunner.Run([projectRoot, "--json", "--quiet"], _jsonOptions)); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var dbDirectory = Path.GetDirectoryName(dbPath)!; + using (var db = new DbContext(dbPath)) + { + using var cmd = db.Connection.CreateCommand(); + cmd.CommandText = "UPDATE codeindex_meta SET value = '0' WHERE key = 'fold_key_version'"; + cmd.ExecuteNonQuery(); + + using var checkpoint = db.Connection.CreateCommand(); + checkpoint.CommandText = "PRAGMA wal_checkpoint(TRUNCATE);"; + checkpoint.ExecuteNonQuery(); + } + SqliteConnection.ClearAllPools(); + + var (exitCode, stdout, stderr) = RunBuiltCli( + ["status", "--db", "file:codeindex.db?immutable=1", "--json"], + dbDirectory); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("stale_fold_key_version", json.GetProperty("fold_ready_reason").GetString()); + Assert.Contains(dbPath, json.GetProperty("recommended_action").GetString()); + Assert.Contains(dbPath, json.GetProperty("alternative_action").GetString()); + Assert.DoesNotContain("", json.GetProperty("recommended_action").GetString()); + Assert.DoesNotContain("", json.GetProperty("alternative_action").GetString()); + Assert.DoesNotContain("file:", json.GetProperty("recommended_action").GetString()); + Assert.DoesNotContain("file:", json.GetProperty("alternative_action").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + SqliteConnection.ClearAllPools(); + } + } + + [Fact] + public void RunStatus_HumanOutput_RelativeReadOnlyUriUsesWorkingDirectoryDbPath() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_status_fold_only_relative_uri_human"); + try + { + var sourcePath = Path.Combine(projectRoot, "app.cs"); + File.WriteAllText(sourcePath, "public class App { public void Run() { } }\n"); + + Assert.Equal(CommandExitCodes.Success, IndexCommandRunner.Run([projectRoot, "--json", "--quiet"], _jsonOptions)); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var dbDirectory = Path.GetDirectoryName(dbPath)!; + using (var db = new DbContext(dbPath)) + { + using var cmd = db.Connection.CreateCommand(); + cmd.CommandText = "UPDATE codeindex_meta SET value = '0' WHERE key = 'fold_key_version'"; + cmd.ExecuteNonQuery(); + + using var checkpoint = db.Connection.CreateCommand(); + checkpoint.CommandText = "PRAGMA wal_checkpoint(TRUNCATE);"; + checkpoint.ExecuteNonQuery(); + } + SqliteConnection.ClearAllPools(); + + var (exitCode, stdout, stderr) = RunBuiltCli( + ["status", "--db", "file:codeindex.db?mode=ro"], + dbDirectory); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Contains(dbPath, stdout); + Assert.Contains("cdidx backfill-fold --db", stdout); + Assert.Contains("cdidx index", stdout); + Assert.DoesNotContain("", stdout); + Assert.DoesNotContain("file:codeindex.db", stdout); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + SqliteConnection.ClearAllPools(); + } + } + + [Fact] + public void RunStatus_HumanOutput_WarnsWhenHotspotFamilyTrustIsDegraded() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_status_hotspots_family_human"); + try + { + var dbPath = CreateHotspotFamilyFixtureDb(projectRoot, markHotspotFamilyReady: false); + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", dbPath], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Contains("cross-file hotspot family grouping", stdout); + Assert.Contains("authoritative cross-file hotspot families", stdout); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunStatus_ReportsDegradedSqlGraphContractTrust() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_status_sql_graph_contract"); + try + { + var dbPath = CreateSqlGraphContractFixtureDb(projectRoot); + DowngradeSqlGraphContractRows(dbPath); + + var (jsonExitCode, jsonStdout, jsonStderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", dbPath, "--json"], + _jsonOptions)); + + using (var jsonDocument = ParseJsonOutput(jsonStdout)) + { + var json = jsonDocument.RootElement; + Assert.Equal(CommandExitCodes.Success, jsonExitCode); + Assert.Equal(string.Empty, jsonStderr); + Assert.False(json.GetProperty("sql_graph_contract_ready").GetBoolean()); + Assert.Contains("sql_graph_contract_ready=false", json.GetProperty("sql_graph_contract_degraded_reason").GetString()); + Assert.Contains("DEGRADED", json.GetProperty("summary").GetString()); + } + + var (humanExitCode, humanStdout, humanStderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", dbPath], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, humanExitCode); + Assert.Equal(string.Empty, humanStderr); + Assert.Contains("SQL graph/dependency results may be stale.", humanStdout); + Assert.Contains(Path.GetFullPath(projectRoot), humanStdout); + Assert.Contains(Path.GetFullPath(dbPath), humanStdout); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunFiles_ZeroJson_OnEmptyIndex_EmitsNullIndexedAt() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_zero_json_empty_index"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFiles( + ["definitely-missing-path", "--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Equal(0, json.GetProperty("files").GetArrayLength()); + Assert.Equal(0, json.GetProperty("indexed_file_count").GetInt64()); + Assert.True(json.GetProperty("freshness_available").GetBoolean()); + Assert.True(json.TryGetProperty("indexed_at", out var indexedAt)); + Assert.Equal(JsonValueKind.Null, indexedAt.ValueKind); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunFiles_ZeroJson_OnLegacyReadOnlyDb_EmitsFreshnessDegradedSignal() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_zero_json_legacy_freshness"); + try + { + var dbPath = CreateLegacyDbWithoutIndexedAt(projectRoot); + var readOnlyUri = new Uri(dbPath).AbsoluteUri + "?immutable=1"; + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFiles( + ["definitely-missing-path", "--db", readOnlyUri, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Equal(0, json.GetProperty("files").GetArrayLength()); + Assert.Equal(1, json.GetProperty("indexed_file_count").GetInt64()); + Assert.False(json.GetProperty("freshness_available").GetBoolean()); + Assert.Contains("files.indexed_at column missing", json.GetProperty("freshness_degraded_reason").GetString()); + Assert.Equal(JsonValueKind.Null, json.GetProperty("indexed_at").ValueKind); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunFiles_ZeroResultJson_EmitsStructuredPayloadWithFreshnessHint() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_files_zero_json"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", "class App {}\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFiles( + ["missing-file-fragment", "--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Equal(0, json.GetProperty("files").GetArrayLength()); + Assert.Equal(1, json.GetProperty("indexed_file_count").GetInt32()); + Assert.True(json.TryGetProperty("indexed_at", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunFiles_ZeroResultJson_EmptyIndexEmitsNullIndexedAt() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_files_zero_json_empty"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFiles( + ["missing-file-fragment", "--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Equal(0, json.GetProperty("files").GetArrayLength()); + Assert.Equal(0, json.GetProperty("indexed_file_count").GetInt32()); + Assert.Equal(JsonValueKind.Null, json.GetProperty("indexed_at").ValueKind); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunFiles_ZeroResultJson_CountOnlyEmitsFreshnessHint() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_files_zero_json_count"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", "class App {}\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFiles( + ["missing-file-fragment", "--db", dbPath, "--json", "--count"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Equal(1, json.GetProperty("indexed_file_count").GetInt32()); + Assert.True(json.TryGetProperty("indexed_at", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunFiles_ZeroResultJson_CountOnlyEmptyIndexEmitsNullIndexedAt() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_files_zero_json_count_empty"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFiles( + ["missing-file-fragment", "--db", dbPath, "--json", "--count"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Equal(0, json.GetProperty("indexed_file_count").GetInt32()); + Assert.Equal(JsonValueKind.Null, json.GetProperty("indexed_at").ValueKind); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunStatus_HumanReadableIncludesGitMetadata() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_status"); + try + { + TestProjectHelper.InitializeGitRepo(projectRoot); + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + var sourcePath = Path.Combine(projectRoot, "src", "app.cs"); + File.WriteAllText(sourcePath, "class App {}\n"); + TestProjectHelper.RunGit(projectRoot, "add", "src/app.cs"); + TestProjectHelper.RunGit(projectRoot, "commit", "-m", "initial"); + + var expectedHead = TestProjectHelper.RunGit(projectRoot, "rev-parse", "HEAD").Trim(); + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", "class App {}\n"); + + File.WriteAllText(sourcePath, "class App { void Run() {} }\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", dbPath], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Contains("Files : 1", stdout); + Assert.Contains($"Git HEAD : {expectedHead}", stdout); + Assert.Contains("Git Dirty: True", stdout); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunStatus_HumanOutput_TranslatesReadinessFields() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_status_readiness"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", "class App {}\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", dbPath], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Contains("Readiness:", stdout); + Assert.Contains("Reference graph table", stdout); + Assert.Contains("Validation issues data", stdout); + Assert.Contains("Unicode exact-name fold contract", stdout); + Assert.Contains("C# metadata target contract", stdout); + Assert.Contains("cdidx backfill-fold", stdout); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunStatus_Explain_PrintsReadinessFieldDescriptionWithoutDatabase() + { + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--explain", "fold_ready"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Contains("Unicode exact-name fold contract (fold_ready)", stdout); + Assert.Contains("Ready:", stdout); + Assert.Contains("Degraded:", stdout); + Assert.Contains("Remediation:", stdout); + Assert.Contains("cdidx backfill-fold", stdout); + } + + [Fact] + public void RunStatus_Explain_RejectsUnknownReadinessField() + { + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--explain", "nope"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Equal(string.Empty, stdout); + Assert.Contains("unknown status readiness field", stderr); + Assert.Contains("fold_ready", stderr); + } + + [Fact] + public void RunStatus_Explain_RejectsJsonMode() + { + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--explain", "fold_ready", "--json"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Equal(string.Empty, stdout); + Assert.Contains("cannot be combined with --json", stderr); + Assert.Contains("status --json", stderr); + } + + [Theory] + [InlineData("~/cdidx-logs", "cdidx-logs")] + [InlineData("$HOME/cdidx-logs", "cdidx-logs")] + [InlineData("${HOME}/cdidx-logs", "cdidx-logs")] + public void RunStatus_LogPath_ExpandsUserHomeOverrides(string overrideValue, string childDirectory) + { + using var env = EnvironmentVariableScope.Capture( + "CDIDX_GLOBAL_TOOL_LOG_DIR", + "XDG_STATE_HOME", + "XDG_CACHE_HOME", + "XDG_RUNTIME_DIR"); + env.Set("CDIDX_GLOBAL_TOOL_LOG_DIR", overrideValue); + env.Set("XDG_STATE_HOME", null); + env.Set("XDG_CACHE_HOME", null); + env.Set("XDG_RUNTIME_DIR", null); + var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--log-path"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(Path.GetFullPath(Path.Combine(home, childDirectory)), stdout.Trim()); + } + + [Fact] + public void RunStatus_LogPath_JsonPrintsResolvedDirectoryWithoutDatabase() + { + var logDir = Path.Combine(Path.GetTempPath(), $"cdidx_status_log_path_{Guid.NewGuid():N}"); + using var env = EnvironmentVariableScope.Capture( + "CDIDX_GLOBAL_TOOL_LOG_DIR", + "XDG_STATE_HOME", + "XDG_CACHE_HOME", + "XDG_RUNTIME_DIR"); + env.Set("CDIDX_GLOBAL_TOOL_LOG_DIR", logDir); + env.Set("XDG_STATE_HOME", null); + env.Set("XDG_CACHE_HOME", null); + env.Set("XDG_RUNTIME_DIR", null); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--log-path", "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(Path.GetFullPath(logDir), document.RootElement.GetProperty("log_path").GetString()); + } + + [Fact] + public void RunStatus_LogPath_JsonHonorsXdgCacheHome() + { + using var env = EnvironmentVariableScope.Capture( + "CDIDX_GLOBAL_TOOL_LOG_DIR", + "XDG_STATE_HOME", + "XDG_CACHE_HOME", + "XDG_RUNTIME_DIR"); + var cacheHome = Path.Combine(Path.GetTempPath(), $"cdidx_status_log_path_xdg_cache_{Guid.NewGuid():N}"); + try + { + env.Set("CDIDX_GLOBAL_TOOL_LOG_DIR", null); + env.Set("XDG_STATE_HOME", null); + env.Set("XDG_CACHE_HOME", cacheHome); + env.Set("XDG_RUNTIME_DIR", Path.Combine(Path.GetTempPath(), "ignored-runtime")); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--log-path", "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(Path.Combine(cacheHome, "cdidx", "logs"), document.RootElement.GetProperty("log_path").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(cacheHome); + } + } + + [Fact] + public void RunStatus_Json_UsesIndexedAndSourceFreshnessInsteadOfClockAge() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_status_freshness"); + try + { + TestProjectHelper.InitializeGitRepo(projectRoot); + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + var sourcePath = Path.Combine(projectRoot, "src", "app.cs"); + File.WriteAllText(sourcePath, "class App {}\n"); + TestProjectHelper.RunGit(projectRoot, "add", "src/app.cs"); + TestProjectHelper.RunGit(projectRoot, "commit", "-m", "initial"); + + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + var modified = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc); + var indexedAt = new DateTime(2025, 1, 2, 0, 0, 0, DateTimeKind.Utc); + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", "class App {}\n", modified); + using (var db = new DbContext(dbPath)) + { + using var cmd = db.Connection.CreateCommand(); + cmd.CommandText = "UPDATE files SET indexed_at = @indexed_at WHERE path = @path"; + cmd.Parameters.AddWithValue("@indexed_at", indexedAt); + cmd.Parameters.AddWithValue("@path", "src/app.cs"); + cmd.ExecuteNonQuery(); + } + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Contains("index fresh", json.GetProperty("summary").GetString()); + Assert.DoesNotContain("index stale", json.GetProperty("summary").GetString()); + var pragmas = json.GetProperty("db_pragma_settings"); + Assert.Equal("wal", pragmas.GetProperty("journal_mode").GetString()); + Assert.Equal(DbContext.DefaultSynchronousMode, pragmas.GetProperty("synchronous").GetString()); + Assert.Equal(DbContext.DefaultWalAutocheckpointPages, pragmas.GetProperty("wal_autocheckpoint").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunStatus_Json_UsesSourceNewerThanIndexAsStale() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_status_stale"); + try + { + TestProjectHelper.InitializeGitRepo(projectRoot); + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + var sourcePath = Path.Combine(projectRoot, "src", "app.cs"); + File.WriteAllText(sourcePath, "class App {}\n"); + TestProjectHelper.RunGit(projectRoot, "add", "src/app.cs"); + TestProjectHelper.RunGit(projectRoot, "commit", "-m", "initial"); + + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + var modified = new DateTime(2025, 1, 2, 0, 0, 0, DateTimeKind.Utc); + var indexedAt = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc); + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", "class App {}\n", modified); + using (var db = new DbContext(dbPath)) + { + using var cmd = db.Connection.CreateCommand(); + cmd.CommandText = "UPDATE files SET indexed_at = @indexed_at WHERE path = @path"; + cmd.Parameters.AddWithValue("@indexed_at", indexedAt); + cmd.Parameters.AddWithValue("@path", "src/app.cs"); + cmd.ExecuteNonQuery(); + } + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Contains("index stale", json.GetProperty("summary").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunStatus_Json_ReportsStructuredGuidanceForMultipleReadinessDegradations() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_status_multi_degraded"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", "class App {}\n"); + using (var db = new DbContext(dbPath)) + { + var writer = new DbWriter(db.Connection); + writer.MarkBatchInProgress(); + } + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + var degradations = json.GetProperty("readiness_degradations"); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Contains("Last batch did not complete", stderr); + Assert.True(json.GetProperty("migration_in_progress").GetBoolean()); + Assert.True(json.GetProperty("issues_table_available").GetBoolean()); + Assert.False(json.GetProperty("file_issues_data_current").GetBoolean()); + Assert.Equal("migration_in_progress", json.GetProperty("degraded_root_cause").GetString()); + Assert.Contains("DEGRADED", json.GetProperty("summary").GetString()); + Assert.Contains(degradations.EnumerateArray(), item => + item.GetProperty("field").GetString() == "migration_in_progress" + && item.GetProperty("root_cause").GetString() == "migration_in_progress"); + Assert.Contains(degradations.EnumerateArray(), item => + item.GetProperty("field").GetString() == "file_issues_data_current" + && item.GetProperty("root_cause").GetString() == "file_issues_data_current=false"); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunStatus_CheckJson_ReturnsSuccessWhenIndexMatchesWorkspace() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_status_check_match"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText(Path.Combine(projectRoot, "src", "app.cs"), "class App {}\n"); + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", "class App {}\n"); + MarkStatusReadinessReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", dbPath, "--check", "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + var check = json.GetProperty("workspace_check"); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.True(json.GetProperty("index_matches_workspace").GetBoolean()); + Assert.True(check.GetProperty("checked").GetBoolean()); + Assert.True(check.GetProperty("matches_workspace").GetBoolean()); + Assert.Equal("matched", check.GetProperty("reason").GetString()); + Assert.Equal(1, check.GetProperty("matched_file_count").GetInt32()); + Assert.Contains("index fresh", json.GetProperty("summary").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunStatus_CheckJson_ReportsEffectiveStaleThreshold() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_status_stale_after_json"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText(Path.Combine(projectRoot, "src", "app.cs"), "class App {}\n"); + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", "class App {}\n"); + MarkStatusReadinessReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", dbPath, "--check", "--stale-after", "30m", "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(30 * 60, json.GetProperty("stale_after_seconds").GetInt64()); + Assert.True(json.GetProperty("index_age_seconds").GetInt64() >= 0); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunStatus_CheckHuman_PrintsEffectiveStaleThreshold() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_status_stale_after_human"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText(Path.Combine(projectRoot, "src", "app.cs"), "class App {}\n"); + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", "class App {}\n"); + MarkStatusReadinessReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", dbPath, "--check", "--stale-after", "7d"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Contains("threshold: 7d", stdout); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunStatus_Json_IgnoresInvalidStaleAfterEnvironmentWithoutCheck() + { + var prior = Environment.GetEnvironmentVariable(QueryCommandRunner.StaleAfterEnvironmentVariable); + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_status_stale_after_env_plain"); + try + { + Environment.SetEnvironmentVariable(QueryCommandRunner.StaleAfterEnvironmentVariable, "bad-value"); + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", "class App {}\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.False(json.TryGetProperty("stale_after_seconds", out _)); + Assert.False(json.TryGetProperty("index_age_seconds", out _)); + } + finally + { + Environment.SetEnvironmentVariable(QueryCommandRunner.StaleAfterEnvironmentVariable, prior); + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunStatus_CheckJson_ReturnsStaleIndexWhenContentChecksumDiffers() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_status_check_changed"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + var sourcePath = Path.Combine(projectRoot, "src", "app.cs"); + File.WriteAllText(sourcePath, "class App {}\n"); + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", "class App {}\n"); + MarkStatusReadinessReady(dbPath); + File.WriteAllText(sourcePath, "class App { void Run() {} }\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", dbPath, "--check", "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + var check = json.GetProperty("workspace_check"); + + Assert.Equal(1, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.False(json.GetProperty("index_matches_workspace").GetBoolean()); + Assert.Equal("workspace_stale", json.GetProperty("failed_checks")[0].GetString()); + Assert.True(check.GetProperty("checked").GetBoolean()); + Assert.False(check.GetProperty("matches_workspace").GetBoolean()); + Assert.Equal("changed_files", check.GetProperty("reason").GetString()); + Assert.Equal(1, check.GetProperty("changed_file_count").GetInt32()); + Assert.Equal("src/app.cs", check.GetProperty("changed_files")[0].GetString()); + Assert.Contains("index stale", json.GetProperty("summary").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunStatus_CheckJson_DetectsMissingAndUnindexedFiles() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_status_check_paths"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText(Path.Combine(projectRoot, "src", "new.cs"), "class NewFile {}\n"); + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/old.cs", "csharp", "class OldFile {}\n"); + MarkStatusReadinessReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", dbPath, "--check", "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var check = document.RootElement.GetProperty("workspace_check"); + + Assert.Equal(1, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.False(document.RootElement.GetProperty("index_matches_workspace").GetBoolean()); + Assert.Equal("workspace_stale", document.RootElement.GetProperty("failed_checks")[0].GetString()); + Assert.Equal(1, check.GetProperty("missing_file_count").GetInt32()); + Assert.Equal(1, check.GetProperty("unindexed_file_count").GetInt32()); + Assert.Equal("src/old.cs", check.GetProperty("missing_files")[0].GetString()); + Assert.Equal("src/new.cs", check.GetProperty("unindexed_files")[0].GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunStatus_CheckJson_ReclassifiesSkipWorktreePathsAsOutsideSparseCone() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_status_check_sparse_cone"); + try + { + TestProjectHelper.InitializeGitRepo(projectRoot); + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + var insidePath = Path.Combine(projectRoot, "src", "inside.cs"); + var outsidePath = Path.Combine(projectRoot, "src", "outside.cs"); + File.WriteAllText(insidePath, "class Inside {}\n"); + File.WriteAllText(outsidePath, "class Outside {}\n"); + TestProjectHelper.RunGit(projectRoot, "add", "src/inside.cs", "src/outside.cs"); + TestProjectHelper.RunGit(projectRoot, "commit", "-m", "initial"); + + // Flag src/outside.cs skip-worktree and remove it from disk to mimic a sparse-checkout + // working tree. The freshness checker must classify it as "outside sparse cone", + // not as a true "missing" file. + // src/outside.cs に skip-worktree を立て disk からも消し sparse-checkout を再現する。 + // freshness checker は "outside sparse cone" として分類し "missing" を立ててはいけない。 + TestProjectHelper.RunGit(projectRoot, "update-index", "--skip-worktree", "src/outside.cs"); + File.Delete(outsidePath); + + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/inside.cs", "csharp", "class Inside {}\n"); + TestProjectHelper.InsertIndexedFile(dbPath, "src/outside.cs", "csharp", "class Outside {}\n"); + MarkStatusReadinessReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", dbPath, "--check", "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var check = document.RootElement.GetProperty("workspace_check"); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.True(document.RootElement.GetProperty("index_matches_workspace").GetBoolean()); + Assert.Equal(0, check.GetProperty("missing_file_count").GetInt32()); + Assert.Equal(1, check.GetProperty("outside_sparse_cone_file_count").GetInt32()); + Assert.Equal("src/outside.cs", check.GetProperty("outside_sparse_cone_files")[0].GetString()); + Assert.Equal("matched", check.GetProperty("reason").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunStatus_CheckJson_KeepsTrulyMissingFilesSeparateFromSparseCone() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_status_check_sparse_mix"); + try + { + TestProjectHelper.InitializeGitRepo(projectRoot); + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + var keptPath = Path.Combine(projectRoot, "src", "kept.cs"); + var sparsePath = Path.Combine(projectRoot, "src", "sparse.cs"); + File.WriteAllText(keptPath, "class Kept {}\n"); + File.WriteAllText(sparsePath, "class Sparse {}\n"); + TestProjectHelper.RunGit(projectRoot, "add", "src/kept.cs", "src/sparse.cs"); + TestProjectHelper.RunGit(projectRoot, "commit", "-m", "initial"); + + // src/sparse.cs is skip-worktree → outside cone. src/deleted.cs is indexed only, + // never tracked by git → must remain a real "missing" entry. + // src/sparse.cs は skip-worktree → cone 外。src/deleted.cs は DB のみで git 追跡無し + // → 本当の "missing" として残らなければならない。 + TestProjectHelper.RunGit(projectRoot, "update-index", "--skip-worktree", "src/sparse.cs"); + File.Delete(sparsePath); + + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/kept.cs", "csharp", "class Kept {}\n"); + TestProjectHelper.InsertIndexedFile(dbPath, "src/sparse.cs", "csharp", "class Sparse {}\n"); + TestProjectHelper.InsertIndexedFile(dbPath, "src/deleted.cs", "csharp", "class Deleted {}\n"); + MarkStatusReadinessReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", dbPath, "--check", "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var check = document.RootElement.GetProperty("workspace_check"); + + Assert.Equal(1, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.False(document.RootElement.GetProperty("index_matches_workspace").GetBoolean()); + Assert.Equal("workspace_stale", document.RootElement.GetProperty("failed_checks")[0].GetString()); + Assert.Equal(1, check.GetProperty("missing_file_count").GetInt32()); + Assert.Equal("src/deleted.cs", check.GetProperty("missing_files")[0].GetString()); + Assert.Equal(1, check.GetProperty("outside_sparse_cone_file_count").GetInt32()); + Assert.Equal("src/sparse.cs", check.GetProperty("outside_sparse_cone_files")[0].GetString()); + Assert.Equal("missing_indexed_files", check.GetProperty("reason").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunStatus_CheckHuman_SuccessKeepsOutputSilent() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_status_check_sparse_human"); + try + { + TestProjectHelper.InitializeGitRepo(projectRoot); + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + var sparsePath = Path.Combine(projectRoot, "src", "sparse.cs"); + File.WriteAllText(sparsePath, "class Sparse {}\n"); + TestProjectHelper.RunGit(projectRoot, "add", "src/sparse.cs"); + TestProjectHelper.RunGit(projectRoot, "commit", "-m", "initial"); + TestProjectHelper.RunGit(projectRoot, "update-index", "--skip-worktree", "src/sparse.cs"); + File.Delete(sparsePath); + + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/sparse.cs", "csharp", "class Sparse {}\n"); + MarkStatusReadinessReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", dbPath, "--check"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(string.Empty, stdout); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunStatus_CheckHuman_WritesStaleDiagnosticToStderr() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_status_check_stderr"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + var sourcePath = Path.Combine(projectRoot, "src", "app.cs"); + File.WriteAllText(sourcePath, "class App {}\n"); + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", "class App {}\n"); + MarkStatusReadinessReady(dbPath); + File.WriteAllText(sourcePath, "class App { void Run() {} }\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", dbPath, "--check"], + _jsonOptions)); + + Assert.Equal(1, exitCode); + Assert.Equal(string.Empty, stdout); + Assert.Contains("[stale] workspace_check reason=changed_files", stderr); + Assert.Contains("changed=1", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunStatus_CheckJsonScopedFold_ReportsOnlyFoldDegradation() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_status_check_fold_scope"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText(Path.Combine(projectRoot, "src", "app.cs"), "class App {}\n"); + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", "class App {}\n"); + using (var db = new DbContext(dbPath)) + { + using var cmd = db.Connection.CreateCommand(); + cmd.CommandText = "PRAGMA user_version = 0"; + cmd.ExecuteNonQuery(); + } + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", dbPath, "--check=fold", "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + var failedChecks = json.GetProperty("failed_checks"); + + Assert.Equal(2, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.False(json.GetProperty("fold_ready").GetBoolean()); + Assert.Single(failedChecks.EnumerateArray()); + Assert.Equal("fold_ready", failedChecks[0].GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Theory] + [InlineData("graph", "graph_table_available")] + [InlineData("issues", "file_issues_data_current")] + [InlineData("hotspot", "hotspot_family_ready")] + [InlineData("csharp", "csharp_symbol_name_ready")] + [InlineData("sql", "sql_graph_contract_ready")] + [InlineData("newer", "index_newer_than_reader")] + public void RunStatus_CheckJsonScopedReadiness_ReportsOnlyRequestedSubsystem(string scope, string expectedFailure) + { + var projectRoot = TestProjectHelper.CreateTempProject($"cdidx_query_runner_status_check_scope_{scope}"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText(Path.Combine(projectRoot, "src", "app.cs"), "class App {}\n"); + File.WriteAllText(Path.Combine(projectRoot, "src", "query.sql"), "SELECT run_me();\n"); + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", "class App {}\n"); + TestProjectHelper.InsertIndexedFile(dbPath, "src/query.sql", "sql", "SELECT run_me();\n"); + MarkStatusReadinessReady(dbPath); + using (var db = new DbContext(dbPath)) + { + var writer = new DbWriter(db.Connection); + switch (scope) + { + case "graph": + ExecuteNonQuery(db, $"PRAGMA user_version = {DbContext.CurrentSchemaVersion & ~DbContext.GraphReadyFlag}"); + break; + case "issues": + ExecuteNonQuery(db, $"PRAGMA user_version = {DbContext.CurrentSchemaVersion & ~DbContext.IssuesReadyFlag}"); + break; + case "hotspot": + writer.ClearHotspotFamilyReady(); + break; + case "csharp": + writer.SetMeta(DbContext.CSharpSymbolNameContractVersionMetaKey, null); + break; + case "sql": + writer.SetMeta(DbContext.SqlGraphContractVersionMetaKey, null); + break; + case "newer": + writer.SetMeta("fold_key_version", (NameFold.Version + 1).ToString(System.Globalization.CultureInfo.InvariantCulture)); + break; + } + } + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", dbPath, $"--check={scope}", "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var failedChecks = document.RootElement.GetProperty("failed_checks").EnumerateArray().Select(e => e.GetString()).ToArray(); + + Assert.Equal(2, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal([expectedFailure], failedChecks); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunStatus_CheckJson_UsesRepositoryRootIgnoreRulesForSubdirectoryIndex() + { + var repoRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_status_check_parent_ignore"); + try + { + TestProjectHelper.InitializeGitRepo(repoRoot); + File.WriteAllText(Path.Combine(repoRoot, ".gitignore"), "sub/generated/\n"); + + var projectRoot = Path.Combine(repoRoot, "sub"); + Directory.CreateDirectory(Path.Combine(projectRoot, "generated")); + File.WriteAllText(Path.Combine(projectRoot, "app.cs"), "class App {}\n"); + File.WriteAllText(Path.Combine(projectRoot, "generated", "ignored.cs"), "class Ignored {}\n"); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--db", dbPath, "--json", "--quiet"], + _jsonOptions)); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", dbPath, "--check", "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var check = document.RootElement.GetProperty("workspace_check"); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.True(document.RootElement.GetProperty("index_matches_workspace").GetBoolean()); + Assert.Equal(1, check.GetProperty("indexed_file_count").GetInt32()); + Assert.Equal(1, check.GetProperty("workspace_file_count").GetInt32()); + Assert.Equal(0, check.GetProperty("unindexed_file_count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(repoRoot); + } + } + + [Fact] + public void RunStatus_ReadOnlyUriForExplicitDb_UsesPersistedProjectRootMetadata() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_status_uri"); + var dbPath = Path.Combine(Path.GetTempPath(), $"cdidx_query_runner_status_{Guid.NewGuid():N}.db"); + try + { + TestProjectHelper.InitializeGitRepo(projectRoot); + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + var sourcePath = Path.Combine(projectRoot, "src", "app.cs"); + File.WriteAllText(sourcePath, "class App {}\n"); + TestProjectHelper.RunGit(projectRoot, "add", "src/app.cs"); + TestProjectHelper.RunGit(projectRoot, "commit", "-m", "initial"); + + var expectedHead = TestProjectHelper.RunGit(projectRoot, "rev-parse", "HEAD").Trim(); + using (var db = new DbContext(dbPath)) + { + db.InitializeSchema(); + var writer = new DbWriter(db.Connection); + writer.SetMeta(DbContext.IndexedProjectRootMetaKey, projectRoot); + } + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", "class App {}\n"); + using (var db = new DbContext(dbPath)) + { + using var cmd = db.Connection.CreateCommand(); + cmd.CommandText = "PRAGMA wal_checkpoint(TRUNCATE);"; + cmd.ExecuteNonQuery(); + } + SqliteConnection.ClearAllPools(); + + File.WriteAllText(sourcePath, "class App { void Run() {} }\n"); + + var readOnlyUri = new Uri(dbPath).AbsoluteUri + "?immutable=1"; + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", readOnlyUri, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(projectRoot, json.GetProperty("project_root").GetString()); + Assert.Equal(expectedHead, json.GetProperty("git_head").GetString()); + Assert.True(json.GetProperty("git_is_dirty").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + SqliteConnection.ClearAllPools(); + if (File.Exists(dbPath)) + File.Delete(dbPath); + } + } + + [Fact] + public void RunStatus_CustomDbUnderCdidx_UsesPersistedProjectRootMetadata() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_status_custom_db"); + var dbContainerRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_status_custom_container"); + var dbPath = Path.Combine(dbContainerRoot, ".cdidx", "shared.db"); + try + { + TestProjectHelper.InitializeGitRepo(projectRoot); + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + var sourcePath = Path.Combine(projectRoot, "src", "app.cs"); + File.WriteAllText(sourcePath, "class App {}\n"); + TestProjectHelper.RunGit(projectRoot, "add", "src/app.cs"); + TestProjectHelper.RunGit(projectRoot, "commit", "-m", "initial"); + + var expectedHead = TestProjectHelper.RunGit(projectRoot, "rev-parse", "HEAD").Trim(); + Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!); + using (var db = new DbContext(dbPath)) + { + db.InitializeSchema(); + var writer = new DbWriter(db.Connection); + writer.SetMeta(DbContext.IndexedProjectRootMetaKey, projectRoot); + } + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", "class App {}\n"); + + File.WriteAllText(sourcePath, "class App { void Run() {} }\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(projectRoot, json.GetProperty("project_root").GetString()); + Assert.Equal(expectedHead, json.GetProperty("git_head").GetString()); + Assert.True(json.GetProperty("git_is_dirty").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + TestProjectHelper.DeleteDirectory(dbContainerRoot); + } + } + + [Fact] + public void RunStatus_ExplicitProjectLocalDb_LeavesWorkspaceMetadataNullWhenMetadataIsMissing() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_status_project_local_explicit"); + try + { + TestProjectHelper.InitializeGitRepo(projectRoot); + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + var sourcePath = Path.Combine(projectRoot, "src", "app.cs"); + File.WriteAllText(sourcePath, "class App {}\n"); + TestProjectHelper.RunGit(projectRoot, "add", "src/app.cs"); + TestProjectHelper.RunGit(projectRoot, "commit", "-m", "initial"); + + var expectedHead = TestProjectHelper.RunGit(projectRoot, "rev-parse", "HEAD").Trim(); + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + using (var db = new DbContext(dbPath)) + { + using var cmd = db.Connection.CreateCommand(); + cmd.CommandText = "DELETE FROM codeindex_meta WHERE key = @key"; + cmd.Parameters.AddWithValue("@key", DbContext.IndexedProjectRootMetaKey); + cmd.ExecuteNonQuery(); + } + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", "class App {}\n"); + + File.WriteAllText(sourcePath, "class App { void Run() {} }\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(JsonValueKind.Null, json.GetProperty("project_root").ValueKind); + Assert.Equal(JsonValueKind.Null, json.GetProperty("git_head").ValueKind); + Assert.Equal(JsonValueKind.Null, json.GetProperty("git_is_dirty").ValueKind); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunStatus_ExplicitProjectLocalReadOnlyUri_LeavesWorkspaceMetadataNullWhenMetadataIsMissing() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_status_project_local_uri"); + try + { + TestProjectHelper.InitializeGitRepo(projectRoot); + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + var sourcePath = Path.Combine(projectRoot, "src", "app.cs"); + File.WriteAllText(sourcePath, "class App {}\n"); + TestProjectHelper.RunGit(projectRoot, "add", "src/app.cs"); + TestProjectHelper.RunGit(projectRoot, "commit", "-m", "initial"); + + var expectedHead = TestProjectHelper.RunGit(projectRoot, "rev-parse", "HEAD").Trim(); + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + using (var db = new DbContext(dbPath)) + { + using var cmd = db.Connection.CreateCommand(); + cmd.CommandText = "DELETE FROM codeindex_meta WHERE key = @key"; + cmd.Parameters.AddWithValue("@key", DbContext.IndexedProjectRootMetaKey); + cmd.ExecuteNonQuery(); + } + using (var db = new DbContext(dbPath)) + { + using var cmd = db.Connection.CreateCommand(); + cmd.CommandText = "PRAGMA wal_checkpoint(TRUNCATE);"; + cmd.ExecuteNonQuery(); + } + SqliteConnection.ClearAllPools(); + + File.WriteAllText(sourcePath, "class App { void Run() {} }\n"); + + var readOnlyUri = new Uri(dbPath).AbsoluteUri + "?immutable=1"; + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", readOnlyUri, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(JsonValueKind.Null, json.GetProperty("project_root").ValueKind); + Assert.Equal(JsonValueKind.Null, json.GetProperty("git_head").ValueKind); + Assert.Equal(JsonValueKind.Null, json.GetProperty("git_is_dirty").ValueKind); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + SqliteConnection.ClearAllPools(); + } + } + + [Fact] + public void RunStatus_ExplicitExternalCodeIndexDb_UsesPersistedProjectRootMetadata() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_status_codeindex_db"); + var dbContainerRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_status_codeindex_container"); + var dbPath = Path.Combine(dbContainerRoot, ".cdidx", "codeindex.db"); + try + { + TestProjectHelper.InitializeGitRepo(projectRoot); + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + var sourcePath = Path.Combine(projectRoot, "src", "app.cs"); + File.WriteAllText(sourcePath, "class App {}\n"); + TestProjectHelper.RunGit(projectRoot, "add", "src/app.cs"); + TestProjectHelper.RunGit(projectRoot, "commit", "-m", "initial"); + + var expectedHead = TestProjectHelper.RunGit(projectRoot, "rev-parse", "HEAD").Trim(); + Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!); + using (var db = new DbContext(dbPath)) + { + db.InitializeSchema(); + var writer = new DbWriter(db.Connection); + writer.SetMeta(DbContext.IndexedProjectRootMetaKey, projectRoot); + } + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", "class App {}\n"); + + File.WriteAllText(sourcePath, "class App { void Run() {} }\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(projectRoot, json.GetProperty("project_root").GetString()); + Assert.Equal(expectedHead, json.GetProperty("git_head").GetString()); + Assert.True(json.GetProperty("git_is_dirty").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + TestProjectHelper.DeleteDirectory(dbContainerRoot); + } + } + + [Fact] + public void RunStatus_ExplicitExternalCodeIndexDb_IgnoresSingleSiblingPathCollision() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_status_codeindex_collision_db"); + var dbContainerRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_status_codeindex_collision_container"); + var dbPath = Path.Combine(dbContainerRoot, ".cdidx", "codeindex.db"); + try + { + TestProjectHelper.InitializeGitRepo(projectRoot); + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + Directory.CreateDirectory(Path.Combine(dbContainerRoot, "src")); + + const string content = "class App {}\n"; + File.WriteAllText(Path.Combine(projectRoot, "src", "app.cs"), content); + File.WriteAllText(Path.Combine(dbContainerRoot, "src", "app.cs"), content); + TestProjectHelper.RunGit(projectRoot, "add", "src/app.cs"); + TestProjectHelper.RunGit(projectRoot, "commit", "-m", "initial"); + + var expectedHead = TestProjectHelper.RunGit(projectRoot, "rev-parse", "HEAD").Trim(); + Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!); + using (var db = new DbContext(dbPath)) + { + db.InitializeSchema(); + var writer = new DbWriter(db.Connection); + writer.SetMeta(DbContext.IndexedProjectRootMetaKey, projectRoot); + } + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", content); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(projectRoot, json.GetProperty("project_root").GetString()); + Assert.Equal(expectedHead, json.GetProperty("git_head").GetString()); + Assert.False(json.GetProperty("git_is_dirty").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + TestProjectHelper.DeleteDirectory(dbContainerRoot); + } + } + + [Fact] + public void RunStatus_ExplicitExternalCodeIndexDbWithoutMetadata_IgnoresSiblingPathCollisionAndLeavesWorkspaceMetadataNull() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_status_codeindex_missing_meta_db"); + var dbContainerRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_status_codeindex_missing_meta_container"); + var dbPath = Path.Combine(dbContainerRoot, ".cdidx", "codeindex.db"); + try + { + TestProjectHelper.InitializeGitRepo(projectRoot); + TestProjectHelper.InitializeGitRepo(dbContainerRoot); + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + Directory.CreateDirectory(Path.Combine(dbContainerRoot, "src")); + + const string indexedContent = "class App {}\n"; + const string siblingContent = "class App { void Different() {} }\n"; + File.WriteAllText(Path.Combine(projectRoot, "src", "app.cs"), indexedContent); + File.WriteAllText(Path.Combine(dbContainerRoot, "src", "app.cs"), siblingContent); + TestProjectHelper.RunGit(projectRoot, "add", "src/app.cs"); + TestProjectHelper.RunGit(projectRoot, "commit", "-m", "initial"); + TestProjectHelper.RunGit(dbContainerRoot, "add", "src/app.cs"); + TestProjectHelper.RunGit(dbContainerRoot, "commit", "-m", "initial"); + + Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!); + using (var db = new DbContext(dbPath)) + { + db.InitializeSchema(); + } + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", indexedContent); + using (var db = new DbContext(dbPath)) + { + using var cmd = db.Connection.CreateCommand(); + cmd.CommandText = "DELETE FROM codeindex_meta WHERE key = @key"; + cmd.Parameters.AddWithValue("@key", DbContext.IndexedProjectRootMetaKey); + cmd.ExecuteNonQuery(); + } + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(JsonValueKind.Null, json.GetProperty("project_root").ValueKind); + Assert.Equal(JsonValueKind.Null, json.GetProperty("git_head").ValueKind); + Assert.Equal(JsonValueKind.Null, json.GetProperty("git_is_dirty").ValueKind); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + TestProjectHelper.DeleteDirectory(dbContainerRoot); + } + } + + [Fact] + public void RunStatus_MissingDatabaseReturnsGuidance() + { + var missingDbPath = Path.Combine(Path.GetTempPath(), $"missing_{Guid.NewGuid():N}.db"); + + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", missingDbPath], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("Error [E001_DB_NOT_FOUND]: --db", stderr); + // Verify full (absolute) path is shown, not just the basename / フルパス表示を検証 + Assert.Contains(Path.GetFullPath(missingDbPath), stderr); + Assert.Contains("does not point to an existing database file", stderr); + Assert.Contains("Hint: create or refresh the index with `cdidx index `", stderr); + } + + [Fact] + public void RunFiles_HumanOutputFormatsSizesAndBytesFlagKeepsRawCounts() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_files_size_units"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/big.cs", "csharp", "class Big {}\n"); + SetIndexedFileSize(dbPath, "src/big.cs", 5L * 1024 * 1024 * 1024); + + var (formattedExit, formattedStdout, formattedStderr) = CaptureConsole(() => QueryCommandRunner.RunFiles( + ["--db", dbPath], + _jsonOptions)); + var (rawExit, rawStdout, rawStderr) = CaptureConsole(() => QueryCommandRunner.RunFiles( + ["--db", dbPath, "--bytes"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, formattedExit); + Assert.Equal(CommandExitCodes.Success, rawExit); + Assert.Contains("5.0 GiB", formattedStdout); + Assert.DoesNotContain("5368709120 bytes", formattedStdout); + Assert.Contains("5368709120 bytes", rawStdout); + Assert.Equal("(1 files)" + Environment.NewLine, formattedStderr); + Assert.Equal("(1 files)" + Environment.NewLine, rawStderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunFiles_JsonOutputKeepsRawSizeInteger() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_files_size_json"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/big.cs", "csharp", "class Big {}\n"); + SetIndexedFileSize(dbPath, "src/big.cs", 5L * 1024 * 1024 * 1024); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFiles( + ["--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(5L * 1024 * 1024 * 1024, document.RootElement.GetProperty("size").GetInt64()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunStatus_ReadOnlyFlagOpensImmutableUriAndReportsModeFields() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_status_readonly"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", "class App {}\n"); + + var options = QueryCommandRunner.ParseArgs( + ["--db", dbPath, "--read-only", "--json"], + jsonDefault: false, + allowStatusCheck: true, + validateDefaultLimit: false, + validateDefaultSnippetLines: false, + validateDefaultMaxLineWidth: false); + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", dbPath, "--read-only", "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.StartsWith("file:", options.DbPath, StringComparison.OrdinalIgnoreCase); + Assert.Contains("immutable=1", options.DbPath, StringComparison.OrdinalIgnoreCase); + Assert.Contains("mode=ro", options.DbPath, StringComparison.OrdinalIgnoreCase); + Assert.False(document.RootElement.GetProperty("read_only_fallback").GetBoolean()); + Assert.False(document.RootElement.GetProperty("wal_checkpoint_attempted").GetBoolean()); + Assert.False(document.RootElement.GetProperty("wal_checkpoint_succeeded").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } +} diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerGraphTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerGraphTests.cs new file mode 100644 index 0000000000..1d69ad0da4 --- /dev/null +++ b/tests/CodeIndex.Tests/QueryCommandRunnerGraphTests.cs @@ -0,0 +1,13526 @@ +using System.Text.Json; +using CodeIndex.Cli; +using CodeIndex.Database; +using CodeIndex.Models; +using Microsoft.Data.Sqlite; + +namespace CodeIndex.Tests; + +public partial class QueryCommandRunnerTests +{ + [Fact] + public void RunReferences_AllowsExcludePathValueThatLooksLikePreviewOption() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_references_preview_like_exclude_path_value"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["target", "--db", dbPath, "--exclude-path=--focus-line", "--count"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal("0", stdout.Trim()); + Assert.DoesNotContain("is not supported", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_RejectsMissingMaxLineWidthValue() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_references_missing_max_line_width"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["target", "--db", dbPath, "--max-line-width", "--json"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + // Missing-value guard short-circuits before TryParsePositiveInt; see + // RunExcerpt_RejectsMissingFocusColumnValue for the matching contract note. + // TryParsePositiveInt より前で値欠如として短絡する。契約の詳細は上記テスト参照。 + Assert.Contains("--max-line-width requires a value", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_UsageIncludesCount() + { + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + [], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("[--count]", stderr); + } + + [Fact] + public void RunImpact_MissingDepthValueShowsPerFlagHint_Issue1507() + { + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunImpact(["QueryCommandRunner", "--depth"], _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("Error: --depth requires a value.", stderr); + Assert.Contains("Hint: deprecated alias", stderr); + Assert.Contains("--max-hops 5", stderr); + } + + [Fact] + public void RunImpact_OutOfRangeDepthUpperBound_ReturnsUsageError_Issue1700() + { + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( + ["Target", "--depth", "999999999"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Equal(string.Empty, stdout); + Assert.Contains("--depth must be less than or equal to 64", stderr); + Assert.Contains($"Usage: {ConsoleUi.GetUsageLine("impact")}", stderr); + Assert.DoesNotContain("database not found", stderr); + } + + [Theory] + [InlineData("-1")] + [InlineData("abc")] + public void RunReferences_RejectsNegativeOrNonNumericMaxLineWidthValue(string invalidValue) + { + var projectRoot = TestProjectHelper.CreateTempProject($"cdidx_references_invalid_max_line_width_{invalidValue}"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["target", "--db", dbPath, "--max-line-width", invalidValue, "--json"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("--max-line-width requires an integer between 0 and 4096", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void GraphCommands_BodyOptionAddsCappedBodyExcerpt_Issue1594() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_graph_body"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Session.cs", "csharp", """ + class Session + { + int Run(int user) + { + var value = user; + return value; + } + + int Login(int user) + { + return Run(user); + } + } + """); + using (var db = new DbContext(dbPath)) + { + using var select = db.Connection.CreateCommand(); + select.CommandText = "SELECT id FROM files WHERE path = 'src/Session.cs'"; + var fileId = Convert.ToInt32(select.ExecuteScalar()); + var writer = new DbWriter(db.Connection); + writer.InsertReferences([ + new ReferenceRecord + { + FileId = fileId, + SymbolName = "Run", + ReferenceKind = "call", + Line = 11, + Column = 16, + Context = " return Run(user);", + ContainerKind = "function", + ContainerName = "Login", + } + ]); + writer.MarkGraphReady(); + } + + AssertBodyExcerpt( + QueryCommandRunner.RunReferences, + ["Run", "--db", dbPath, "--json", "--body", "--snippet-lines", "1"], + "int Login(int user)"); + AssertBodyExcerpt( + QueryCommandRunner.RunCallers, + ["Run", "--db", dbPath, "--json", "--body", "--snippet-lines", "2"], + "int Login(int user)"); + AssertBodyExcerpt( + QueryCommandRunner.RunCallees, + ["Login", "--db", dbPath, "--json", "--body", "--snippet-lines", "1"], + "int Run(int user)"); + + var (impactExitCode, impactStdout, impactStderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( + ["Run", "--db", dbPath, "--json", "--body", "--snippet-lines", "2"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, impactExitCode); + Assert.Equal(string.Empty, impactStderr); + using var impactDocument = ParseJsonOutput(impactStdout); + var impactCaller = impactDocument.RootElement.GetProperty("callers")[0]; + Assert.Contains("int Login(int user)", impactCaller.GetProperty("body_content").GetString()); + Assert.Equal(2, CountLines(impactCaller.GetProperty("body_content").GetString()!)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_JsonZeroResults_EmitEnvelopeAndFreshness() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_references_zero_json"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + using (var db = new DbContext(dbPath)) + { + var writer = new DbWriter(db.Connection); + var fileId = writer.UpsertFile(new FileRecord + { + Path = "src/app.py", + Lang = "python", + Size = 32, + Lines = 1, + Modified = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc), + }); + writer.InsertSymbols([ + new SymbolRecord { FileId = fileId, Kind = "function", Name = "bootstrap", Line = 1, StartLine = 1, EndLine = 1 } + ]); + writer.MarkGraphReady(); + } + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["MissingRef", "--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + AssertZeroResultPayload(json, "references"); + Assert.True(json.GetProperty("graph_table_available").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_JsonKeepsCsharpTypeAliasPatternReference() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_references_csharp_type_alias"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/Defs.cs", + "csharp", + """ + using Red = RealTypes.Red; + using static Probe.Color; + + namespace Probe; + + enum Color { Red, Blue } + class Demo + { + bool Match(object value) => value is Red; + void ProbeType() { _ = typeof(Red); } + } + + namespace RealTypes; + class Red {} + """); + + using (var db = new DbContext(dbPath)) + { + var countCmd = db.Connection.CreateCommand(); + countCmd.CommandText = "SELECT COUNT(*) FROM symbol_references WHERE symbol_name = 'Red'"; + Assert.Equal(2L, (long)countCmd.ExecuteScalar()!); + } + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + + var references = ParseJsonLines(stdout).Select(line => line.RootElement).ToList(); + Assert.Equal(2, references.Count); + Assert.Contains(references, reference => + reference.GetProperty("symbol_name").GetString() == "Red" + && reference.GetProperty("reference_kind").GetString() == "type_reference" + && reference.GetProperty("container_name").GetString() == "Match"); + Assert.Contains(references, reference => + reference.GetProperty("symbol_name").GetString() == "Red" + && reference.GetProperty("reference_kind").GetString() == "type_reference" + && reference.GetProperty("container_name").GetString() == "ProbeType"); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_JsonKeepsGlobalCsharpTypeAliasPatternReference() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_references_csharp_global_type_alias"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/GlobalUsings.cs", + "csharp", + """ + global using Red = RealTypes.Red; + """); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/Use.cs", + "csharp", + """ + using static Probe.Color; + + namespace Probe; + + enum Color { Red, Blue } + class Demo + { + bool Match(object value) => value is Red; + } + """); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/RealRed.cs", + "csharp", + """ + namespace RealTypes; + class Red {} + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + + var references = ParseJsonLines(stdout).Select(line => line.RootElement).ToList(); + Assert.Single(references); + Assert.Equal("Red", references[0].GetProperty("symbol_name").GetString()); + Assert.Equal("type_reference", references[0].GetProperty("reference_kind").GetString()); + Assert.Equal("Match", references[0].GetProperty("container_name").GetString()); + + var (countExitCode, countStdout, countStderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name", "--count"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, countExitCode); + Assert.Equal(string.Empty, countStderr); + var countJson = ParseJsonOutput(countStdout).RootElement; + Assert.Equal(1, countJson.GetProperty("count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_JsonKeepsCsharpQualifiedIsPatternCallExact() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_references_csharp_qualified_is_pattern_call_exact"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/Use.cs", + "csharp", + """ + namespace Probe; + + public enum Color + { + Red, + Blue + } + + public class Red {} + + class Demo + { + bool Match(object value) => value is Color.Red or Color.Blue; + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + + var references = ParseJsonLines(stdout).Select(line => line.RootElement).ToList(); + 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("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"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, countExitCode); + Assert.Equal(string.Empty, countStderr); + var countJson = ParseJsonOutput(countStdout).RootElement; + Assert.Equal(1, countJson.GetProperty("count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_JsonClampsLongSingleLineContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_references_long_line"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + var longLine = "const x = 0; " + new string('a', 320) + " target(); " + new string('b', 320); + using (var db = new DbContext(dbPath)) + { + var writer = new DbWriter(db.Connection); + var fileId = writer.UpsertFile(new FileRecord + { + Path = "dist/app.js", + Lang = "javascript", + Size = longLine.Length, + Lines = 1, + Modified = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc), + }); + writer.InsertChunks([ + new ChunkRecord { FileId = fileId, ChunkIndex = 0, StartLine = 1, EndLine = 1, Content = longLine } + ]); + writer.InsertReferences([ + new ReferenceRecord + { + FileId = fileId, + SymbolName = "target", + ReferenceKind = "call", + Line = 1, + Column = longLine.IndexOf("target", StringComparison.Ordinal) + 1, + Context = longLine, + } + ]); + writer.MarkGraphReady(); + } + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["target", "--db", dbPath, "--json", "--max-line-width", "96"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.True(json.GetProperty("context_truncated").GetBoolean()); + Assert.Contains("target()", json.GetProperty("context").GetString()); + Assert.True(json.GetProperty("context").GetString()!.Length <= 96); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_JsonZeroResults_WithMissingGraphTable_ReturnsDegradedPayload() + { + var (projectRoot, readOnlyUri) = CreateReadOnlyMissingGraphTableDb("cdidx_references_zero_json_missing_graph"); + try + { + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Run", "--db", readOnlyUri, "--json", "--exact"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.False(json.GetProperty("graph_table_available").GetBoolean()); + Assert.True(json.GetProperty("degraded").GetBoolean()); + Assert.False(json.GetProperty("exact_index_available").GetBoolean()); + Assert.Contains("symbol_references table missing", json.GetProperty("degraded_reason").GetString()); + Assert.Equal(0, json.GetProperty("references").GetArrayLength()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpInterpolatedRawStringPreservesCallSite() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_references_csharp_interpolated_raw"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "app.cs"), + """" + public class App + { + private string Run() => "ok"; + + public string Render() + { + return $""" + value = {Run()} + literal = function main() + """; + } + } + """"); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Run", "--db", dbPath, "--json", "--exact-name", "--lang", "csharp"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("Run", json.GetProperty("symbol_name").GetString()); + Assert.Equal("src/app.cs", json.GetProperty("path").GetString()); + Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("Render", json.GetProperty("container_name").GetString()); + Assert.Equal(8, json.GetProperty("line").GetInt32()); + Assert.True(json.GetProperty("exact_index_available").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpNestedInterpolatedStringInsideRawInterpolationPreservesCallSite() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_references_csharp_nested_interpolated_raw"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "app.cs"), + """" + public class App + { + private string Run() => "ok"; + + public string Render() + { + return $""" + value = {$"{Run()}"} + literal = function main() + """; + } + } + """"); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Run", "--db", dbPath, "--json", "--exact-name", "--lang", "csharp"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("Run", json.GetProperty("symbol_name").GetString()); + Assert.Equal("src/app.cs", json.GetProperty("path").GetString()); + Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("Render", json.GetProperty("container_name").GetString()); + Assert.Equal(8, json.GetProperty("line").GetInt32()); + Assert.True(json.GetProperty("exact_index_available").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_Json_CSharpInterpolatedVerbatimStringEscapedBracesDoNotCreatePhantomReference() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_references_csharp_escaped_verbatim_braces"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "app.cs"), + """ + public class App + { + public string Render() + { + return $@"{{Run()}}"; + } + } + """); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Run", "--db", dbPath, "--json", "--exact-name", "--lang", "csharp"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Equal(0, json.GetProperty("references").GetArrayLength()); + Assert.True(json.GetProperty("exact_index_available").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_AcceptsTypeReferenceKind_WithoutUnknownKindWarning() + { + // issue #444: `references --kind type_reference` is a legitimate query (compile-time + // type-position edges from C#/Java base lists, declaration types, generic constraints, + // `is`/`as`/`instanceof`, and XML-doc `cref`). It must succeed without the "unknown + // reference kind" hint that was previously printed by `WriteGraphReferenceKindHint`. + // issue #444: `references --kind type_reference` は compile-time な型位置エッジを + // 列挙する正当なクエリ(C#/Java の継承リスト・宣言型・generic 制約・`is`/`as`/`instanceof`・ + // XML-doc `cref`)。以前は `WriteGraphReferenceKindHint` が "unknown reference kind" と + // 警告していたが、その偽警告を出さずに成功しなければならない。 + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_references_type_reference_kind"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "Target.cs"), + """ + public class TargetBase { } + """); + File.WriteAllText( + Path.Combine(projectRoot, "src", "Consumer.cs"), + """ + public class Consumer : TargetBase + { + } + """); + + var (indexExitCode, _, _) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["TargetBase", "--db", dbPath, "--kind", "type_reference", "--lang", "csharp", "--exact"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.DoesNotContain("not a known reference kind", stderr); + Assert.DoesNotContain("WARN:", stderr); + Assert.Contains("type_reference", stdout); + Assert.Contains("TargetBase", stdout); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_AcceptsAugmentationKind_WithoutUnknownKindWarning() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_references_augmentation_kind"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "a.ts"), + """ + interface Widget { a: number } + """); + File.WriteAllText( + Path.Combine(projectRoot, "src", "b.ts"), + """ + interface Widget { b: string } + """); + + var (indexExitCode, _, _) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Widget", "--db", dbPath, "--kind", "augmentation", "--lang", "typescript", "--exact"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.DoesNotContain("not a known reference kind", stderr); + Assert.DoesNotContain("WARN:", stderr); + Assert.Contains("augmentation", stdout); + Assert.Contains("Widget", stdout); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_JsonTypeReferenceKind_EmitsNoStderrWarning() + { + // issue #444 JSON path: the stderr "unknown reference kind" hint is suppressed for + // `--json`, but the fix also straightens the validation set so `type_reference` is + // accepted everywhere without relying on JSON suppression. + // issue #444 JSON 経路: `--json` のときは stderr のヒント自体が抑制されるが、 + // 今回の修正で検証集合も整理されたため、JSON 抑制に頼らずとも `type_reference` が + // 受理されることを確認する。 + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_references_type_reference_kind_json"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "User.cs"), + """ + public class User { } + """); + File.WriteAllText( + Path.Combine(projectRoot, "src", "Consumer.cs"), + """ + public class Consumer : User + { + } + """); + + var (indexExitCode, _, _) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["User", "--db", dbPath, "--kind", "type_reference", "--json", "--lang", "csharp", "--exact"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Contains("\"reference_kind\":\"type_reference\"", stdout); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunCallers_JsonZeroResults_WithMissingGraphTable_ReturnsDegradedPayload() + { + var (projectRoot, readOnlyUri) = CreateReadOnlyMissingGraphTableDb("cdidx_callers_zero_json_missing_graph"); + try + { + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunCallers( + ["Run", "--db", readOnlyUri, "--json", "--exact"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.False(json.GetProperty("graph_table_available").GetBoolean()); + Assert.True(json.GetProperty("degraded").GetBoolean()); + Assert.False(json.GetProperty("exact_index_available").GetBoolean()); + Assert.Contains("symbol_references table missing", json.GetProperty("degraded_reason").GetString()); + Assert.Equal(0, json.GetProperty("callers").GetArrayLength()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunCallers_ExactJson_FindsTernaryContinuationCallSite() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_callers_csharp_ternary"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "dispatcher.cs"), + """ + public class Dispatcher + { + private string Select(bool isUpdate) + => isUpdate + ? RunUpdateMode() + : RunFullScan(); + + private string RunUpdateMode() => "update"; + private string RunFullScan() => "full"; + } + """); + + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunCallers( + ["RunUpdateMode", "--db", Path.Combine(projectRoot, ".cdidx", "codeindex.db"), "--json", "--exact", "--lang", "csharp"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("src/dispatcher.cs", json.GetProperty("path").GetString()); + // With #233 fixed, the expression-bodied `Select` method spans its declaration + // through the terminating ';' (multi-line ternary on the RHS of `=>`), so the + // RunUpdateMode call at line 5 attributes to Select, not the enclosing class. + // #233 修正により、`=>` で始まる式本体メソッド `Select` の範囲が宣言行から + // 末尾 `;` までに広がり、line 5 の RunUpdateMode 呼び出しは外側クラスではなく + // Select に帰属する。 + Assert.Equal("function", json.GetProperty("caller_kind").GetString()); + Assert.Equal("Select", json.GetProperty("caller_name").GetString()); + Assert.Equal("RunUpdateMode", json.GetProperty("callee_name").GetString()); + Assert.Equal(5, json.GetProperty("first_line").GetInt32()); + Assert.Equal(1, json.GetProperty("reference_count").GetInt32()); + Assert.True(json.GetProperty("exact_index_available").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunCallers_ExactJson_FindsCallerInsideAllmanStyleBlockBodyProperty() + { + // issue #233 review follow-up: Allman-style (next-line `{`) block-bodied C# + // properties were not extracted as symbols, so accessor-internal calls fell + // through to the enclosing class. End-to-end verify that `callers` attributes + // the call to the property itself once the extraction regex handles this shape. + // issue #233 のレビュー指摘: Allman スタイル(次行 `{`)の block-bodied プロパティが + // 抽出されておらず、accessor 内部の呼び出しが外側クラスに帰属していた。抽出 regex が + // この形を扱えるようになった後、`callers` が property に帰属することを end-to-end で確認する。 + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_callers_csharp_allman_prop"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "calc.cs"), + """ + public class Calc + { + public int Compute() => 42; + + public int Wrap + { + get { return Compute(); } + } + } + """); + + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunCallers( + ["Compute", "--db", Path.Combine(projectRoot, ".cdidx", "codeindex.db"), "--json", "--exact", "--lang", "csharp"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("src/calc.cs", json.GetProperty("path").GetString()); + Assert.Equal("property", json.GetProperty("caller_kind").GetString()); + Assert.Equal("Wrap", json.GetProperty("caller_name").GetString()); + Assert.Equal("Compute", json.GetProperty("callee_name").GetString()); + Assert.Equal(1, json.GetProperty("reference_count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunCallers_ExactJson_FindsCallerInsideMultiLineExpressionBodiedProperty() + { + // issue #233 second review follow-up: expression-bodied properties split across + // two lines (declaration + `=> expr;` continuation) must still attribute + // accessor-internal calls to the property through the CLI `callers` command. + // issue #233 の再レビュー指摘: 宣言行の次行に `=> expr;` が続く multi-line 式本体 + // プロパティでも、CLI `callers` で accessor 内呼び出しが property に帰属すること。 + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_callers_csharp_ml_exprprop"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "calc.cs"), + """ + public class Calc + { + public int Compute() => 42; + public int Wrap + => Compute(); + } + """); + + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunCallers( + ["Compute", "--db", Path.Combine(projectRoot, ".cdidx", "codeindex.db"), "--json", "--exact-name", "--lang", "csharp"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("src/calc.cs", json.GetProperty("path").GetString()); + Assert.Equal("property", json.GetProperty("caller_kind").GetString()); + Assert.Equal("Wrap", json.GetProperty("caller_name").GetString()); + Assert.Equal("Compute", json.GetProperty("callee_name").GetString()); + Assert.Equal(5, json.GetProperty("first_line").GetInt32()); + Assert.Equal(1, json.GetProperty("reference_count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunCallers_ExactJson_FindsCallerInsideBraceSameLineAccessorNextLineProperty() + { + // issue #233 fifth review follow-up: the common Microsoft-style block-bodied + // property (`{` on the header line, accessor on the following line) must have + // CLI `callers` attribute the accessor call to the property itself. + // issue #233 第5次レビュー指摘: `{` が宣言行末にあり、accessor が次行にある + // 標準的な block-bodied property でも、CLI `callers` は accessor 内部の呼び出しを + // property に帰属させなければならない。 + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_callers_csharp_brace_same_line"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "calc.cs"), + """ + public class Calc + { + public int Compute() => 42; + + public int Wrap { + get { return Compute(); } + } + } + """); + + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunCallers( + ["Compute", "--db", Path.Combine(projectRoot, ".cdidx", "codeindex.db"), "--json", "--exact-name", "--lang", "csharp"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("src/calc.cs", json.GetProperty("path").GetString()); + Assert.Equal("property", json.GetProperty("caller_kind").GetString()); + Assert.Equal("Wrap", json.GetProperty("caller_name").GetString()); + Assert.Equal("Compute", json.GetProperty("callee_name").GetString()); + Assert.Equal(1, json.GetProperty("reference_count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunCallers_ExactJson_FindsCallerInsideAllmanPropertyWithBlockComment() + { + // issue #233 fourth review follow-up: a multi-line /* ... */ block comment + // between the property header line and its `{` must not prevent CLI `callers` + // from attributing accessor-internal calls to the property itself. + // issue #233 の 4 回目レビュー指摘: property のヘッダ行と `{` の間に複数行の + // /* ... */ ブロックコメントが入っていても、CLI `callers` は accessor 内部の + // 呼び出しを外側クラスではなく property に帰属させなければならない。 + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_callers_csharp_allman_prop_cmt"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "calc.cs"), + """ + public class Calc + { + public int Compute() => 42; + + public int Wrap + /* some multi-line + block comment */ + { + get { return Compute(); } + } + } + """); + + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunCallers( + ["Compute", "--db", Path.Combine(projectRoot, ".cdidx", "codeindex.db"), "--json", "--exact-name", "--lang", "csharp"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("src/calc.cs", json.GetProperty("path").GetString()); + Assert.Equal("property", json.GetProperty("caller_kind").GetString()); + Assert.Equal("Wrap", json.GetProperty("caller_name").GetString()); + Assert.Equal("Compute", json.GetProperty("callee_name").GetString()); + Assert.Equal(1, json.GetProperty("reference_count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunCallers_ExactJson_FindsCallerInsideMultiLineExpressionPropertyWithBlockComment() + { + // issue #233 fourth review follow-up: a multi-line /* ... */ block comment + // between the property header line and its `=>` continuation must not prevent + // CLI `callers` from attributing the expression-body call to the property itself. + // issue #233 の 4 回目レビュー指摘: property のヘッダ行と `=>` 継続行の間に + // 複数行の /* ... */ ブロックコメントが入っていても、CLI `callers` は式本体の + // 呼び出しを外側クラスではなく property に帰属させなければならない。 + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_callers_csharp_ml_exprprop_cmt"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "calc.cs"), + """ + public class Calc + { + public int Compute() => 42; + + public int Wrap + /* multi-line + comment */ + => Compute(); + } + """); + + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunCallers( + ["Compute", "--db", Path.Combine(projectRoot, ".cdidx", "codeindex.db"), "--json", "--exact-name", "--lang", "csharp"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("src/calc.cs", json.GetProperty("path").GetString()); + Assert.Equal("property", json.GetProperty("caller_kind").GetString()); + Assert.Equal("Wrap", json.GetProperty("caller_name").GetString()); + Assert.Equal("Compute", json.GetProperty("callee_name").GetString()); + Assert.Equal(1, json.GetProperty("reference_count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunCallers_ExactJson_CSharpCompactSameLineTypeBody_PrefersInnermostMethodContainer() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_callers_csharp_compact_same_line_type_body"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace N; + enum Color { Red } + 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"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("function", json.GetProperty("caller_kind").GetString()); + Assert.Equal("M", json.GetProperty("caller_name").GetString()); + Assert.Equal("Red", json.GetProperty("callee_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunCallers_ExactJson_MultiLineSwitchArm_AttributesToEnclosingFunction() + { + // issue #233 third review follow-up: inside a switch expression whose `=>` is + // placed on a continuation line, calls from the arm body must still attribute to + // the enclosing function. If the switch-expression guard does not cover the + // continuation `=>`, the pattern variable is emitted as a phantom property and + // `callers Trim` would return caller_kind=property, caller_name=text. + // issue #233 第3次レビュー指摘: switch expression arm の `=>` が継続行にある場合でも、 + // arm 本体の呼び出しは外側関数に帰属しなければならない。継続 `=>` まで switch-expression + // ガードを広げないと、パターン変数が phantom property になり、`callers Trim` が + // caller_kind=property, caller_name=text を返してしまう。 + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_callers_csharp_ml_switch_arm"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "sample.cs"), + """ + class C + { + string M(object o) + { + return o switch + { + string text + => text.Trim(), + _ => "" + }; + } + } + """); + + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunCallers( + ["Trim", "--db", Path.Combine(projectRoot, ".cdidx", "codeindex.db"), "--json", "--exact-name", "--lang", "csharp"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("src/sample.cs", json.GetProperty("path").GetString()); + Assert.Equal("function", json.GetProperty("caller_kind").GetString()); + Assert.Equal("M", json.GetProperty("caller_name").GetString()); + Assert.Equal("Trim", json.GetProperty("callee_name").GetString()); + Assert.Equal(1, json.GetProperty("reference_count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunCallees_JsonZeroResults_WithMissingGraphTable_ReturnsDegradedPayload() + { + var (projectRoot, readOnlyUri) = CreateReadOnlyMissingGraphTableDb("cdidx_callees_zero_json_missing_graph"); + try + { + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunCallees( + ["Run", "--db", readOnlyUri, "--json", "--exact"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.False(json.GetProperty("graph_table_available").GetBoolean()); + Assert.True(json.GetProperty("degraded").GetBoolean()); + Assert.False(json.GetProperty("exact_index_available").GetBoolean()); + Assert.Contains("symbol_references table missing", json.GetProperty("degraded_reason").GetString()); + Assert.Equal(0, json.GetProperty("callees").GetArrayLength()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_StaleSqlGraphContractIncludesDegradedState() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_references_sql_graph_contract"); + try + { + var dbPath = CreateSqlGraphContractFixtureDb(projectRoot); + DowngradeSqlGraphContractRows(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["dbo.fn_Target", "--db", dbPath, "--json", "--lang", "sql", "--exact", "--count"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(1, json.GetProperty("count").GetInt32()); + Assert.False(json.GetProperty("exact_index_available").GetBoolean()); + Assert.False(json.GetProperty("sql_graph_contract_ready").GetBoolean()); + Assert.Contains("sql_graph_contract_ready=false", json.GetProperty("degraded_reason").GetString()); + Assert.Contains("sql_graph_contract_ready=false", json.GetProperty("sql_graph_contract_degraded_reason").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_JsonResults_StaleSqlGraphContractIncludesDegradedState() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_references_sql_graph_contract_results"); + try + { + var dbPath = CreateSqlGraphContractFixtureDb(projectRoot); + DowngradeSqlGraphContractRows(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["dbo.fn_Target", "--db", dbPath, "--json", "--lang", "sql"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("fn_Target", json.GetProperty("symbol_name").GetString()); + Assert.False(json.GetProperty("sql_graph_contract_ready").GetBoolean()); + Assert.Contains("sql_graph_contract_ready=false", json.GetProperty("sql_graph_contract_degraded_reason").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunCallers_JsonResults_StaleSqlGraphContractIncludesDegradedState() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_callers_sql_graph_contract_results"); + try + { + var dbPath = CreateSqlGraphContractFixtureDb(projectRoot); + DowngradeSqlGraphContractRows(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunCallers( + ["dbo.fn_Target", "--db", dbPath, "--json", "--lang", "sql"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("fn_Target", json.GetProperty("callee_name").GetString()); + Assert.False(json.GetProperty("sql_graph_contract_ready").GetBoolean()); + Assert.Contains("sql_graph_contract_ready=false", json.GetProperty("sql_graph_contract_degraded_reason").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunCallers_ExactJson_MixedRepoStaleSqlGraphContractDoesNotDegradePureCSharpQuery() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_callers_mixed_sql_graph_contract_results"); + try + { + var dbPath = CreateMixedSqlGraphContractFixtureDb(projectRoot); + DowngradeSqlGraphContractRows(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunCallers( + ["N", "--db", dbPath, "--json", "--exact"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("N", json.GetProperty("callee_name").GetString()); + Assert.False(json.TryGetProperty("sql_graph_contract_ready", out _)); + Assert.False(json.TryGetProperty("sql_graph_contract_degraded_reason", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunCallers_ExactCountJson_MixedRepoStaleSqlGraphContractDoesNotDegradePureCSharpQuery() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_callers_mixed_sql_graph_contract_count_pure_csharp"); + try + { + var dbPath = CreateMixedSqlGraphContractFixtureDb(projectRoot); + DowngradeSqlGraphContractRows(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunCallers( + ["N", "--db", dbPath, "--json", "--exact", "--count"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(1, json.GetProperty("count").GetInt32()); + Assert.Equal(1, json.GetProperty("files").GetInt32()); + Assert.False(json.TryGetProperty("sql_graph_contract_ready", out _)); + Assert.False(json.TryGetProperty("sql_graph_contract_degraded_reason", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactCountJson_MixedRepoStaleSqlGraphContractIncludesDegradedStateWhenCountContainsSql() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_references_mixed_sql_graph_contract_count"); + try + { + var dbPath = CreateMixedSqlGraphContractCountFixtureDb(projectRoot); + DowngradeMixedSqlGraphContractCountRows(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Target", "--db", dbPath, "--json", "--exact", "--count"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(2, json.GetProperty("count").GetInt32()); + Assert.Equal(2, json.GetProperty("files").GetInt32()); + Assert.False(json.GetProperty("sql_graph_contract_ready").GetBoolean()); + Assert.Contains("sql_graph_contract_ready=false", json.GetProperty("sql_graph_contract_degraded_reason").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunCallers_ExactCountJson_MixedRepoStaleSqlGraphContractIncludesDegradedStateWhenCountContainsSql() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_callers_mixed_sql_graph_contract_count"); + try + { + var dbPath = CreateMixedSqlGraphContractCountFixtureDb(projectRoot); + DowngradeMixedSqlGraphContractCountRows(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunCallers( + ["Target", "--db", dbPath, "--json", "--exact", "--count"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(2, json.GetProperty("count").GetInt32()); + Assert.Equal(2, json.GetProperty("files").GetInt32()); + Assert.False(json.GetProperty("sql_graph_contract_ready").GetBoolean()); + Assert.Contains("sql_graph_contract_ready=false", json.GetProperty("sql_graph_contract_degraded_reason").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunCallees_CountJson_MixedRepoStaleSqlGraphContractIncludesDegradedStateWhenCountContainsSql() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_callees_mixed_sql_graph_contract_count"); + try + { + var dbPath = CreateMixedSqlGraphContractCountFixtureDb(projectRoot); + DowngradeMixedSqlGraphContractCountRows(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunCallees( + ["Caller", "--db", dbPath, "--json", "--count"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(2, json.GetProperty("count").GetInt32()); + Assert.Equal(2, json.GetProperty("files").GetInt32()); + Assert.False(json.GetProperty("sql_graph_contract_ready").GetBoolean()); + Assert.Contains("sql_graph_contract_ready=false", json.GetProperty("sql_graph_contract_degraded_reason").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunCallees_JsonResults_StaleSqlGraphContractIncludesDegradedState() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_callees_sql_graph_contract_results"); + try + { + var dbPath = CreateSqlGraphContractFixtureDb(projectRoot); + DowngradeSqlGraphContractRows(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunCallees( + ["dbo.usp_Caller", "--db", dbPath, "--json", "--lang", "sql"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("dbo.usp_Caller", json.GetProperty("caller_name").GetString()); + Assert.False(json.GetProperty("sql_graph_contract_ready").GetBoolean()); + Assert.Contains("sql_graph_contract_ready=false", json.GetProperty("sql_graph_contract_degraded_reason").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunCallees_ExactJson_NormalizesBracketedSqlCallerNames() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_callees_sql_exact_bracketed"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/sql_exact_bracketed_callee_targets.sql", + "sql", + """ + CREATE PROCEDURE [dbo].[fn_Target] + AS + BEGIN + SELECT 1; + END + GO + + CREATE PROCEDURE [sales].[fn_Target] + AS + BEGIN + EXEC [sales].[fn_Target]; + EXEC fn_Target; + END + GO + """); + using (var db = new DbContext(dbPath)) + { + var writer = new DbWriter(db.Connection); + writer.MarkGraphReady(); + writer.MarkSqlGraphContractReady(); + } + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunCallees( + ["sales.fn_Target", "--db", dbPath, "--json", "--lang", "sql", "--exact"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("[sales].[fn_Target]", json.GetProperty("caller_name").GetString()); + Assert.Equal("fn_Target", json.GetProperty("callee_name").GetString()); + Assert.Equal(2, json.GetProperty("reference_count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunImpact_CountOnlyJson_StaleSqlGraphContractIncludesDegradedState() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_impact_sql_graph_contract"); + try + { + var dbPath = CreateSqlGraphContractFixtureDb(projectRoot); + DowngradeSqlGraphContractRows(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( + ["fn_Target", "--db", dbPath, "--json", "--lang", "sql", "--count"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(1, json.GetProperty("count").GetInt32()); + Assert.False(json.GetProperty("sql_graph_contract_ready").GetBoolean()); + Assert.Contains("sql_graph_contract_ready=false", json.GetProperty("sql_graph_contract_degraded_reason").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunImpact_ZeroJson_EmitsEnvelopeAndFreshness() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_zero_json_impact"); + try + { + var dbPath = CreateIndexedDbWithSingleFile(projectRoot, markGraphReady: true); + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( + ["DefinitelyMissingSymbol", "--db", dbPath, "--json", "--max-hops", "3"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + AssertZeroResultPayload(json, "callers"); + Assert.Equal("DefinitelyMissingSymbol", json.GetProperty("query").GetString()); + Assert.Equal(3, json.GetProperty("max_hops").GetInt32()); + Assert.Equal(3, json.GetProperty("max_depth").GetInt32()); + Assert.False(json.GetProperty("truncated").GetBoolean()); + Assert.True(json.GetProperty("graph_table_available").GetBoolean()); + Assert.Equal("no_matching_definition", json.GetProperty("zero_result_reason").GetString()); + Assert.Equal("resolution", json.GetProperty("suggestion_type").GetString()); + Assert.Equal("definition_not_found", Assert.Single(json.GetProperty("impact_failure_chain").EnumerateArray()).GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunImpact_ZeroDepthJson_ResolvesSymbolWithoutTraversingCallers() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_zero_depth_impact"); + try + { + var dbPath = CreateIndexedDbWithSingleFile(projectRoot, markGraphReady: true); + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( + ["HandleRequest", "--db", dbPath, "--json", "--max-hops", "0"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("HandleRequest", json.GetProperty("query").GetString()); + Assert.Equal(0, json.GetProperty("max_hops").GetInt32()); + Assert.Equal(0, json.GetProperty("max_depth").GetInt32()); + Assert.Equal(0, json.GetProperty("actual_depth").GetInt32()); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Equal(1, json.GetProperty("definition_count").GetInt32()); + Assert.Equal("depth_requested_zero", json.GetProperty("zero_result_reason").GetString()); + Assert.Equal("precondition", json.GetProperty("suggestion_type").GetString()); + Assert.Equal("depth_requested_zero", Assert.Single(json.GetProperty("impact_failure_chain").EnumerateArray()).GetString()); + Assert.Equal("Use `cdidx impact --max-hops 1` or higher to traverse callers.", json.GetProperty("suggestion").GetString()); + Assert.Empty(json.GetProperty("callers").EnumerateArray()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunImpact_StrictReturnsFeatureUnavailableForResolutionFailure() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_impact_strict_resolution_failure"); + try + { + var dbPath = CreateIndexedDbWithSingleFile(projectRoot, markGraphReady: true); + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( + ["DefinitelyMissingSymbol", "--db", dbPath, "--json", "--strict"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.FeatureUnavailable, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("no_matching_definition", json.GetProperty("zero_result_reason").GetString()); + Assert.Equal("definition_not_found", Assert.Single(json.GetProperty("impact_failure_chain").EnumerateArray()).GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_UnsupportedLanguageWithoutMatches_PrintsGraphSupportHint() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_refs"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["MissingSymbol", "--db", dbPath, "--lang", "markdown"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Contains("No references found.", stderr); + Assert.Contains("call-graph queries are not indexed for 'markdown'", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Theory] + [InlineData("references", "MissingSymbol")] + [InlineData("callers", "MissingSymbol")] + [InlineData("callees", "MissingSymbol")] + public void GraphCommands_SymbolKindArgumentWarnsAboutReferenceKindSemantics(string command, string query) + { + var projectRoot = TestProjectHelper.CreateTempProject($"cdidx_{command}_kind_warning"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, _, stderr) = CaptureConsole(() => RunGraphCommand( + command, + [query, "--db", dbPath, "--kind", "class"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Contains("symbol kind", stderr); + Assert.Contains("filters by reference kind", stderr); + Assert.Contains("call", stderr); + Assert.Contains("friend", stderr); + Assert.Contains("instantiate", stderr); + Assert.Contains("subscribe", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Theory] + [InlineData("references")] + [InlineData("callers")] + [InlineData("callees")] + public void GraphCommands_InvalidReferenceKindFailsWithScopedValidKindList(string command) + { + var args = new[] { "Target", "--kind", "badkind" }; + + var (exitCode, stdout, stderr) = CaptureConsole(() => RunGraphCommand(command, args, _jsonOptions)); + + Assert.Equal(CommandExitCodes.InvalidArgument, exitCode); + Assert.Equal(string.Empty, stdout); + Assert.Contains("invalid --kind value `badkind`", stderr); + Assert.Contains("Hint: use one of:", stderr); + Assert.Contains("call", stderr); + Assert.Contains(command == "references" ? "type_reference" : "friend", stderr); + Assert.Contains($"Usage: {ConsoleUi.GetUsageLine(command)}", stderr); + } + + [Fact] + public void RunReferences_CountJsonKeepsSubscribeRowsVisibleByDefault() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_references_subscribe_count"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Publisher.cs", "csharp", + """ + using System; + + public class Publisher + { + public event EventHandler? Changed; + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Subscriber.cs", "csharp", + """ + using System; + + public class Subscriber + { + public void Hook(Publisher publisher) + { + publisher.Changed += OnChanged; + } + + private void OnChanged(object? sender, EventArgs e) { } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Changed", "--db", dbPath, "--json", "--count", "--lang", "csharp", "--exact"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(1, json.GetProperty("count").GetInt32()); + Assert.Equal(1, json.GetProperty("files").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunCallers_JsonKeepsSubscribeRowsVisibleByDefault() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_callers_subscribe_default"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Publisher.cs", "csharp", + """ + using System; + + public class Publisher + { + public event EventHandler? Changed; + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Subscriber.cs", "csharp", + """ + using System; + + public class Subscriber + { + public void Hook(Publisher publisher) + { + publisher.Changed += OnChanged; + } + + private void OnChanged(object? sender, EventArgs e) { } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunCallers( + ["Changed", "--db", dbPath, "--json", "--lang", "csharp", "--exact"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("Hook", json.GetProperty("caller_name").GetString()); + Assert.Equal("Changed", json.GetProperty("callee_name").GetString()); + Assert.Equal("event", json.GetProperty("reference_kind").GetString()); + Assert.Equal(1, json.GetProperty("reference_count").GetInt32()); + // #501: every grouped caller row carries `reference_kinds` + + // `has_mixed_reference_kinds`, even when the row is single-kind, so AI + // clients never have to guess whether the field was omitted vs empty. + // #501: グループ化された caller 行は single-kind でも必ず `reference_kinds` / + // `has_mixed_reference_kinds` を返すため、AI クライアントは「未出力」と + // 「空配列」を判別せずに済む。 + var kinds = json.GetProperty("reference_kinds").EnumerateArray().Select(k => k.GetString()).ToArray(); + Assert.Equal(new[] { "event" }, kinds); + Assert.False(json.GetProperty("has_mixed_reference_kinds").GetBoolean()); + + var (rawExitCode, rawStdout, rawStderr) = CaptureConsole(() => QueryCommandRunner.RunCallers( + ["Changed", "--db", dbPath, "--json", "--lang", "csharp", "--exact", "--raw-kinds"], + _jsonOptions)); + using var rawDocument = ParseJsonOutput(rawStdout); + Assert.Equal(CommandExitCodes.Success, rawExitCode); + Assert.Equal(string.Empty, rawStderr); + Assert.Equal("subscribe", rawDocument.RootElement.GetProperty("reference_kind").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunCallers_JsonKeepsRazorEventBindingsVisibleByDefault() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_callers_razor_event_binding"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "Pages")); + File.WriteAllText( + Path.Combine(projectRoot, "Pages", "User.razor"), + """ + + + + @code { + void HandleClick() { } + } + """); + + var (indexExitCode, _, _) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunCallers( + ["HandleClick", "--db", dbPath, "--json", "--lang", "csharp", "--exact"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("HandleClick", json.GetProperty("callee_name").GetString()); + Assert.Equal("event", json.GetProperty("reference_kind").GetString()); + + var (referencesExitCode, referencesStdout, referencesStderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["HandleClick", "--db", dbPath, "--kind", "razor_event_binding", "--lang", "csharp", "--exact"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, referencesExitCode); + Assert.DoesNotContain("not a known reference kind", referencesStderr); + Assert.Contains("razor_event_binding", referencesStdout); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunCallers_SurfacesMixedReferenceKindsWhenContainerMixesCallAndSubscribe() + { + // #501: a single container that reaches the same callee via both `call` and + // `subscribe` must not collapse to a lone summary label. The grouped row + // must expose every distinct kind in JSON (`reference_kinds` / + // `has_mixed_reference_kinds`) and the human renderer must join them with + // `+` so operators see the mixed semantics at a glance. + // #501: 同一コンテナが同じ callee に対して `call` と `subscribe` の両方を持つ場合、 + // グループ化された caller 行は要約ラベル 1 つに潰さず、JSON では `reference_kinds` + // と `has_mixed_reference_kinds` で distinct kind をすべて返し、人間向け出力は + // `+` で連結して混在していることが一目で分かるようにする。 + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_callers_mixed_kind"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/MixedOwner.cs", "csharp", + """ + using System; + + public class MixedOwner + { + public event EventHandler? Changed; + + public void SetupAndFire() + { + Changed += OnChanged; + Changed(this, EventArgs.Empty); + } + + private void OnChanged(object? sender, EventArgs e) { } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (jsonExitCode, jsonStdout, jsonStderr) = CaptureConsole(() => QueryCommandRunner.RunCallers( + ["Changed", "--db", dbPath, "--json", "--lang", "csharp", "--exact"], + _jsonOptions)); + + using var document = ParseJsonOutput(jsonStdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, jsonExitCode); + Assert.Equal(string.Empty, jsonStderr); + Assert.Equal("SetupAndFire", json.GetProperty("caller_name").GetString()); + Assert.Equal("Changed", json.GetProperty("callee_name").GetString()); + Assert.Equal(2, json.GetProperty("reference_count").GetInt32()); + Assert.True(json.GetProperty("has_mixed_reference_kinds").GetBoolean()); + var kinds = json.GetProperty("reference_kinds").EnumerateArray().Select(k => k.GetString()).ToArray(); + Assert.Equal(new[] { "event", "invoke" }, kinds); + Assert.Equal(1, json.GetProperty("reference_kind_counts").GetProperty("call").GetInt32()); + Assert.Equal(1, json.GetProperty("reference_kind_counts").GetProperty("subscribe").GetInt32()); + Assert.Equal("event", json.GetProperty("reference_kind").GetString()); + + var (humanExitCode, humanStdout, humanStderr) = CaptureConsole(() => QueryCommandRunner.RunCallers( + ["Changed", "--db", dbPath, "--lang", "csharp", "--exact"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, humanExitCode); + Assert.Contains("call, subscribe", humanStdout); + Assert.Contains("SetupAndFire", humanStdout); + Assert.Contains("-> Changed (2 refs)", humanStdout); + Assert.Contains("(1 callers in 1 files)", humanStderr); + + var (rawExitCode, rawStdout, rawStderr) = CaptureConsole(() => QueryCommandRunner.RunCallers( + ["Changed", "--db", dbPath, "--json", "--lang", "csharp", "--exact", "--raw-kinds"], + _jsonOptions)); + using var rawDocument = ParseJsonOutput(rawStdout); + var rawKinds = rawDocument.RootElement.GetProperty("reference_kinds").EnumerateArray().Select(k => k.GetString()).ToArray(); + Assert.Equal(CommandExitCodes.Success, rawExitCode); + Assert.Equal(string.Empty, rawStderr); + Assert.Equal(new[] { "call", "subscribe" }, rawKinds); + Assert.Equal("subscribe", rawDocument.RootElement.GetProperty("reference_kind").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunCallers_Json_CSharpTopLevelStatementsUseSyntheticTopLevelCaller() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_callers_csharp_toplevel"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Program.cs", "csharp", + """ + using System; + + Console.WriteLine("boot"); + + void Run() + { + Console.WriteLine("inside"); + } + + Run(); + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunCallers( + ["Run", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("function", json.GetProperty("caller_kind").GetString()); + Assert.Equal("", json.GetProperty("caller_name").GetString()); + Assert.Equal("Run", json.GetProperty("callee_name").GetString()); + Assert.Equal(1, json.GetProperty("reference_count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunCallers_WithExplicitKind_CSharpTopLevelStatementsUseSyntheticTopLevelCaller() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_callers_csharp_toplevel_kind"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Program.cs", "csharp", + """ + using System; + + Console.WriteLine("boot"); + + void Run() + { + Console.WriteLine("inside"); + } + + Run(); + """); + MarkGraphAndFoldReady(dbPath); + + var (humanExitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunCallers( + ["Run", "--db", dbPath, "--lang", "csharp", "--exact-name", "--kind", "call"], + _jsonOptions)); + var (jsonExitCode, jsonStdout, jsonStderr) = CaptureConsole(() => QueryCommandRunner.RunCallers( + ["Run", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name", "--kind", "call"], + _jsonOptions)); + + using var document = ParseJsonOutput(jsonStdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, humanExitCode); + Assert.Equal(CommandExitCodes.Success, jsonExitCode); + Assert.Contains("(1 callers in 1 files)", stderr); + Assert.Equal(string.Empty, jsonStderr); + Assert.Contains("function", stdout); + Assert.Contains("", stdout); + Assert.Equal("function", json.GetProperty("caller_kind").GetString()); + Assert.Equal("", json.GetProperty("caller_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunCallers_HumanOutput_ShowsReferenceKindPerRow() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_callers_human_reference_kind"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/BaseWidget.cs", "csharp", + """ + public class BaseWidget + { + public BaseWidget() { } + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/DerivedWidget.cs", "csharp", + """ + public class DerivedWidget : BaseWidget + { + public DerivedWidget() : base() { } + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Factory.cs", "csharp", + """ + public class Factory + { + public BaseWidget Make() => new BaseWidget(); + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunCallers( + ["BaseWidget", "--db", dbPath, "--lang", "csharp", "--exact"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Contains("call function DerivedWidget", stdout); + Assert.Contains("src/DerivedWidget.cs:3 -> BaseWidget (1 refs)", stdout); + Assert.Contains("instantiate function Make", stdout); + Assert.Contains("src/Factory.cs:3 -> BaseWidget (1 refs)", stdout); + Assert.Contains("(2 callers in 2 files)", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunCallees_JsonKeepsSubscribeRowsVisibleByDefault() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_callees_subscribe_default"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Publisher.cs", "csharp", + """ + using System; + + public class Publisher + { + public event EventHandler? Changed; + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Subscriber.cs", "csharp", + """ + using System; + + public class Subscriber + { + public void Hook(Publisher publisher) + { + publisher.Changed += OnChanged; + } + + private void OnChanged(object? sender, EventArgs e) { } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunCallees( + ["Hook", "--db", dbPath, "--json", "--lang", "csharp", "--exact"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("Hook", json.GetProperty("caller_name").GetString()); + Assert.Equal("Changed", json.GetProperty("callee_name").GetString()); + Assert.Equal("event", json.GetProperty("reference_kind").GetString()); + Assert.Equal(1, json.GetProperty("reference_count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpEnumMembersReturnIndexedReferences() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_references"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public class Outer + { + public enum First { None } + } + + public enum Nested + { + A = 1, + B = A + } + + public class UsesEnum + { + public void Use() + { + _ = Nested.A; + _ = Outer.First.None; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["A", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + 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("function", json.GetProperty("container_kind").GetString()); + Assert.Equal("Use", json.GetProperty("container_name").GetString()); + Assert.True(json.GetProperty("exact_index_available").GetBoolean()); + Assert.False(json.TryGetProperty("graph_degraded", out _)); + Assert.False(json.TryGetProperty("unsupported_symbol_kind", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunCallers_ExactCountJson_LargeMixedCandidateSetStillMarksEnumMemberGap() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_callers_large_mixed_exact_count"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + for (var i = 0; i < 70; i++) + { + TestProjectHelper.InsertIndexedFile(dbPath, $"src/Worker{i}.cs", "csharp", + $$""" + namespace Demo; + + public class Worker{{i}} + { + public void Ready() { } + } + """); + } + + TestProjectHelper.InsertIndexedFile(dbPath, "src/Status.cs", "csharp", + """ + namespace Demo; + + public enum Status + { + Ready + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunCallers( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name", "--count"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.True(json.GetProperty("exact_index_available").GetBoolean()); + Assert.False(json.TryGetProperty("unsupported_symbol_kind", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunCallers_ExactJson_CSharpEnumMember_ReturnsIndexedCaller() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_callers_enum_member_gap"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public enum Nested + { + A = 1, + B = A + } + + public class UsesEnum + { + public Nested Value => Nested.A; + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunCallers( + ["A", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("src/cases.cs", json.GetProperty("path").GetString()); + Assert.Equal("property", json.GetProperty("caller_kind").GetString()); + Assert.Equal("Value", json.GetProperty("caller_name").GetString()); + Assert.Equal("A", json.GetProperty("callee_name").GetString()); + Assert.Equal(1, json.GetProperty("reference_count").GetInt32()); + Assert.True(json.GetProperty("exact_index_available").GetBoolean()); + Assert.False(json.TryGetProperty("unsupported_symbol_kind", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_ZeroResultsWithoutOverride_UsesZeroSchema() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_references_exact_zero_schema"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public class Worker + { + public void Ready() { } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Equal(0, json.GetProperty("references").GetArrayLength()); + Assert.True(json.GetProperty("exact_index_available").GetBoolean()); + Assert.False(json.TryGetProperty("unsupported_symbol_kind", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_WithResults_StaysCleanWhenEnumMembersAlsoExist() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_references_enum_member_success_metadata"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public class Worker + { + public void A() { } + + public void Use() + { + A(); + } + } + + public enum Status + { + A + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["A", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("src/cases.cs", json.GetProperty("path").GetString()); + Assert.Equal("A", json.GetProperty("symbol_name").GetString()); + Assert.Equal("Use", json.GetProperty("container_name").GetString()); + Assert.True(json.GetProperty("exact_index_available").GetBoolean()); + Assert.False(json.TryGetProperty("unsupported_symbol_kind", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_WithoutLang_MixedCallableAndEnumMember_ReturnsPrimaryHit() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_references_exact_mixed_without_lang"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "web/app.js", "javascript", + """ + function Ready() {} + + Ready(); + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/status.cs", "csharp", + """ + namespace Demo; + + public enum Status + { + Ready + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("web/app.js", json.GetProperty("path").GetString()); + Assert.Equal("javascript", json.GetProperty("lang").GetString()); + Assert.Equal("Ready", json.GetProperty("symbol_name").GetString()); + Assert.True(json.GetProperty("exact_index_available").GetBoolean()); + Assert.False(json.TryGetProperty("unsupported_symbol_kind", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunCallers_ExactJson_WithResults_StayCleanWhenEnumMembersAlsoExist() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_callers_enum_member_success_metadata"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public class Worker + { + public void A() { } + + public void Use() + { + A(); + } + } + + public enum Status + { + A + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunCallers( + ["A", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("src/cases.cs", json.GetProperty("path").GetString()); + Assert.Equal("Use", json.GetProperty("caller_name").GetString()); + Assert.Equal("A", json.GetProperty("callee_name").GetString()); + Assert.Equal(1, json.GetProperty("reference_count").GetInt32()); + Assert.True(json.GetProperty("exact_index_available").GetBoolean()); + Assert.False(json.TryGetProperty("unsupported_symbol_kind", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunCallers_ExactJson_ZeroResultsWithoutOverride_UsesZeroSchema() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_callers_exact_zero_schema"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public class Worker + { + public void Ready() { } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunCallers( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Equal(0, json.GetProperty("callers").GetArrayLength()); + Assert.True(json.GetProperty("exact_index_available").GetBoolean()); + Assert.False(json.TryGetProperty("unsupported_symbol_kind", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunCallees_ExactJson_CSharpEnumMember_UsesZeroSchema() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_callees_enum_member_gap"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public enum Nested + { + A = 1, + B = A + } + + public class UsesEnum + { + public Nested Value => Nested.A; + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunCallees( + ["A", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Equal(0, json.GetProperty("callees").GetArrayLength()); + Assert.True(json.GetProperty("exact_index_available").GetBoolean()); + Assert.False(json.TryGetProperty("unsupported_symbol_kind", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunCallees_ExactJson_ZeroResultsWithoutOverride_UsesZeroSchema() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_callees_exact_zero_schema"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public class Worker + { + public void Ready() { } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunCallees( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Equal(0, json.GetProperty("callees").GetArrayLength()); + Assert.True(json.GetProperty("exact_index_available").GetBoolean()); + Assert.False(json.TryGetProperty("unsupported_symbol_kind", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunCallees_ExactJson_WithResults_StayCleanWhenEnumMembersAlsoExist() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_callees_enum_member_success_metadata"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public class Worker + { + public void A() + { + B(); + } + + public void B() { } + } + + public enum Status + { + A + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunCallees( + ["A", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("src/cases.cs", json.GetProperty("path").GetString()); + Assert.Equal("A", json.GetProperty("caller_name").GetString()); + Assert.Equal("B", json.GetProperty("callee_name").GetString()); + Assert.Equal("invoke", json.GetProperty("reference_kind").GetString()); + Assert.True(json.GetProperty("exact_index_available").GetBoolean()); + Assert.False(json.TryGetProperty("unsupported_symbol_kind", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CrossLanguageMixedHitDoesNotForceCSharpGraphLanguage() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_references_cross_language_mixed"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "web/app.js", "javascript", + """ + export function Ready() {} + + Ready(); + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/status.cs", "csharp", + """ + namespace Demo; + + public enum Status + { + Ready + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("web/app.js", json.GetProperty("path").GetString()); + Assert.Equal("javascript", json.GetProperty("lang").GetString()); + Assert.Equal("Ready", json.GetProperty("symbol_name").GetString()); + Assert.True(json.GetProperty("exact_index_available").GetBoolean()); + Assert.False(json.TryGetProperty("unsupported_symbol_kind", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpNonEnumQualifiedMemberAccessDoesNotLeakAsEnumMemberReference() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_false_positive"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public enum EnumHolder + { + A = 1 + } + + public static class Values + { + public static int Alpha = 1; + } + + public class UsesValues + { + public int Read() + { + return Values.A; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["A", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Equal(0, json.GetProperty("references").GetArrayLength()); + Assert.True(json.GetProperty("exact_index_available").GetBoolean()); + Assert.False(json.TryGetProperty("unsupported_symbol_kind", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpEnumMemberRepeatedAliasNamesUseNearestAliasScope() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_repeated_alias_scope"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo + { + public enum Status + { + Ready + } + + public static class Values + { + public static int Ready = 1; + } + } + + namespace B + { + using Alias = Demo.Values; + + public class UsesValues + { + public int Read() + { + return Alias.Ready; + } + } + } + + namespace C + { + using Alias = Demo.Status; + + public class UsesEnum + { + public Demo.Status Read() + { + return Alias.Ready; + } + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal(35, json.GetProperty("line").GetInt32()); + Assert.Equal("Read", json.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpLaterSiblingAliasRebindingDoesNotStealEarlierEnumScope() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_alias_rebinding"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo + { + public enum Status + { + Ready + } + + public static class Values + { + public static int Ready = 1; + } + } + + namespace B + { + using Alias = Demo.Status; + + public class UsesEnum + { + public Demo.Status Read() + { + return Alias.Ready; + } + } + } + + namespace C + { + using Alias = Demo.Values; + + public class UsesValues + { + public int Read() + { + return Alias.Ready; + } + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + 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("Read", json.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpInstancePropertyShadowDoesNotHideStaticEnumReference() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_static_method_instance_property"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Holder Status { get; } = new(); + + public static Demo.Status Read() + { + return Status.Ready; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal(19, json.GetProperty("line").GetInt32()); + Assert.Equal("Read", json.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpPropertyAccessorLocalShadowingDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_property_accessor_collision"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Demo.Status Value + { + get + { + Holder Status = new(); + _ = Status.Ready; + return Demo.Status.Ready; + } + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("Value", json.GetProperty("container_name").GetString()); + Assert.Equal("property", json.GetProperty("container_kind").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpGetterLocalShadowingDoesNotLeakIntoSetter() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_property_accessor_scope_end"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Status Value + { + get + { + Holder Status = new(); + _ = Status.Ready; + return Demo.Status.Ready; + } + set + { + _ = Status.Ready; + } + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name", "--limit", "10"], + _jsonOptions)); + + var rows = ParseJsonLines(stdout); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal([21, 25], rows.Select(row => row.RootElement.GetProperty("line").GetInt32()).OrderBy(line => line).ToArray()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpOutDeclarationShadowingDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_out_declaration_collision"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + private static bool TryGet(out Holder holder) + { + holder = new Holder(); + return true; + } + + public Demo.Status Read() + { + if (TryGet(out Holder Status)) + { + _ = Status.Ready; + } + + return Demo.Status.Ready; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("Read", json.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpCatchShadowingDoesNotLeakAfterCatchBlock() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_catch_scope_end"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + using System; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Status Read() + { + try + { + throw new Exception(); + } + catch (Exception Status) + { + _ = Status.Message; + } + + return Status.Ready; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("Read", json.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpUsingStatementShadowingDoesNotLeakAfterUsingBlock() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_using_scope_end"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + using System; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder : IDisposable + { + public int Ready { get; set; } + + public void Dispose() + { + } + } + + public sealed class Uses + { + public Status Read(bool flag) + { + if (flag) + { + using (Holder Status = new()) + { + _ = Status.Ready; + } + } + + return Status.Ready; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("Read", json.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpQueryRangeVariableNamedLikeEnum_OrderByCommaDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_query_orderby_comma_collision"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public IEnumerable Read(IEnumerable items) + { + return from Status in items + orderby Status, items.Count() + select Status.Ready; + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Empty(json.GetProperty("references").EnumerateArray()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpQueryRangeVariableMemberNamedSelectDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_query_orderby_member_named_select_collision"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + public int select { get; set; } + } + + public sealed class Uses + { + public IEnumerable Read(IEnumerable items) + { + return from Status in items + orderby Status.select, items.Count() + select Status.Ready; + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Empty(json.GetProperty("references").EnumerateArray()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpQueryRangeVariableOrderByAnonymousTypeCommaDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_query_orderby_anonymous_type_collision"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public IEnumerable Read(IEnumerable items) + { + return from Status in items + orderby new { X = Status.Ready, Y = items.Count() }, items.Count() + select Status.Ready; + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Empty(json.GetProperty("references").EnumerateArray()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpQueryRangeVariableNestedQueryBeforeOrderByCommaDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_nested_query_orderby_collision"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public IEnumerable Read(IEnumerable items, IEnumerable others) + { + return from Status in items + let nested = from x in others select x + orderby items.Count(), nested.Count() + select Status.Ready; + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Empty(json.GetProperty("references").EnumerateArray()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpQueryKeywordNamedLocalFunctionInSelectExpressionPreservesLaterEnumReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_query_keyword_local_function"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public static class Sink + { + public static Status Pick(IEnumerable left, Status right) => right; + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Status Read(IEnumerable items) + { + static int from(IEnumerable xs) => xs.Count(); + return Sink.Pick(from Status in items select from(items), Status.Ready); + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + var rows = ParseJsonLines(stdout); + var row = Assert.Single(rows).RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("Ready", row.GetProperty("symbol_name").GetString()); + Assert.Equal(26, row.GetProperty("line").GetInt32()); + Assert.Contains("Status.Ready", row.GetProperty("context").GetString(), StringComparison.Ordinal); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpParenthesizedTerminalSelectInArgumentPreservesLaterEnumReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_parenthesized_terminal_select"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public static class Sink + { + public static Status Pick(object left, Status right) => right; + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Status Read(IEnumerable items) + { + return Sink.Pick(from Status in items select(Status.Ready), Status.Ready); + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + var rows = ParseJsonLines(stdout); + var row = Assert.Single(rows).RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("Ready", row.GetProperty("symbol_name").GetString()); + Assert.Equal("Read", row.GetProperty("container_name").GetString()); + Assert.Contains("Status.Ready", row.GetProperty("context").GetString(), StringComparison.Ordinal); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpGroupByQueryInArgumentPreservesLaterEnumReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_group_by_argument"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public static class Sink + { + public static object Pick(object left, Status right) => right; + } + + public sealed class Uses + { + public object Read(IEnumerable items) + { + return Sink.Pick(from Status in items group Status.Ready by items.Count(), Status.Ready); + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + var rows = ParseJsonLines(stdout); + var row = Assert.Single(rows).RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("Ready", row.GetProperty("symbol_name").GetString()); + Assert.Equal(25, row.GetProperty("line").GetInt32()); + Assert.Contains("Status.Ready", row.GetProperty("context").GetString(), StringComparison.Ordinal); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpParenthesizedGroupByQueryInArgumentPreservesLaterEnumReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_parenthesized_group_by_argument"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public static class Sink + { + public static Status Pick(object left, Status right) => right; + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Status Read(IEnumerable items) + { + return Sink.Pick(from Status in items group(Status.Ready) by items.Count(), Status.Ready); + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + var rows = ParseJsonLines(stdout); + var row = Assert.Single(rows).RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("Ready", row.GetProperty("symbol_name").GetString()); + Assert.Equal("Read", row.GetProperty("container_name").GetString()); + Assert.Contains("Status.Ready", row.GetProperty("context").GetString(), StringComparison.Ordinal); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpQueryKeywordNamedLocalFunctionInParenthesizedOrderByExpressionDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_parenthesized_orderby_keyword_local_function"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public IEnumerable Read(IEnumerable items) + { + static int select(IEnumerable xs) => xs.Count(); + return from Status in items + orderby select(items), items.Count() + select Status.Ready; + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Empty(json.GetProperty("references").EnumerateArray()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpQueryKeywordNamedLocalFunctionAfterGreaterThanInOrderByTernaryDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_parenthesized_orderby_greater_than_local_function"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public IEnumerable Read(IEnumerable items) + { + static int select(IEnumerable xs) => xs.Count(); + return from Status in items + orderby items.Count() > select(items) ? 1 : 0, items.Count() + select Status.Ready; + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Empty(json.GetProperty("references").EnumerateArray()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpQueryKeywordNamedLocalFunctionAfterLessThanInOrderByTernaryDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_parenthesized_orderby_less_than_local_function"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public IEnumerable Read(IEnumerable items) + { + static int select(IEnumerable xs) => xs.Count(); + return from Status in items + orderby items.Count() < select(items) ? 1 : 0, items.Count() + select Status.Ready; + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Empty(json.GetProperty("references").EnumerateArray()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpQueryKeywordNamedLocalFunctionAfterBangInOrderByTernaryDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_parenthesized_orderby_bang_local_function"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public IEnumerable Read(IEnumerable items) + { + static bool select(IEnumerable xs) => xs.Any(); + return from Status in items + orderby ! select(items) ? 1 : 0, items.Count() + select Status.Ready; + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Empty(json.GetProperty("references").EnumerateArray()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpAwaitBeforeQueryKeywordNamedLocalFunctionInOrderByDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_parenthesized_orderby_await_local_function"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public async Task> Read(IEnumerable items) + { + static async Task select(IEnumerable xs) => await Task.FromResult(xs.Count()); + return from Status in items + orderby await select(items), items.Count() + select Status.Ready; + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Empty(json.GetProperty("references").EnumerateArray()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpCommentSeparatedAwaitBeforeQueryKeywordNamedLocalFunctionInOrderByDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_parenthesized_orderby_await_local_function_comment_gap"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public async Task> Read(IEnumerable items) + { + static async Task select(IEnumerable xs) => await Task.FromResult(xs.Count()); + return from Status in items + orderby await select /*comment*/ (items), items.Count() + select Status.Ready; + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Empty(json.GetProperty("references").EnumerateArray()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpPostfixNullForgivingBeforeParenthesizedTerminalSelectPreservesLaterEnumReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_parenthesized_terminal_select_after_null_forgiving"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public static class Sink + { + public static Status Pick(object left, Status right) => right; + public static Holder? Maybe(Holder value) => value; + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Status Read(IEnumerable items) + { + return Sink.Pick(from Status in items + let alias = Sink.Maybe(Status)! + select(Status.Ready), + Status.Ready); + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + var rows = ParseJsonLines(stdout); + var row = Assert.Single(rows).RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("Ready", row.GetProperty("symbol_name").GetString()); + Assert.Equal("Read", row.GetProperty("container_name").GetString()); + Assert.Contains("Status.Ready", row.GetProperty("context").GetString(), StringComparison.Ordinal); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpThrowBeforeQueryKeywordNamedLocalFunctionInOrderByDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_parenthesized_orderby_throw_select_local_function"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public IEnumerable Read(IEnumerable items) + { + static System.Exception select(IEnumerable xs) => new System.Exception(xs.Count().ToString()); + return from Status in items + orderby items.Count() > 0 ? throw select(items) : 0, items.Count() + select Status.Ready; + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Empty(json.GetProperty("references").EnumerateArray()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpThrowBeforeGroupNamedLocalFunctionInOrderByDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_parenthesized_orderby_throw_group_local_function"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public IEnumerable Read(IEnumerable items) + { + static System.Exception group(IEnumerable xs) => new System.Exception(xs.Count().ToString()); + return from Status in items + orderby items.Count() > 0 ? throw group(items) : 0, items.Count() + select Status.Ready; + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Empty(json.GetProperty("references").EnumerateArray()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpMultilineParenthesizedSelectAfterGreaterThanDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_parenthesized_orderby_multiline_select_after_greater_than"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public IEnumerable Read(IEnumerable items) + { + static int select(IEnumerable xs) => xs.Count(); + return from Status in items + orderby items.Count() > + select + (items) ? 1 : 0, items.Count() + select Status.Ready; + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Empty(json.GetProperty("references").EnumerateArray()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CssScssVariableAndExtendReferences_AreVisible() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_css_scss_variable_and_extend_references"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "styles")); + File.WriteAllText( + Path.Combine(projectRoot, "styles", "theme.scss"), + """ + $primary: #3366cc; + $spacing-base: 8px; + + @mixin rounded($radius) { + border-radius: $radius; + } + + %button-base { + padding: 4px; + } + + .button { + color: $primary; + padding: $spacing-base * 2; + @include rounded(4px); + } + + .card { + @extend %button-base; + border: 1px solid $primary; + } + """); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (primaryExitCode, primaryStdout, primaryStderr) = RunBuiltCli(["references", "$primary", "--db", dbPath, "--json", "--lang", "css", "--exact-name"]); + var (spacingExitCode, spacingStdout, spacingStderr) = RunBuiltCli(["references", "spacing-base", "--db", dbPath, "--json", "--lang", "css", "--exact-name"]); + var (buttonExitCode, buttonStdout, buttonStderr) = RunBuiltCli(["references", "%button-base", "--db", dbPath, "--json", "--lang", "css", "--exact-name"]); + var (radiusExitCode, radiusStdout, radiusStderr) = RunBuiltCli(["references", "radius", "--db", dbPath, "--json", "--lang", "css", "--exact-name"]); + + var primaryRows = ParseJsonLines(primaryStdout); + var spacingRows = ParseJsonLines(spacingStdout); + var buttonRows = ParseJsonLines(buttonStdout); + var radiusRows = ParseJsonLines(radiusStdout); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + + Assert.Equal(CommandExitCodes.Success, primaryExitCode); + Assert.Equal(string.Empty, primaryStderr); + Assert.Equal(2, primaryRows.Count); + Assert.All(primaryRows, row => Assert.Equal("primary", row.RootElement.GetProperty("symbol_name").GetString())); + Assert.All(primaryRows, row => Assert.Equal("call", row.RootElement.GetProperty("reference_kind").GetString())); + + Assert.Equal(CommandExitCodes.Success, spacingExitCode); + Assert.Equal(string.Empty, spacingStderr); + var spacingRow = Assert.Single(spacingRows); + Assert.Equal("spacing-base", spacingRow.RootElement.GetProperty("symbol_name").GetString()); + Assert.Equal("call", spacingRow.RootElement.GetProperty("reference_kind").GetString()); + + Assert.Equal(CommandExitCodes.Success, buttonExitCode); + Assert.Equal(string.Empty, buttonStderr); + var buttonRow = Assert.Single(buttonRows); + Assert.Equal("%button-base", buttonRow.RootElement.GetProperty("symbol_name").GetString()); + Assert.Equal("call", buttonRow.RootElement.GetProperty("reference_kind").GetString()); + + Assert.Equal(CommandExitCodes.Success, radiusExitCode); + Assert.Equal(string.Empty, radiusStderr); + var radiusRow = Assert.Single(radiusRows); + Assert.Equal("radius", radiusRow.RootElement.GetProperty("symbol_name").GetString()); + Assert.Equal("call", radiusRow.RootElement.GetProperty("reference_kind").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_SqlModifierPrefixedObjectsResolveRealNames() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_sql_modifier_prefixed_objects"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "sql")); + File.WriteAllText( + Path.Combine(projectRoot, "sql", "repro.sql"), + """ + SELECT TOP (10) * FROM top_users; + UPDATE TOP (10) audit_log SET action = 'done'; + DELETE TOP (5) FROM audit_log; + SELECT * FROM ONLY public.users; + UPDATE ONLY public.users SET active = true; + SELECT * FROM LATERAL fn_users(42); + MERGE TOP (5) audit_log AS t USING staging_log AS s ON t.id = s.id WHEN MATCHED THEN UPDATE SET action = s.action; + INSERT TOP (10) INTO inserted_log (action) VALUES ('done'); + INSERT TOP (2) INTO #inserted_batch (action) VALUES ('queued'); + SELECT * FROM #inserted_batch; + MERGE TOP (5) #batch_log AS u USING staging_batch AS v ON u.id = v.id WHEN MATCHED THEN UPDATE SET action = v.action; + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (auditExitCode, auditStdout, auditStderr) = RunBuiltCli(["references", "audit_log", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (insertedExitCode, insertedStdout, insertedStderr) = RunBuiltCli(["references", "inserted_log", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (insertedBatchExitCode, insertedBatchStdout, insertedBatchStderr) = RunBuiltCli(["references", "#inserted_batch", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (batchExitCode, batchStdout, batchStderr) = RunBuiltCli(["references", "#batch_log", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (batchSourceExitCode, batchSourceStdout, batchSourceStderr) = RunBuiltCli(["references", "staging_batch", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (topUsersExitCode, topUsersStdout, topUsersStderr) = RunBuiltCli(["references", "top_users", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (usersExitCode, usersStdout, usersStderr) = RunBuiltCli(["references", "users", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (fnExitCode, fnStdout, fnStderr) = RunBuiltCli(["references", "fn_users", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (topExitCode, topStdout, topStderr) = RunBuiltCli(["references", "TOP", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (onlyExitCode, onlyStdout, onlyStderr) = RunBuiltCli(["references", "ONLY", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (lateralExitCode, lateralStdout, lateralStderr) = RunBuiltCli(["references", "LATERAL", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + + var auditRows = ParseJsonLines(auditStdout); + var insertedRows = ParseJsonLines(insertedStdout); + var insertedBatchRows = ParseJsonLines(insertedBatchStdout); + var batchRows = ParseJsonLines(batchStdout); + var batchSourceRows = ParseJsonLines(batchSourceStdout); + var topUsersRows = ParseJsonLines(topUsersStdout); + var usersRows = ParseJsonLines(usersStdout); + var fnRows = ParseJsonLines(fnStdout); + using var topDocument = ParseJsonOutput(topStdout); + using var onlyDocument = ParseJsonOutput(onlyStdout); + using var lateralDocument = ParseJsonOutput(lateralStdout); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + + Assert.Equal(CommandExitCodes.Success, auditExitCode); + Assert.Equal(string.Empty, auditStderr); + Assert.Equal(3, auditRows.Count); + Assert.All(auditRows, row => Assert.Equal("audit_log", row.RootElement.GetProperty("symbol_name").GetString())); + + Assert.Equal(CommandExitCodes.Success, insertedExitCode); + Assert.Equal(string.Empty, insertedStderr); + var insertedRow = Assert.Single(insertedRows); + Assert.Equal("inserted_log", insertedRow.RootElement.GetProperty("symbol_name").GetString()); + + Assert.Equal(CommandExitCodes.Success, insertedBatchExitCode); + Assert.Equal(string.Empty, insertedBatchStderr); + Assert.Equal(2, insertedBatchRows.Count); + Assert.All(insertedBatchRows, row => Assert.Equal("#inserted_batch", row.RootElement.GetProperty("symbol_name").GetString())); + + Assert.Equal(CommandExitCodes.Success, batchExitCode); + Assert.Equal(string.Empty, batchStderr); + var batchRow = Assert.Single(batchRows); + Assert.Equal("#batch_log", batchRow.RootElement.GetProperty("symbol_name").GetString()); + + Assert.Equal(CommandExitCodes.Success, batchSourceExitCode); + Assert.Equal(string.Empty, batchSourceStderr); + var batchSourceRow = Assert.Single(batchSourceRows); + Assert.Equal("staging_batch", batchSourceRow.RootElement.GetProperty("symbol_name").GetString()); + + Assert.Equal(CommandExitCodes.Success, topUsersExitCode); + Assert.Equal(string.Empty, topUsersStderr); + var topUsersRow = Assert.Single(topUsersRows); + Assert.Equal("top_users", topUsersRow.RootElement.GetProperty("symbol_name").GetString()); + + Assert.Equal(CommandExitCodes.Success, usersExitCode); + Assert.Equal(string.Empty, usersStderr); + Assert.Equal(2, usersRows.Count); + Assert.All(usersRows, row => Assert.Equal("users", row.RootElement.GetProperty("symbol_name").GetString())); + + Assert.Equal(CommandExitCodes.Success, fnExitCode); + Assert.Equal(string.Empty, fnStderr); + var fnRow = Assert.Single(fnRows); + Assert.Equal("fn_users", fnRow.RootElement.GetProperty("symbol_name").GetString()); + Assert.Equal("call", fnRow.RootElement.GetProperty("reference_kind").GetString()); + + Assert.Equal(CommandExitCodes.Success, topExitCode); + Assert.Equal(string.Empty, topStderr); + Assert.Equal(0, topDocument.RootElement.GetProperty("count").GetInt32()); + + Assert.Equal(CommandExitCodes.Success, onlyExitCode); + Assert.Equal(string.Empty, onlyStderr); + Assert.Equal(0, onlyDocument.RootElement.GetProperty("count").GetInt32()); + + Assert.Equal(CommandExitCodes.Success, lateralExitCode); + Assert.Equal(string.Empty, lateralStderr); + Assert.Equal(0, lateralDocument.RootElement.GetProperty("count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_SqlTruncateTargetsHandleOnlyAndMultipleTargets() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_sql_truncate_targets"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "sql")); + File.WriteAllText( + Path.Combine(projectRoot, "sql", "repro.sql"), + """ + TRUNCATE TABLE ONLY public.users; + TRUNCATE TABLE audit_log, archived_log; + TRUNCATE TABLE [dbo].[users], `analytics`.`logs`, "public"."accounts"; + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (usersExitCode, usersStdout, usersStderr) = RunBuiltCli(["references", "users", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (archivedExitCode, archivedStdout, archivedStderr) = RunBuiltCli(["references", "archived_log", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (logsExitCode, logsStdout, logsStderr) = RunBuiltCli(["references", "logs", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (accountsExitCode, accountsStdout, accountsStderr) = RunBuiltCli(["references", "accounts", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (onlyExitCode, onlyStdout, onlyStderr) = RunBuiltCli(["references", "ONLY", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (qualifiedExitCode, qualifiedStdout, qualifiedStderr) = RunBuiltCli(["references", "public.users", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (mangledBracketExitCode, mangledBracketStdout, mangledBracketStderr) = RunBuiltCli(["references", "dbo].[users", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (mangledBacktickExitCode, mangledBacktickStdout, mangledBacktickStderr) = RunBuiltCli(["references", "analytics`.`logs", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (mangledDoubleQuoteExitCode, mangledDoubleQuoteStdout, mangledDoubleQuoteStderr) = RunBuiltCli(["references", "public\".\"accounts", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + + var usersRows = ParseJsonLines(usersStdout); + var archivedRows = ParseJsonLines(archivedStdout); + var logsRows = ParseJsonLines(logsStdout); + var accountsRows = ParseJsonLines(accountsStdout); + using var onlyDocument = ParseJsonOutput(onlyStdout); + using var qualifiedDocument = ParseJsonOutput(qualifiedStdout); + using var mangledBracketDocument = ParseJsonOutput(mangledBracketStdout); + using var mangledBacktickDocument = ParseJsonOutput(mangledBacktickStdout); + using var mangledDoubleQuoteDocument = ParseJsonOutput(mangledDoubleQuoteStdout); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + + Assert.Equal(CommandExitCodes.Success, usersExitCode); + Assert.Equal(string.Empty, usersStderr); + Assert.Equal(2, usersRows.Count); + + Assert.Equal(CommandExitCodes.Success, archivedExitCode); + Assert.Equal(string.Empty, archivedStderr); + Assert.Single(archivedRows); + + Assert.Equal(CommandExitCodes.Success, logsExitCode); + Assert.Equal(string.Empty, logsStderr); + Assert.Single(logsRows); + + Assert.Equal(CommandExitCodes.Success, accountsExitCode); + Assert.Equal(string.Empty, accountsStderr); + Assert.Single(accountsRows); + + Assert.Equal(CommandExitCodes.Success, onlyExitCode); + Assert.Equal(string.Empty, onlyStderr); + Assert.Equal(0, onlyDocument.RootElement.GetProperty("count").GetInt32()); + + Assert.Equal(CommandExitCodes.Success, qualifiedExitCode); + Assert.Equal(string.Empty, qualifiedStderr); + Assert.Equal(1, qualifiedDocument.RootElement.GetProperty("line").GetInt32()); + Assert.Equal("users", qualifiedDocument.RootElement.GetProperty("symbol_name").GetString()); + + Assert.Equal(CommandExitCodes.Success, mangledBracketExitCode); + Assert.Equal(string.Empty, mangledBracketStderr); + Assert.Equal(0, mangledBracketDocument.RootElement.GetProperty("count").GetInt32()); + + Assert.Equal(CommandExitCodes.Success, mangledBacktickExitCode); + Assert.Equal(string.Empty, mangledBacktickStderr); + Assert.Equal(0, mangledBacktickDocument.RootElement.GetProperty("count").GetInt32()); + + Assert.Equal(CommandExitCodes.Success, mangledDoubleQuoteExitCode); + Assert.Equal(string.Empty, mangledDoubleQuoteStderr); + Assert.Equal(0, mangledDoubleQuoteDocument.RootElement.GetProperty("count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_SqlDeleteUsingCapturesSourceReferences() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_sql_delete_using"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "sql")); + File.WriteAllText( + Path.Combine(projectRoot, "sql", "repro.sql"), + """ + DELETE FROM audit_log USING staging_log, archived_log + WHERE audit_log.id = staging_log.id; + DELETE FROM public.audit_log USING staging.stage_log, [archive].[archived_log], "public"."source" + WHERE audit_log.id = stage_log.id; + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (stagingExitCode, stagingStdout, stagingStderr) = RunBuiltCli(["references", "staging_log", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (archivedExitCode, archivedStdout, archivedStderr) = RunBuiltCli(["references", "archived_log", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (stageExitCode, stageStdout, stageStderr) = RunBuiltCli(["references", "stage_log", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (sourceExitCode, sourceStdout, sourceStderr) = RunBuiltCli(["references", "source", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (qualifiedTargetExitCode, qualifiedTargetStdout, qualifiedTargetStderr) = RunBuiltCli(["references", "public.audit_log", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (qualifiedSourceExitCode, qualifiedSourceStdout, qualifiedSourceStderr) = RunBuiltCli(["references", "staging.stage_log", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (mangledBracketExitCode, mangledBracketStdout, mangledBracketStderr) = RunBuiltCli(["references", "archive].[archived_log", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (mangledDoubleQuoteExitCode, mangledDoubleQuoteStdout, mangledDoubleQuoteStderr) = RunBuiltCli(["references", "public\".\"source", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + + var stagingRows = ParseJsonLines(stagingStdout); + var archivedRows = ParseJsonLines(archivedStdout); + var stageRows = ParseJsonLines(stageStdout); + var sourceRows = ParseJsonLines(sourceStdout); + using var qualifiedTargetDocument = ParseJsonOutput(qualifiedTargetStdout); + using var qualifiedSourceDocument = ParseJsonOutput(qualifiedSourceStdout); + using var mangledBracketDocument = ParseJsonOutput(mangledBracketStdout); + using var mangledDoubleQuoteDocument = ParseJsonOutput(mangledDoubleQuoteStdout); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + + Assert.Equal(CommandExitCodes.Success, stagingExitCode); + Assert.Equal(string.Empty, stagingStderr); + Assert.Single(stagingRows); + + Assert.Equal(CommandExitCodes.Success, archivedExitCode); + Assert.Equal(string.Empty, archivedStderr); + Assert.Equal(2, archivedRows.Count); + + Assert.Equal(CommandExitCodes.Success, stageExitCode); + Assert.Equal(string.Empty, stageStderr); + Assert.Single(stageRows); + + Assert.Equal(CommandExitCodes.Success, sourceExitCode); + Assert.Equal(string.Empty, sourceStderr); + Assert.Single(sourceRows); + + Assert.Equal(CommandExitCodes.Success, qualifiedTargetExitCode); + Assert.Equal(string.Empty, qualifiedTargetStderr); + Assert.Equal(3, qualifiedTargetDocument.RootElement.GetProperty("line").GetInt32()); + Assert.Equal("audit_log", qualifiedTargetDocument.RootElement.GetProperty("symbol_name").GetString()); + + Assert.Equal(CommandExitCodes.Success, qualifiedSourceExitCode); + Assert.Equal(string.Empty, qualifiedSourceStderr); + Assert.Equal(3, qualifiedSourceDocument.RootElement.GetProperty("line").GetInt32()); + Assert.Equal("stage_log", qualifiedSourceDocument.RootElement.GetProperty("symbol_name").GetString()); + + Assert.Equal(CommandExitCodes.Success, mangledBracketExitCode); + Assert.Equal(string.Empty, mangledBracketStderr); + Assert.Equal(0, mangledBracketDocument.RootElement.GetProperty("count").GetInt32()); + + Assert.Equal(CommandExitCodes.Success, mangledDoubleQuoteExitCode); + Assert.Equal(string.Empty, mangledDoubleQuoteStderr); + Assert.Equal(0, mangledDoubleQuoteDocument.RootElement.GetProperty("count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_SqlUsingSourceMatcherSkipsDdlUsingClauses() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_sql_using_source_matcher"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "sql")); + File.WriteAllText( + Path.Combine(projectRoot, "sql", "repro.sql"), + """ + CREATE INDEX idx_users_name ON users USING btree (name); + ALTER TABLE users ALTER COLUMN name TYPE text USING lower(name); + MERGE INTO audit_log AS t + USING staging_log AS s + ON t.id = s.id + WHEN MATCHED THEN + UPDATE SET action = s.action; + MERGE audit_log_archive AS t + USING staging_archive AS s + ON t.id = s.id + WHEN MATCHED THEN + UPDATE SET action = s.action; + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (stagingExitCode, stagingStdout, stagingStderr) = RunBuiltCli(["references", "staging_log", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (archiveTargetExitCode, archiveTargetStdout, archiveTargetStderr) = RunBuiltCli(["references", "audit_log_archive", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (archiveSourceExitCode, archiveSourceStdout, archiveSourceStderr) = RunBuiltCli(["references", "staging_archive", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (btreeExitCode, btreeStdout, btreeStderr) = RunBuiltCli(["references", "btree", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (lowerExitCode, lowerStdout, lowerStderr) = RunBuiltCli(["references", "lower", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + + var stagingRows = ParseJsonLines(stagingStdout); + var archiveTargetRows = ParseJsonLines(archiveTargetStdout); + var archiveSourceRows = ParseJsonLines(archiveSourceStdout); + var lowerRows = ParseJsonLines(lowerStdout); + using var btreeDocument = ParseJsonOutput(btreeStdout); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + + Assert.Equal(CommandExitCodes.Success, stagingExitCode); + Assert.Equal(string.Empty, stagingStderr); + var stagingRow = Assert.Single(stagingRows); + Assert.Equal("staging_log", stagingRow.RootElement.GetProperty("symbol_name").GetString()); + Assert.Equal("reference", stagingRow.RootElement.GetProperty("reference_kind").GetString()); + + Assert.Equal(CommandExitCodes.Success, archiveTargetExitCode); + Assert.Equal(string.Empty, archiveTargetStderr); + var archiveTargetRow = Assert.Single(archiveTargetRows); + Assert.Equal("audit_log_archive", archiveTargetRow.RootElement.GetProperty("symbol_name").GetString()); + Assert.Equal("reference", archiveTargetRow.RootElement.GetProperty("reference_kind").GetString()); + + Assert.Equal(CommandExitCodes.Success, archiveSourceExitCode); + Assert.Equal(string.Empty, archiveSourceStderr); + var archiveSourceRow = Assert.Single(archiveSourceRows); + Assert.Equal("staging_archive", archiveSourceRow.RootElement.GetProperty("symbol_name").GetString()); + Assert.Equal("reference", archiveSourceRow.RootElement.GetProperty("reference_kind").GetString()); + + Assert.Equal(CommandExitCodes.Success, btreeExitCode); + Assert.Equal(string.Empty, btreeStderr); + Assert.Equal(0, btreeDocument.RootElement.GetProperty("count").GetInt32()); + + Assert.Equal(CommandExitCodes.Success, lowerExitCode); + Assert.Equal(string.Empty, lowerStderr); + var lowerRow = Assert.Single(lowerRows); + Assert.Equal("lower", lowerRow.RootElement.GetProperty("symbol_name").GetString()); + Assert.Equal("call", lowerRow.RootElement.GetProperty("reference_kind").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_SqlMergeUsingWithTargetHintStillResolvesSource() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_sql_merge_using_target_hint"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "sql")); + File.WriteAllText( + Path.Combine(projectRoot, "sql", "repro.sql"), + """ + MERGE INTO audit_log WITH (INDEX(ix_audit_log), HOLDLOCK) AS t + USING staging_log AS s + ON t.id = s.id + WHEN MATCHED THEN + UPDATE SET action = s.action; + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (stagingExitCode, stagingStdout, stagingStderr) = RunBuiltCli(["references", "staging_log", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + + var stagingRows = ParseJsonLines(stagingStdout); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, stagingExitCode); + Assert.Equal(string.Empty, stagingStderr); + + var stagingRow = Assert.Single(stagingRows); + Assert.Equal("staging_log", stagingRow.RootElement.GetProperty("symbol_name").GetString()); + Assert.Equal("reference", stagingRow.RootElement.GetProperty("reference_kind").GetString()); + Assert.Equal(2, stagingRow.RootElement.GetProperty("line").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_SqlMergeTempTargetWithoutIntoResolvesTargetAndSource() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_sql_merge_temp_without_into"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "sql")); + File.WriteAllText( + Path.Combine(projectRoot, "sql", "repro.sql"), + """ + MERGE #audit_log AS t + USING staging_log AS s + ON t.id = s.id + WHEN MATCHED THEN + UPDATE SET action = s.action; + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (targetExitCode, targetStdout, targetStderr) = RunBuiltCli(["references", "#audit_log", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (sourceExitCode, sourceStdout, sourceStderr) = RunBuiltCli(["references", "staging_log", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + + var targetRows = ParseJsonLines(targetStdout); + var sourceRows = ParseJsonLines(sourceStdout); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + + Assert.Equal(CommandExitCodes.Success, targetExitCode); + Assert.Equal(string.Empty, targetStderr); + var targetRow = Assert.Single(targetRows); + Assert.Equal("#audit_log", targetRow.RootElement.GetProperty("symbol_name").GetString()); + Assert.Equal("reference", targetRow.RootElement.GetProperty("reference_kind").GetString()); + + Assert.Equal(CommandExitCodes.Success, sourceExitCode); + Assert.Equal(string.Empty, sourceStderr); + var sourceRow = Assert.Single(sourceRows); + Assert.Equal("staging_log", sourceRow.RootElement.GetProperty("symbol_name").GetString()); + Assert.Equal("reference", sourceRow.RootElement.GetProperty("reference_kind").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_SqlMergeTempTargetWithMultilineHintResolvesTargetAndSource() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_sql_merge_temp_with_multiline_hint"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "sql")); + File.WriteAllText( + Path.Combine(projectRoot, "sql", "repro.sql"), + """ + MERGE INTO #audit_log + WITH (INDEX(ix_audit_log), HOLDLOCK) AS t + USING staging_log AS s + ON t.id = s.id + WHEN MATCHED THEN + UPDATE SET action = s.action; + MERGE #archive_log + WITH (HOLDLOCK) AS u + USING staging_archive AS v + ON u.id = v.id + WHEN MATCHED THEN + UPDATE SET action = v.action; + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (targetExitCode, targetStdout, targetStderr) = RunBuiltCli(["references", "#audit_log", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (sourceExitCode, sourceStdout, sourceStderr) = RunBuiltCli(["references", "staging_log", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (archiveTargetExitCode, archiveTargetStdout, archiveTargetStderr) = RunBuiltCli(["references", "#archive_log", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (archiveSourceExitCode, archiveSourceStdout, archiveSourceStderr) = RunBuiltCli(["references", "staging_archive", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + + var targetRows = ParseJsonLines(targetStdout); + var sourceRows = ParseJsonLines(sourceStdout); + var archiveTargetRows = ParseJsonLines(archiveTargetStdout); + var archiveSourceRows = ParseJsonLines(archiveSourceStdout); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + + Assert.Equal(CommandExitCodes.Success, targetExitCode); + Assert.Equal(string.Empty, targetStderr); + var targetRow = Assert.Single(targetRows); + Assert.Equal("#audit_log", targetRow.RootElement.GetProperty("symbol_name").GetString()); + Assert.Equal("reference", targetRow.RootElement.GetProperty("reference_kind").GetString()); + + Assert.Equal(CommandExitCodes.Success, sourceExitCode); + Assert.Equal(string.Empty, sourceStderr); + var sourceRow = Assert.Single(sourceRows); + Assert.Equal("staging_log", sourceRow.RootElement.GetProperty("symbol_name").GetString()); + Assert.Equal("reference", sourceRow.RootElement.GetProperty("reference_kind").GetString()); + + Assert.Equal(CommandExitCodes.Success, archiveTargetExitCode); + Assert.Equal(string.Empty, archiveTargetStderr); + var archiveTargetRow = Assert.Single(archiveTargetRows); + Assert.Equal("#archive_log", archiveTargetRow.RootElement.GetProperty("symbol_name").GetString()); + Assert.Equal("reference", archiveTargetRow.RootElement.GetProperty("reference_kind").GetString()); + + Assert.Equal(CommandExitCodes.Success, archiveSourceExitCode); + Assert.Equal(string.Empty, archiveSourceStderr); + var archiveSourceRow = Assert.Single(archiveSourceRows); + Assert.Equal("staging_archive", archiveSourceRow.RootElement.GetProperty("symbol_name").GetString()); + Assert.Equal("reference", archiveSourceRow.RootElement.GetProperty("reference_kind").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_SqlLineEndCommentsKeepMultilineUsingSources() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_sql_line_end_comment_using"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "sql")); + File.WriteAllText( + Path.Combine(projectRoot, "sql", "repro.sql"), + """ + DELETE FROM audit_log -- trailing comment + USING staging_log + WHERE audit_log.id = staging_log.id; + + MERGE INTO audit_log -- trailing comment + USING staging_merge AS s + ON audit_log.id = s.id + WHEN MATCHED THEN + UPDATE SET action = s.action; + + SELECT id INTO #comment_temp -- trailing comment + FROM users; + SELECT * FROM #comment_temp; + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (deleteExitCode, deleteStdout, deleteStderr) = RunBuiltCli(["references", "staging_log", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (mergeExitCode, mergeStdout, mergeStderr) = RunBuiltCli(["references", "staging_merge", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (tempExitCode, tempStdout, tempStderr) = RunBuiltCli(["references", "#comment_temp", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + + var deleteRows = ParseJsonLines(deleteStdout); + var mergeRows = ParseJsonLines(mergeStdout); + var tempRows = ParseJsonLines(tempStdout); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + + Assert.Equal(CommandExitCodes.Success, deleteExitCode); + Assert.Equal(string.Empty, deleteStderr); + var deleteRow = Assert.Single(deleteRows); + Assert.Equal("staging_log", deleteRow.RootElement.GetProperty("symbol_name").GetString()); + Assert.Equal("reference", deleteRow.RootElement.GetProperty("reference_kind").GetString()); + + Assert.Equal(CommandExitCodes.Success, mergeExitCode); + Assert.Equal(string.Empty, mergeStderr); + var mergeRow = Assert.Single(mergeRows); + Assert.Equal("staging_merge", mergeRow.RootElement.GetProperty("symbol_name").GetString()); + Assert.Equal("reference", mergeRow.RootElement.GetProperty("reference_kind").GetString()); + + Assert.Equal(CommandExitCodes.Success, tempExitCode); + Assert.Equal(string.Empty, tempStderr); + Assert.Equal(2, tempRows.Count); + Assert.All(tempRows, row => Assert.Equal("#comment_temp", row.RootElement.GetProperty("symbol_name").GetString())); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_SqlLineEndCommentsKeepUnfinishedPrefixes() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_sql_line_end_comment_unfinished_prefixes"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "sql")); + File.WriteAllText( + Path.Combine(projectRoot, "sql", "repro.sql"), + """ + SELECT id INTO -- trailing comment + #comment_temp + FROM users; + SELECT * FROM #comment_temp; + + DELETE FROM audit_log USING staging_log, -- trailing comment + archived_log + WHERE audit_log.id = staging_log.id; + + MERGE INTO audit_log WITH (INDEX(ix_audit_log), -- trailing comment + HOLDLOCK) AS t + USING staging_merge AS s + ON t.id = s.id + WHEN MATCHED THEN + UPDATE SET action = s.action; + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (tempExitCode, tempStdout, tempStderr) = RunBuiltCli(["references", "#comment_temp", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (deleteExitCode, deleteStdout, deleteStderr) = RunBuiltCli(["references", "archived_log", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (mergeExitCode, mergeStdout, mergeStderr) = RunBuiltCli(["references", "staging_merge", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + + var tempRows = ParseJsonLines(tempStdout); + var deleteRows = ParseJsonLines(deleteStdout); + var mergeRows = ParseJsonLines(mergeStdout); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + + Assert.Equal(CommandExitCodes.Success, tempExitCode); + Assert.Equal(string.Empty, tempStderr); + Assert.Equal(2, tempRows.Count); + Assert.All(tempRows, row => Assert.Equal("#comment_temp", row.RootElement.GetProperty("symbol_name").GetString())); + + Assert.Equal(CommandExitCodes.Success, deleteExitCode); + Assert.Equal(string.Empty, deleteStderr); + var deleteRow = Assert.Single(deleteRows); + Assert.Equal("archived_log", deleteRow.RootElement.GetProperty("symbol_name").GetString()); + Assert.Equal("reference", deleteRow.RootElement.GetProperty("reference_kind").GetString()); + + Assert.Equal(CommandExitCodes.Success, mergeExitCode); + Assert.Equal(string.Empty, mergeStderr); + var mergeRow = Assert.Single(mergeRows); + Assert.Equal("staging_merge", mergeRow.RootElement.GetProperty("symbol_name").GetString()); + Assert.Equal("reference", mergeRow.RootElement.GetProperty("reference_kind").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_SqlLineEndCommentsKeepUnfinishedTargetPrefixes() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_sql_line_end_comment_target_prefixes"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "sql")); + File.WriteAllText( + Path.Combine(projectRoot, "sql", "repro.sql"), + """ + INSERT INTO -- trailing comment + audit_log (action) VALUES ('x'); + + UPDATE -- trailing comment + #update_temp SET action = 'x'; + SELECT * FROM #update_temp; + + DELETE FROM -- trailing comment + #delete_temp; + SELECT * FROM #delete_temp; + + TRUNCATE TABLE -- trailing comment + #truncate_temp; + SELECT * FROM #truncate_temp; + + CREATE TABLE -- trailing comment + #create_temp (id int); + SELECT * FROM #create_temp; + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (auditExitCode, auditStdout, auditStderr) = RunBuiltCli(["references", "audit_log", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (updateExitCode, updateStdout, updateStderr) = RunBuiltCli(["references", "#update_temp", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (deleteExitCode, deleteStdout, deleteStderr) = RunBuiltCli(["references", "#delete_temp", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (truncateExitCode, truncateStdout, truncateStderr) = RunBuiltCli(["references", "#truncate_temp", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (createExitCode, createStdout, createStderr) = RunBuiltCli(["references", "#create_temp", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + + var auditRows = ParseJsonLines(auditStdout); + var updateRows = ParseJsonLines(updateStdout); + var deleteRows = ParseJsonLines(deleteStdout); + var truncateRows = ParseJsonLines(truncateStdout); + var createRows = ParseJsonLines(createStdout); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + + Assert.Equal(CommandExitCodes.Success, auditExitCode); + Assert.Equal(string.Empty, auditStderr); + var auditRow = Assert.Single(auditRows); + Assert.Equal("audit_log", auditRow.RootElement.GetProperty("symbol_name").GetString()); + Assert.Equal("reference", auditRow.RootElement.GetProperty("reference_kind").GetString()); + + Assert.Equal(CommandExitCodes.Success, updateExitCode); + Assert.Equal(string.Empty, updateStderr); + Assert.Equal(2, updateRows.Count); + Assert.All(updateRows, row => Assert.Equal("#update_temp", row.RootElement.GetProperty("symbol_name").GetString())); + + Assert.Equal(CommandExitCodes.Success, deleteExitCode); + Assert.Equal(string.Empty, deleteStderr); + Assert.Equal(2, deleteRows.Count); + Assert.All(deleteRows, row => Assert.Equal("#delete_temp", row.RootElement.GetProperty("symbol_name").GetString())); + + Assert.Equal(CommandExitCodes.Success, truncateExitCode); + Assert.Equal(string.Empty, truncateStderr); + Assert.Equal(2, truncateRows.Count); + Assert.All(truncateRows, row => Assert.Equal("#truncate_temp", row.RootElement.GetProperty("symbol_name").GetString())); + + Assert.Equal(CommandExitCodes.Success, createExitCode); + Assert.Equal(string.Empty, createStderr); + Assert.Single(createRows); + Assert.All(createRows, row => Assert.Equal("#create_temp", row.RootElement.GetProperty("symbol_name").GetString())); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_SqlTruncateTempTargetsEstablishLaterReads() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_sql_truncate_temp_target"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "sql")); + File.WriteAllText( + Path.Combine(projectRoot, "sql", "repro.sql"), + """ + TRUNCATE TABLE #truncate_temp; + SELECT * FROM #truncate_temp; + SELECT * FROM #future_temp; + TRUNCATE TABLE #future_temp; + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (truncateExitCode, truncateStdout, truncateStderr) = RunBuiltCli(["references", "#truncate_temp", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (futureExitCode, futureStdout, futureStderr) = RunBuiltCli(["references", "#future_temp", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + + var truncateRows = ParseJsonLines(truncateStdout); + var futureRows = ParseJsonLines(futureStdout); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + + Assert.Equal(CommandExitCodes.Success, truncateExitCode); + Assert.Equal(string.Empty, truncateStderr); + Assert.Equal(2, truncateRows.Count); + Assert.All(truncateRows, row => Assert.Equal("#truncate_temp", row.RootElement.GetProperty("symbol_name").GetString())); + + Assert.Equal(CommandExitCodes.Success, futureExitCode); + Assert.Equal(string.Empty, futureStderr); + var futureRow = Assert.Single(futureRows); + Assert.Equal("#future_temp", futureRow.RootElement.GetProperty("symbol_name").GetString()); + Assert.Equal(4, futureRow.RootElement.GetProperty("line").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_SqlBareDollarIdentifiersStayWhole() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_sql_bare_dollar_identifier"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "sql")); + File.WriteAllText( + Path.Combine(projectRoot, "sql", "repro.sql"), + """ + SELECT * FROM my$table; + INSERT INTO my$table (id) VALUES (1); + UPDATE my$table SET id = 2; + DELETE FROM my$table; + TRUNCATE TABLE my$table; + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (dollarExitCode, dollarStdout, dollarStderr) = RunBuiltCli(["references", "my$table", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (prefixExitCode, prefixStdout, prefixStderr) = RunBuiltCli(["references", "my", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + + var dollarRows = ParseJsonLines(dollarStdout); + using var prefixDocument = ParseJsonOutput(prefixStdout); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + + Assert.Equal(CommandExitCodes.Success, dollarExitCode); + Assert.Equal(string.Empty, dollarStderr); + Assert.Equal(5, dollarRows.Count); + Assert.All(dollarRows, row => Assert.Equal("my$table", row.RootElement.GetProperty("symbol_name").GetString())); + + Assert.Equal(CommandExitCodes.Success, prefixExitCode); + Assert.Equal(string.Empty, prefixStderr); + Assert.Equal(0, prefixDocument.RootElement.GetProperty("count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_SqlNonAsciiBareIdentifiersStayWhole() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_sql_non_ascii_bare_identifier"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "sql")); + File.WriteAllText( + Path.Combine(projectRoot, "sql", "repro.sql"), + """ + SELECT * FROM ユーザー; + INSERT INTO ユーザー (id) VALUES (1); + UPDATE ユーザー SET id = 2; + DELETE FROM ユーザー; + TRUNCATE TABLE ユーザー; + CALL ユーザー; + EXEC ユーザー; + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (symbolExitCode, symbolStdout, symbolStderr) = RunBuiltCli(["references", "ユーザー", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + + var rows = ParseJsonLines(symbolStdout); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + + Assert.Equal(CommandExitCodes.Success, symbolExitCode); + Assert.Equal(string.Empty, symbolStderr); + Assert.Equal(7, rows.Count); + Assert.Equal(5, rows.Count(row => row.RootElement.GetProperty("reference_kind").GetString() == "reference")); + Assert.Equal(2, rows.Count(row => row.RootElement.GetProperty("reference_kind").GetString() == "call")); + Assert.All(rows, row => Assert.Equal("ユーザー", row.RootElement.GetProperty("symbol_name").GetString())); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_SqlSemicolonlessSetAndDeclareKeepTempReads() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_sql_semicolonless_set_declare_temp"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "sql")); + File.WriteAllText( + Path.Combine(projectRoot, "sql", "repro.sql"), + """ + SELECT * FROM #future_temp; + SELECT id INTO #set_temp FROM users + SET @count = (SELECT COUNT(*) FROM #set_temp); + SELECT id INTO #declare_temp FROM users + DECLARE @first_id INT = (SELECT TOP (1) id FROM #declare_temp); + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (futureExitCode, futureStdout, futureStderr) = RunBuiltCli(["references", "#future_temp", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (setExitCode, setStdout, setStderr) = RunBuiltCli(["references", "#set_temp", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (declareExitCode, declareStdout, declareStderr) = RunBuiltCli(["references", "#declare_temp", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + + using var futureDocument = ParseJsonOutput(futureStdout); + var setRows = ParseJsonLines(setStdout); + var declareRows = ParseJsonLines(declareStdout); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + + Assert.Equal(CommandExitCodes.Success, futureExitCode); + Assert.Equal(string.Empty, futureStderr); + Assert.Equal(0, futureDocument.RootElement.GetProperty("count").GetInt32()); + + Assert.Equal(CommandExitCodes.Success, setExitCode); + Assert.Equal(string.Empty, setStderr); + Assert.Equal(2, setRows.Count); + Assert.All(setRows, row => Assert.Equal("#set_temp", row.RootElement.GetProperty("symbol_name").GetString())); + + Assert.Equal(CommandExitCodes.Success, declareExitCode); + Assert.Equal(string.Empty, declareStderr); + Assert.Equal(2, declareRows.Count); + Assert.All(declareRows, row => Assert.Equal("#declare_temp", row.RootElement.GetProperty("symbol_name").GetString())); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_SqlSemicolonlessIfAndWhileKeepTempReads() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_sql_semicolonless_if_while_temp"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "sql")); + File.WriteAllText( + Path.Combine(projectRoot, "sql", "repro.sql"), + """ + SELECT id INTO #if_temp FROM users + IF EXISTS (SELECT 1) SELECT * FROM #if_temp; + SELECT id INTO #while_temp FROM users + WHILE 1 = 0 SELECT * FROM #while_temp; + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (ifExitCode, ifStdout, ifStderr) = RunBuiltCli(["references", "#if_temp", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (whileExitCode, whileStdout, whileStderr) = RunBuiltCli(["references", "#while_temp", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + + var ifRows = ParseJsonLines(ifStdout); + var whileRows = ParseJsonLines(whileStdout); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + + Assert.Equal(CommandExitCodes.Success, ifExitCode); + Assert.Equal(string.Empty, ifStderr); + Assert.Equal(2, ifRows.Count); + Assert.All(ifRows, row => Assert.Equal("#if_temp", row.RootElement.GetProperty("symbol_name").GetString())); + + Assert.Equal(CommandExitCodes.Success, whileExitCode); + Assert.Equal(string.Empty, whileStderr); + Assert.Equal(2, whileRows.Count); + Assert.All(whileRows, row => Assert.Equal("#while_temp", row.RootElement.GetProperty("symbol_name").GetString())); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_SqlDoubleQuotedDynamicSqlDoesNotLeakUsersReference() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_sql_double_quoted_dynamic_sql"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "sql")); + File.WriteAllText( + Path.Combine(projectRoot, "sql", "repro.sql"), + """ + SET @sql = "SELECT * FROM users"; + EXECUTE IMMEDIATE @sql; + SELECT * FROM "users"; + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (usersExitCode, usersStdout, usersStderr) = RunBuiltCli(["references", "users", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + + var usersRows = ParseJsonLines(usersStdout); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, usersExitCode); + Assert.Equal(string.Empty, usersStderr); + + var usersRow = Assert.Single(usersRows); + var json = usersRow.RootElement; + Assert.Equal("users", json.GetProperty("symbol_name").GetString()); + Assert.Equal(3, json.GetProperty("line").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_SqlDoubleQuotedDynamicSqlDoesNotEstablishTempTable() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_sql_dynamic_temp_establishment"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "sql")); + File.WriteAllText( + Path.Combine(projectRoot, "sql", "repro.sql"), + """ + SET @sql = "SELECT id INTO #temp_users FROM users"; + SELECT * FROM #temp_users; + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "#temp_users", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Empty(json.GetProperty("references").EnumerateArray()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_SqlTempTablesDoNotLookAheadAcrossProcedureBodies() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_sql_temp_body_boundary"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "sql")); + File.WriteAllText( + Path.Combine(projectRoot, "sql", "repro.sql"), + """ + CREATE PROCEDURE dbo.ReadTemp AS + BEGIN + SELECT * FROM #later_temp; + END; + GO + CREATE PROCEDURE dbo.EstablishTemp AS + BEGIN + SELECT id INTO #later_temp FROM users; + END; + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "#later_temp", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + + var rows = ParseJsonLines(stdout); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + var row = Assert.Single(rows); + var json = row.RootElement; + Assert.Equal("#later_temp", json.GetProperty("symbol_name").GetString()); + Assert.Equal(8, json.GetProperty("line").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_SqlSameLineDollarQuotedBodiesDoNotHideLaterReference() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_sql_same_line_dollar_quoted_bodies"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "sql")); + File.WriteAllText( + Path.Combine(projectRoot, "sql", "repro.sql"), + """ + DO $$BEGIN END$$; SELECT * FROM users; DO $$BEGIN END$$; + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (usersExitCode, usersStdout, usersStderr) = RunBuiltCli(["references", "users", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + + var usersRows = ParseJsonLines(usersStdout); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, usersExitCode); + Assert.Equal(string.Empty, usersStderr); + + var usersRow = Assert.Single(usersRows); + Assert.Equal("users", usersRow.RootElement.GetProperty("symbol_name").GetString()); + Assert.Equal(1, usersRow.RootElement.GetProperty("line").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_SqlEscapedSingleQuotedStringsDoNotLeakPhantomReference() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_sql_escaped_single_quoted_strings"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "sql")); + File.WriteAllText( + Path.Combine(projectRoot, "sql", "repro.sql"), + """ + SELECT 'abc\' FROM phantom'; + SELECT 'abc'' FROM still_phantom'; + SELECT * FROM users # comment with comment_phantom; + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (phantomExitCode, phantomStdout, phantomStderr) = RunBuiltCli(["references", "phantom", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (usersExitCode, usersStdout, usersStderr) = RunBuiltCli(["references", "users", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + + using var phantomDocument = ParseJsonOutput(phantomStdout); + var usersRows = ParseJsonLines(usersStdout); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + + Assert.Equal(CommandExitCodes.Success, phantomExitCode); + Assert.Equal(string.Empty, phantomStderr); + Assert.Equal(0, phantomDocument.RootElement.GetProperty("count").GetInt32()); + + Assert.Equal(CommandExitCodes.Success, usersExitCode); + Assert.Equal(string.Empty, usersStderr); + var usersRow = Assert.Single(usersRows); + Assert.Equal("users", usersRow.RootElement.GetProperty("symbol_name").GetString()); + Assert.Equal(3, usersRow.RootElement.GetProperty("line").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_SqlMultilineSingleQuotedStringsStayOpaque() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_sql_multiline_single_quoted_strings"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "sql")); + File.WriteAllText( + Path.Combine(projectRoot, "sql", "repro.sql"), + """ + SELECT 'abc'' + still escaped \' + FROM phantom + INTO #temp_users + '; + SELECT * FROM users; + SELECT * FROM #temp_users; + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (usersExitCode, usersStdout, usersStderr) = RunBuiltCli(["references", "users", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (tempExitCode, tempStdout, tempStderr) = RunBuiltCli(["references", "#temp_users", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + + var usersRows = ParseJsonLines(usersStdout); + using var tempDocument = ParseJsonOutput(tempStdout); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + + Assert.Equal(CommandExitCodes.Success, usersExitCode); + Assert.Equal(string.Empty, usersStderr); + var usersRow = Assert.Single(usersRows); + Assert.Equal("users", usersRow.RootElement.GetProperty("symbol_name").GetString()); + Assert.Equal(6, usersRow.RootElement.GetProperty("line").GetInt32()); + + Assert.Equal(CommandExitCodes.Success, tempExitCode); + Assert.Equal(string.Empty, tempStderr); + Assert.Equal(0, tempDocument.RootElement.GetProperty("count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_SqlNonCodeRegionsDoNotLeakPhantomReference() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_sql_non_code_regions"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "sql")); + File.WriteAllText( + Path.Combine(projectRoot, "sql", "repro.sql"), + """ + SELECT * FROM users /* comment + FROM phantom */; + UPDATE audit_log SET action = 'done'; + DO $$ + BEGIN + EXECUTE $$SELECT * FROM phantom$$; + END + $$; + DO $body$ + BEGIN + UPDATE phantom SET action = 'nope'; + END + $body$; + SELECT * FROM accounts; + DELETE FROM archived_accounts; + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (phantomExitCode, phantomStdout, phantomStderr) = RunBuiltCli(["references", "phantom", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + var (accountsExitCode, accountsStdout, accountsStderr) = RunBuiltCli(["references", "accounts", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"]); + + using var phantomDocument = ParseJsonOutput(phantomStdout); + var accountsRows = ParseJsonLines(accountsStdout); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + + Assert.Equal(CommandExitCodes.Success, phantomExitCode); + Assert.Equal(string.Empty, phantomStderr); + Assert.Equal(0, phantomDocument.RootElement.GetProperty("count").GetInt32()); + + Assert.Equal(CommandExitCodes.Success, accountsExitCode); + Assert.Equal(string.Empty, accountsStderr); + var accountsRow = Assert.Single(accountsRows); + var json = accountsRow.RootElement; + Assert.Equal("accounts", json.GetProperty("symbol_name").GetString()); + Assert.Equal(14, json.GetProperty("line").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpMultilineThrowBeforeGroupDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_parenthesized_orderby_multiline_throw_group_local_function"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public IEnumerable Read(IEnumerable items) + { + static System.Exception group(IEnumerable xs) => new System.Exception(xs.Count().ToString()); + return from Status in items + orderby items.Count() > 0 ? throw + group + (items) : null, items.Count() + select Status.Ready; + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Empty(json.GetProperty("references").EnumerateArray()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpNullableTypeSuffixBeforeParenthesizedTerminalSelectPreservesLaterEnumReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_parenthesized_terminal_select_after_nullable_type_suffix"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public static class Sink + { + public static Status Pick(object left, Status right) => right; + } + + public sealed class Uses + { + public Status Read(IEnumerable items, object value) + { + return Sink.Pick(from Status in items + let cast = value as Status? + select(Status.Ready), + Status.Ready); + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + var rows = ParseJsonLines(stdout); + var row = Assert.Single(rows).RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("Ready", row.GetProperty("symbol_name").GetString()); + Assert.Equal("Read", row.GetProperty("container_name").GetString()); + Assert.Contains("Status.Ready", row.GetProperty("context").GetString(), StringComparison.Ordinal); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpPostfixIncrementBeforeParenthesizedTerminalSelectPreservesLaterEnumReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_parenthesized_terminal_select_after_postfix_increment"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public static class Sink + { + public static Status Pick(object left, Status right) => right; + } + + public sealed class Uses + { + public Status Read(IEnumerable items, int counter) + { + return Sink.Pick(from Status in items + let n = counter++ + select(Status.Ready), + Status.Ready); + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + var rows = ParseJsonLines(stdout); + var row = Assert.Single(rows).RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("Ready", row.GetProperty("symbol_name").GetString()); + Assert.Equal("Read", row.GetProperty("container_name").GetString()); + Assert.Contains("Status.Ready", row.GetProperty("context").GetString(), StringComparison.Ordinal); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpNullableArrayRankSuffixBeforeParenthesizedTerminalSelectPreservesLaterEnumReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_parenthesized_terminal_select_after_nullable_array_rank_suffix"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public static class Sink + { + public static Status Pick(object left, Status right) => right; + } + + public sealed class Uses + { + public Status Read(IEnumerable items, object value) + { + return Sink.Pick(from Status in items + let cast = value as Status[,]? + select(Status.Ready), + Status.Ready); + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + var rows = ParseJsonLines(stdout); + var row = Assert.Single(rows).RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("Ready", row.GetProperty("symbol_name").GetString()); + Assert.Equal("Read", row.GetProperty("container_name").GetString()); + Assert.Contains("Status.Ready", row.GetProperty("context").GetString(), StringComparison.Ordinal); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpNullableTupleSuffixBeforeParenthesizedTerminalSelectPreservesLaterEnumReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_parenthesized_terminal_select_after_nullable_tuple_suffix"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public static class Sink + { + public static Status Pick(object left, Status right) => right; + } + + public sealed class Uses + { + public Status Read(IEnumerable items, object value) + { + return Sink.Pick(from Status in items + let cast = value as (int Left, int Right)? + select(Status.Ready), + Status.Ready); + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + var rows = ParseJsonLines(stdout); + var row = Assert.Single(rows).RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("Ready", row.GetProperty("symbol_name").GetString()); + Assert.Equal("Read", row.GetProperty("container_name").GetString()); + Assert.Contains("Status.Ready", row.GetProperty("context").GetString(), StringComparison.Ordinal); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpCastedLocalSelectCallInOrderByDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_parenthesized_orderby_casted_select_local_function"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public IEnumerable Read(IEnumerable items) + { + static object select(IEnumerable xs) => xs.Count(); + return from Status in items + orderby (object)select(items), items.Count() + select Status.Ready; + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Empty(json.GetProperty("references").EnumerateArray()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpSimpleIdentifierCastedLocalSelectCallInOrderByPreservesLaterEnumReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_parenthesized_orderby_simple_casted_select_local_function"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public static class Sink + { + public static Status Pick(object left, Status right) => right; + } + + public sealed class CustomType + { + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Status Read(IEnumerable items) + { + static CustomType select(IEnumerable xs) => new(); + return Sink.Pick(from Status in items + orderby (CustomType)select(items), items.Count() + select Status.Ready, + Demo.Status.Ready); + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + var rows = ParseJsonLines(stdout); + var row = Assert.Single(rows).RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("Ready", row.GetProperty("symbol_name").GetString()); + Assert.Equal("Read", row.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpMultilineSimpleIdentifierCastedLocalSelectCallInOrderByPreservesLaterEnumReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_parenthesized_orderby_multiline_simple_casted_select_local_function"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public static class Sink + { + public static Status Pick(object left, Status right) => right; + } + + public sealed class CustomType + { + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Status Read(IEnumerable items) + { + static CustomType select(IEnumerable xs) => new(); + return Sink.Pick(from Status in items + orderby (CustomType) + select(items), items.Count() + select Status.Ready, + Demo.Status.Ready); + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + var rows = ParseJsonLines(stdout); + var row = Assert.Single(rows).RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("Ready", row.GetProperty("symbol_name").GetString()); + Assert.Equal("Read", row.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpParenthesizedTernaryOrderByBeforeParenthesizedTerminalSelectPreservesLaterEnumReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_parenthesized_orderby_ternary_before_parenthesized_select"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public static class Sink + { + public static Status Pick(object left, Status right) => right; + } + + public sealed class Uses + { + public Status Read(IEnumerable items, bool flag, int left, int right) + { + return Sink.Pick(from Status in items + orderby (flag ? left : right) + select(Status.Ready), + Demo.Status.Ready); + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + var rows = ParseJsonLines(stdout); + var row = Assert.Single(rows).RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("Ready", row.GetProperty("symbol_name").GetString()); + Assert.Equal("Read", row.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpParenthesizedCoalesceOrderByBeforeParenthesizedTerminalSelectPreservesLaterEnumReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_parenthesized_orderby_coalesce_before_parenthesized_select"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public static class Sink + { + public static Status Pick(object left, Status right) => right; + } + + public sealed class Uses + { + public Status Read(IEnumerable items, int? left, int right) + { + return Sink.Pick(from Status in items + orderby (left ?? right) + select(Status.Ready), + Demo.Status.Ready); + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + var rows = ParseJsonLines(stdout); + var row = Assert.Single(rows).RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("Ready", row.GetProperty("symbol_name").GetString()); + Assert.Equal("Read", row.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpParenthesizedQualifiedMemberAccessBeforeParenthesizedTerminalSelectPreservesOnlyRealEnumReferences() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_parenthesized_qualified_member_access_before_parenthesized_select"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public static class Sink + { + public static Status Pick(object left, Status right) => right; + } + + public sealed class Uses + { + public Status Read(IEnumerable items) + { + return Sink.Pick(from Status in items + orderby (Demo.Status.Ready) + select(Status.Ready), + Demo.Status.Ready); + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + var rows = ParseJsonLines(stdout); + var parsedRows = rows.Select(document => document.RootElement).ToList(); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(2, parsedRows.Count); + Assert.All(parsedRows, row => Assert.Equal("Read", row.GetProperty("container_name").GetString())); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpLowercaseAliasCastedLocalSelectCallInOrderByPreservesLaterEnumReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_parenthesized_orderby_lowercase_alias_casted_select_local_function"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + using customType = Demo.CustomType; + + namespace Demo; + + public enum Status + { + Ready + } + + public static class Sink + { + public static Status Pick(object left, Status right) => right; + } + + public sealed class CustomType + { + } + + public sealed class Uses + { + public Status Read(IEnumerable items) + { + static customType select(IEnumerable xs) => new(); + return Sink.Pick(from Status in items + orderby (customType)select(items) + select Status.Ready, + Demo.Status.Ready); + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + var rows = ParseJsonLines(stdout); + var row = Assert.Single(rows).RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("Ready", row.GetProperty("symbol_name").GetString()); + Assert.Equal("Read", row.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpParenthesizedKeywordNamedParameterBeforeParenthesizedTerminalSelectPreservesLaterEnumReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_parenthesized_keyword_named_parameter_before_parenthesized_select"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public static class Sink + { + public static Status Pick(object left, Status right) => right; + } + + public sealed class Uses + { + public Status Read(IEnumerable items, int Select) + { + return Sink.Pick(from Status in items + orderby (Select) + select(Status.Ready), + Demo.Status.Ready); + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + var rows = ParseJsonLines(stdout); + var row = Assert.Single(rows).RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("Ready", row.GetProperty("symbol_name").GetString()); + Assert.Equal("Read", row.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpParenthesizedKeywordNamedLocalBeforeParenthesizedTerminalSelectPreservesLaterEnumReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_parenthesized_keyword_named_local_before_parenthesized_select"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public static class Sink + { + public static Status Pick(object left, Status right) => right; + } + + public sealed class Uses + { + public Status Read(IEnumerable items) + { + const int Select = 1; + return Sink.Pick(from Status in items + orderby (Select) + select(Status.Ready), + Demo.Status.Ready); + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + var rows = ParseJsonLines(stdout); + var row = Assert.Single(rows).RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("Ready", row.GetProperty("symbol_name").GetString()); + Assert.Equal("Read", row.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpParenthesizedUppercaseConstantBeforeParenthesizedTerminalSelectPreservesLaterEnumReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_parenthesized_uppercase_constant_before_parenthesized_select"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public static class Sink + { + public static Status Pick(object left, Status right) => right; + } + + public sealed class Uses + { + public Status Read(IEnumerable items) + { + const int READY = 1; + return Sink.Pick(from Status in items + orderby (READY) + select(Status.Ready), + Demo.Status.Ready); + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + var rows = ParseJsonLines(stdout); + var row = Assert.Single(rows).RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("Ready", row.GetProperty("symbol_name").GetString()); + Assert.Equal("Read", row.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpParenthesizedTerminalSelectAfterGenericClosePreservesLaterEnumReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_parenthesized_terminal_select_after_generic_close"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public static class Sink + { + public static Status Pick(object left, Status right) => right; + } + + public sealed class Uses + { + public Status Read(IEnumerable items) + { + return Sink.Pick(from Status in items where Status is List select(Status.Ready), Status.Ready); + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + var rows = ParseJsonLines(stdout); + var row = Assert.Single(rows).RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("Ready", row.GetProperty("symbol_name").GetString()); + Assert.Equal("Read", row.GetProperty("container_name").GetString()); + Assert.Contains("Status.Ready", row.GetProperty("context").GetString(), StringComparison.Ordinal); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpNestedQueryBeforeParenthesizedOrderByCommaPreservesOnlyTrailingEnumReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_nested_query_parenthesized_orderby_collision"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public static class Sink + { + public static Status Pick(object left, Status right) => right; + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Status Read(IEnumerable items, IEnumerable others) + { + return Sink.Pick(from Status in items + let nested = from x in others select x + orderby(items.Count()), nested.Count() + select Status.Ready, + Status.Ready); + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + var rows = ParseJsonLines(stdout); + var row = Assert.Single(rows).RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("Ready", row.GetProperty("symbol_name").GetString()); + Assert.Equal("Read", row.GetProperty("container_name").GetString()); + Assert.Contains("Status.Ready", row.GetProperty("context").GetString(), StringComparison.Ordinal); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpTerminalSelectIdentifierNamedDescendingPreservesLaterEnumReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_terminal_select_descending_identifier"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public static class Sink + { + public static object Pick(object left, Status right) => right; + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public object Read(IEnumerable items) + { + var descending = 1; + return Sink.Pick(from Status in items select descending, Status.Ready); + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + var rows = ParseJsonLines(stdout); + var row = Assert.Single(rows).RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("Ready", row.GetProperty("symbol_name").GetString()); + Assert.Equal(26, row.GetProperty("line").GetInt32()); + Assert.Contains("Status.Ready", row.GetProperty("context").GetString(), StringComparison.Ordinal); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpLambdaParameterNamedLikeEnumDoesNotLeakAfterLambda() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_lambda_scope_end"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + using System; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Demo.Status Read() + { + Func f = Status => Status.Ready; + return Demo.Status.Ready; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("Read", json.GetProperty("container_name").GetString()); + Assert.Equal(20, json.GetProperty("line").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpLambdaParameterNamedLikeEnumDoesNotLeakAfterSameLineLambda() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_lambda_same_line_scope_end"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + using System; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Demo.Status Read() + { + Func f = Status => Status.Ready; return Demo.Status.Ready; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("Read", json.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpParenthesizedLambdaParameterNamedLikeEnumDoesNotSuppressEarlierSameLineReference() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_parenthesized_lambda_prefix_scope"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + using System; + + namespace Demo; + + public enum Status + { + Ready + } + + public static class Sink + { + public static Demo.Status Pick(Demo.Status left, Func right) => left; + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Demo.Status Read() + { + return Sink.Pick(Demo.Status.Ready, (Holder Status) => Status.Ready); + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("Read", json.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpQueryRangeVariableNamedLikeEnumDoesNotLeakAfterQuery() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_query_scope_end"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Demo.Status Read(IEnumerable items) + { + _ = from Status in items + select Status.Ready; + + return Demo.Status.Ready; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("Read", json.GetProperty("container_name").GetString()); + Assert.Equal(23, json.GetProperty("line").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpQueryRangeVariableNamedLikeEnumDoesNotLeakPastQueryArgument() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_query_argument_scope_end"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public static class Sink + { + public static Demo.Status Pick(IEnumerable left, Demo.Status right) => right; + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Demo.Status Read(IEnumerable items) + { + return Sink.Pick(from Status in items select Status.Ready, Demo.Status.Ready); + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("Read", json.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpForeachValueNamedLikeEnumDoesNotLeakAfterEmbeddedStatement() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_foreach_scope_end"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + using System.Collections.Generic; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Demo.Status Read(IEnumerable items) + { + foreach (var Status in items) + _ = Status.Ready; + + return Demo.Status.Ready; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("Read", json.GetProperty("container_name").GetString()); + Assert.Equal(22, json.GetProperty("line").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpForeachValueNamedLikeEnumDoesNotLeakAfterSameLineEmbeddedStatement() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_foreach_same_line_scope_end"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + using System.Collections.Generic; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Demo.Status Read(IEnumerable items) + { + foreach (var Status in items) _ = Status.Ready; return Demo.Status.Ready; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("Read", json.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpForeachValueNamedLikeEnumDoesNotLeakInsideElseBranch() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_foreach_else_scope_end"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + using System.Collections.Generic; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Demo.Status Read(IEnumerable items, bool flag) + { + foreach (var Status in items) + if (flag) + _ = 0; + else + _ = Status.Ready; + + return Demo.Status.Ready; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("Read", json.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpLaterLocalShadowingDoesNotSuppressEarlierReference() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_local_order"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Demo.Status Before() + { + _ = Status.Ready; + Holder Status = new(); + return Demo.Status.Ready; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name", "--limit", "10"], + _jsonOptions)); + + var rows = ParseJsonLines(stdout); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal([17, 19], rows.Select(row => row.RootElement.GetProperty("line").GetInt32()).OrderBy(line => line).ToArray()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpDeclarationPatternVariableDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_declaration_pattern_collision"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Demo.Status Read(object value) + { + if (value is Holder Status) + { + _ = Status.Ready; + } + + return Demo.Status.Ready; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + 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("Read", json.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpMultiLineIfDeclarationPatternVariableDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_multiline_if_declaration_pattern_collision"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Demo.Status Read(object value) + { + if ( + value is Holder Status) + { + _ = Status.Ready; + } + + return Demo.Status.Ready; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal(23, json.GetProperty("line").GetInt32()); + Assert.Equal("Read", json.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpMultiLineWhileDeclarationPatternVariableDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_multiline_while_declaration_pattern_collision"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Demo.Status Read(object value) + { + while ( + value is Holder Status) + { + _ = Status.Ready; + break; + } + + return Demo.Status.Ready; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal(24, json.GetProperty("line").GetInt32()); + Assert.Equal("Read", json.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpLambdaScopedDeclarationPatternVariableDoesNotLeakIntoOuterIfBody() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_lambda_scoped_declaration_pattern_collision"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace RealNs; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public RealNs.Status Read(object[] values) + { + if (values.Any(value => value is Holder RealNs)) + { + return RealNs.Status.Ready; + } + + return RealNs.Status.Ready; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var first = ParseJsonOutput(stdout); + var lines = ParseJsonLines(stdout) + .Select(document => document.RootElement.GetProperty("line").GetInt32()) + .OrderBy(line => line) + .ToArray(); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("call", first.RootElement.GetProperty("reference_kind").GetString()); + Assert.Equal([19, 22], lines); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpNestedLambdaScopedDeclarationPatternVariableDoesNotLeakIntoOuterIfBody() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_nested_lambda_scoped_declaration_pattern_collision"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace RealNs; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public RealNs.Status Read(object[] values) + { + if (values.Any(value => value is Holder RealNs && values.Any(other => other is Holder Other))) + { + return RealNs.Status.Ready; + } + + return RealNs.Status.Ready; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var first = ParseJsonOutput(stdout); + var lines = ParseJsonLines(stdout) + .Select(document => document.RootElement.GetProperty("line").GetInt32()) + .OrderBy(line => line) + .ToArray(); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("call", first.RootElement.GetProperty("reference_kind").GetString()); + Assert.Equal([19, 22], lines); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpSwitchCaseDeclarationPatternVariableDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_switch_case_declaration_pattern_collision"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Demo.Status Read(object value) + { + switch (value) + { + case Holder Status: + _ = Status.Ready; + break; + } + + return Demo.Status.Ready; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal(24, json.GetProperty("line").GetInt32()); + Assert.Equal("Read", json.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpConditionalExpressionDeclarationPatternVariableDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_conditional_expression_declaration_pattern_collision"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Demo.Status Read(object value) + { + return value is Holder Status + ? (Demo.Status)Status.Ready + : Demo.Status.Ready; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal(19, json.GetProperty("line").GetInt32()); + Assert.Equal("Read", json.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpRecursivePatternVariableDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_recursive_pattern_collision"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Demo.Status Read(object value) + { + if (value is Holder { Ready: > 0 } Status) + { + _ = Status.Ready; + } + + return Demo.Status.Ready; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + 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("Read", json.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpMultiLineRecursivePatternVariableDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_multiline_recursive_pattern_collision"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Demo.Status Read(object value) + { + if (value is Holder + { + Ready: > 0 + } Status) + { + _ = Status.Ready; + } + + return Demo.Status.Ready; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal(25, json.GetProperty("line").GetInt32()); + Assert.Equal("Read", json.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpQueryRangeVariableGenericSelectorDoesNotLeakEnumReference() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_query_generic_selector"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public static class Sink + { + public static int Wrap(int value) => value; + } + + public sealed class Uses + { + public IEnumerable Read(IEnumerable items) + { + return from Status in items + select Sink.Wrap(Status.Ready); + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Equal(0, json.GetProperty("references").GetArrayLength()); + Assert.True(json.GetProperty("exact_index_available").GetBoolean()); + Assert.False(json.TryGetProperty("unsupported_symbol_kind", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpQueryRangeVariableShiftSelectorPreservesOnlyTrailingEnumReference() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_query_shift_selector"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public static class Sink + { + public static IEnumerable Pick(IEnumerable left, Status right) => left; + } + + public sealed class Uses + { + public IEnumerable Read(IEnumerable items) + { + return Sink.Pick( + from Status in items + select (Status.Ready << 1) >> (1 + Status.Ready), + Status.Ready); + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal(28, json.GetProperty("line").GetInt32()); + Assert.Equal("Read", json.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpQueryRangeVariableGenericTypePatternDesignationDoesNotLeakEnumReference() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_query_generic_type_pattern_designation"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public int Read(IEnumerable items) + { + return (from Status in items + select Status is Dictionary dict ? Status.Ready : 0).First(); + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Equal(0, json.GetProperty("references").GetArrayLength()); + Assert.True(json.GetProperty("exact_index_available").GetBoolean()); + Assert.False(json.TryGetProperty("unsupported_symbol_kind", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpQueryRangeVariableGenericTypePatternWithoutDesignationDoesNotLeakEnumReference() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_query_generic_type_pattern_without_designation"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public int Read(IEnumerable items) + { + return (from Status in items + select Status is Dictionary ? Status.Ready : 0).First(); + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Equal(0, json.GetProperty("references").GetArrayLength()); + Assert.True(json.GetProperty("exact_index_available").GetBoolean()); + Assert.False(json.TryGetProperty("unsupported_symbol_kind", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Theory] + [InlineData("!=")] + [InlineData("==")] + public void RunReferences_ExactJson_CSharpQueryRangeVariableGenericAsNullComparisonDoesNotLeakEnumReference(string comparisonOperator) + { + var projectRoot = TestProjectHelper.CreateTempProject($"cdidx_query_runner_enum_member_query_generic_as_null_{comparisonOperator switch { "!=" => "not_equal", "==" => "equal_equal", _ => "unknown" }}"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + $$""" + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public int Read(IEnumerable items) + { + return (from Status in items + select Status as Dictionary {{comparisonOperator}} null ? Status.Ready : 0).First(); + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Equal(0, json.GetProperty("references").GetArrayLength()); + Assert.True(json.GetProperty("exact_index_available").GetBoolean()); + Assert.False(json.TryGetProperty("unsupported_symbol_kind", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpQueryRangeVariableGenericAsNullComparisonPreservesLaterEnumReference() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_query_generic_as_null_preserves_later_reference"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public static class Sink + { + public static Status Pick(int left, Status right) => right; + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Status Read(IEnumerable items) + { + return Sink.Pick( + (from Status in items + select Status as Dictionary != null ? Status.Ready : 0).First(), + Status.Ready); + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal(28, json.GetProperty("line").GetInt32()); + Assert.Equal("Read", json.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpQueryRangeVariableTupleGenericSelectorDoesNotLeakEnumReference() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_query_tuple_generic_selector"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public static class Sink + { + public static int Wrap(int value) => value; + } + + public sealed class Uses + { + public IEnumerable Read(IEnumerable items) + { + return from Status in items + select Sink.Wrap<(int, List)>(Status.Ready); + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Equal(0, json.GetProperty("references").GetArrayLength()); + Assert.True(json.GetProperty("exact_index_available").GetBoolean()); + Assert.False(json.TryGetProperty("unsupported_symbol_kind", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpRecursivePatternCaseVariableDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_recursive_pattern_case_collision"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Demo.Status Read(object value) + { + switch (value) + { + case Holder { Ready: > 0 } Status: + _ = Status.Ready; + break; + } + + return Demo.Status.Ready; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal(24, json.GetProperty("line").GetInt32()); + Assert.Equal("Read", json.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpSwitchExpressionDeclarationPatternWhenInMultiLineCommentDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_switch_expression_declaration_pattern_when_multiline_comment_collision"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Demo.Status Read(object value) + { + return value switch + { + Holder /* trivia + when comment */ Status when Status.Ready > 0 => Demo.Status.Ready, + _ => Demo.Status.Ready + }; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var first = ParseJsonOutput(stdout); + var firstJson = first.RootElement; + var rows = ParseJsonLines(stdout) + .Select(document => ( + Line: document.RootElement.GetProperty("line").GetInt32(), + Column: document.RootElement.GetProperty("column").GetInt32(), + ContainerName: document.RootElement.GetProperty("container_name").GetString())) + .OrderBy(row => row.Line) + .ThenBy(row => row.Column) + .ToArray(); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("call", firstJson.GetProperty("reference_kind").GetString()); + Assert.Equal([20, 21], rows.Select(row => row.Line).ToArray()); + Assert.Equal([83, 30], rows.Select(row => row.Column).ToArray()); + Assert.All(rows, row => Assert.Equal("Read", row.ContainerName)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpSwitchExpressionDeclarationPatternWhenGuardDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_switch_expression_declaration_pattern_when_guard_collision"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Demo.Status Read(object value) + { + return value switch + { + Holder Status when Status.Ready > 0 => Demo.Status.Ready, + _ => Demo.Status.Ready + }; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var first = ParseJsonOutput(stdout); + var firstJson = first.RootElement; + var rows = ParseJsonLines(stdout) + .Select(document => ( + Line: document.RootElement.GetProperty("line").GetInt32(), + Column: document.RootElement.GetProperty("column").GetInt32(), + ContainerName: document.RootElement.GetProperty("container_name").GetString())) + .OrderBy(row => row.Line) + .ThenBy(row => row.Column) + .ToArray(); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("call", firstJson.GetProperty("reference_kind").GetString()); + Assert.Equal([19, 20], rows.Select(row => row.Line).ToArray()); + Assert.Equal([64, 30], rows.Select(row => row.Column).ToArray()); + Assert.All(rows, row => Assert.Equal("Read", row.ContainerName)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpSwitchExpressionRecursivePatternVariableDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_switch_expression_recursive_pattern_collision"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Demo.Status Read(object value) + { + return value switch + { + Holder { Ready: > 0 } Status => (Demo.Status)Status.Ready, + _ => Demo.Status.Ready + }; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal(20, json.GetProperty("line").GetInt32()); + Assert.Equal("Read", json.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpStaticLambdaScopedDeclarationPatternVariableDoesNotLeakIntoOuterIfBody() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_static_lambda_declaration_pattern_collision"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace RealNs; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public RealNs.Status Read(object[] values) + { + if (values.Any(static value => value is Holder RealNs)) + { + return RealNs.Status.Ready; + } + + return RealNs.Status.Ready; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var first = ParseJsonOutput(stdout); + var lines = ParseJsonLines(stdout) + .Select(document => document.RootElement.GetProperty("line").GetInt32()) + .OrderBy(line => line) + .ToArray(); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("call", first.RootElement.GetProperty("reference_kind").GetString()); + Assert.Equal([19, 22], lines); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpDottedValueReceiverChainDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_dotted_value_receiver_collision"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace RealNs; + + public enum Status + { + Ready + } + + namespace Test; + + public sealed class ReadyHolder + { + public int Ready { get; set; } + } + + public sealed class NamespaceLike + { + public ReadyHolder Status { get; } = new(); + } + + public sealed class Uses + { + public global::RealNs.Status Read(NamespaceLike RealNs) + { + _ = RealNs.Status.Ready; + return global::RealNs.Status.Ready; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal(25, json.GetProperty("line").GetInt32()); + Assert.Equal("Read", json.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpGlobalQualifiedEnumMemberSurvivesConflictingTypeName() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_global_qualified"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public enum Status + { + Ready + } + + namespace Other; + + public static class Status + { + public static int Value = 1; + } + + public class Uses + { + public Demo.Status Read() + { + return global::Demo.Status.Ready; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal(19, json.GetProperty("line").GetInt32()); + Assert.Equal("Read", json.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpGlobalQualifiedEnumMemberSurvivesPropertyShadowing() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_global_property_shadow"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + enum Color + { + Red + } + + class C + { + int Color => 0; + + void M() + { + var x = global::Color.Red; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal(12, json.GetProperty("line").GetInt32()); + Assert.Equal("M", json.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpCompactSameLineTypeBody_PrefersInnermostMethodContainer() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_references_compact_same_line_type_body"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace N; + enum Color { Red } + class C { int N => 0; void M() { var x = global::N.Color.Red; } } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal("function", json.GetProperty("container_kind").GetString()); + Assert.Equal("M", json.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpGlobalQualifiedEnumMemberSurvivesConflictingUsingAlias() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_global_alias_shadow"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo + { + public enum Color + { + Red + } + } + + namespace Shadow + { + public static class Demo + { + public static int Red => 0; + } + } + + using Demo = Shadow; + + class C + { + Demo.Color M() + { + return global::Demo.Color.Red; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal(23, json.GetProperty("line").GetInt32()); + Assert.Equal("M", json.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpLogicalConstantMemberPatternDoesNotLeakAcrossFiles() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_constant_member_pattern_cross_file"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Defs.cs", "csharp", + """ + namespace Defs; + + public enum Color + { + Red, + Blue + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Use.cs", "csharp", + """ + using Defs; + + class Demo + { + void Run(Color value) + { + switch (value) + { + case Color.Red or Color.Blue: + break; + } + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpMixedLogicalPatternKeepsTypeHead() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_mixed_logical_type_pattern"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Probe; + + enum Color { Red, Blue } + class Point {} + + class Demo + { + bool Match1(object value) => value is Color.Red or Point; + bool Match2(object value) => value is Point or Color.Red; + + void Run1(object value) + { + switch (value) + { + case Color.Red or Point: + break; + } + } + + void Run2(object value) + { + switch (value) + { + case Point or Color.Red: + break; + } + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Point", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + var rows = ParseJsonLines(stdout); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(4, rows.Count); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_SqlQuotedTvfCallsStayVisible() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_sql_quoted_tvf_calls"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/repro.sql", "sql", + """ + SELECT * FROM [dbo].[fn_GetUserStats](42); + SELECT * FROM `fn_GetUserStats`(42); + SELECT * FROM dbo.fn_GetUserStats(42); + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["fn_GetUserStats", "--db", dbPath, "--json", "--lang", "sql", "--exact-name"], + _jsonOptions)); + + var rows = ParseJsonLines(stdout) + .Select(document => document.RootElement) + .ToList(); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(3, rows.Count); + Assert.All(rows, row => + { + Assert.Equal("fn_GetUserStats", row.GetProperty("symbol_name").GetString()); + Assert.Equal("call", row.GetProperty("reference_kind").GetString()); + }); + Assert.Contains(rows, row => row.GetProperty("line").GetInt32() == 1); + Assert.Contains(rows, row => row.GetProperty("line").GetInt32() == 2); + Assert.Contains(rows, row => row.GetProperty("line").GetInt32() == 3); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_NonExactJson_CSharpMultiLineUsingStaticConstantPatternKeepsRows() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_multiline_constant_pattern_refs"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Defs.cs", "csharp", + """ + namespace Probe; + + public enum Color + { + Red + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Use.cs", "csharp", + """ + using static Probe.Color; + + namespace Probe; + + class Demo + { + bool Match(object value) => value is + Red + or + Red; + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--path", "src/Use.cs"], + _jsonOptions)); + + var rows = ParseJsonLines(stdout) + .Select(document => document.RootElement) + .ToList(); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(2, rows.Count); + Assert.Equal([8, 10], rows.Select(row => row.GetProperty("line").GetInt32()).ToArray()); + Assert.All(rows, row => Assert.Equal("Match", row.GetProperty("container_name").GetString())); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpMultiLineCaseTypePatternsKeepFirstAndLaterHeads() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_multiline_case_type_patterns"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Probe; + + class Point {} + class Shape {} + + class Demo + { + void Run(object value) + { + switch (value) + { + case + Point: + break; + case Point or + Shape: + break; + } + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (pointExitCode, pointStdout, pointStderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Point", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + var pointRows = ParseJsonLines(pointStdout) + .Select(document => document.RootElement) + .ToList(); + + Assert.Equal(CommandExitCodes.Success, pointExitCode); + Assert.Equal(string.Empty, pointStderr); + Assert.Equal(2, pointRows.Count); + Assert.Equal([13, 15], pointRows.Select(row => row.GetProperty("line").GetInt32()).OrderBy(line => line).ToArray()); + Assert.All(pointRows, row => Assert.Equal("Run", row.GetProperty("container_name").GetString())); + + var (shapeExitCode, shapeStdout, shapeStderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Shape", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + var shapeRows = ParseJsonLines(shapeStdout) + .Select(document => document.RootElement) + .ToList(); + + Assert.Equal(CommandExitCodes.Success, shapeExitCode); + Assert.Equal(string.Empty, shapeStderr); + var shapeRow = Assert.Single(shapeRows); + Assert.Equal(16, shapeRow.GetProperty("line").GetInt32()); + Assert.Equal("Run", shapeRow.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpCommentSeparatedMultiLineTypePatternsKeepRows() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_comment_separated_multiline_type_patterns"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Probe; + + class Point {} + + class Demo + { + bool Match(object value) => value is + // formatting-only comment + Point; + + void Run(object value) + { + switch (value) + { + case + // formatting-only comment + Point: + break; + } + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Point", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + var rows = ParseJsonLines(stdout) + .Select(document => document.RootElement) + .ToList(); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(2, rows.Count); + Assert.Equal([9, 17], rows.Select(row => row.GetProperty("line").GetInt32()).OrderBy(line => line).ToArray()); + Assert.Equal(["Match", "Run"], rows.Select(row => row.GetProperty("container_name").GetString()).OrderBy(name => name).ToArray()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpStandaloneNotLineMultiLineTypePatternsKeepRows() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_standalone_not_line_multiline_type_patterns"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Probe; + + class Point {} + + class Demo + { + bool Match(object value) => value is + not + Point; + + void Run(object value) + { + switch (value) + { + case + not + Point: + break; + } + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Point", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + var rows = ParseJsonLines(stdout) + .Select(document => document.RootElement) + .ToList(); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(2, rows.Count); + Assert.Equal([9, 17], rows.Select(row => row.GetProperty("line").GetInt32()).OrderBy(line => line).ToArray()); + Assert.Equal(["Match", "Run"], rows.Select(row => row.GetProperty("container_name").GetString()).OrderBy(name => name).ToArray()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpNonTypeCaseLabelsDoNotEmitPhantomTypeReferences() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_non_type_case_labels"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Probe; + + class Demo + { + void Run(int value) + { + switch (value) + { + case > 0: + Target(); + break; + } + } + + void Target() {} + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Target", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + var rows = ParseJsonLines(stdout) + .Select(document => document.RootElement) + .ToList(); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + var row = Assert.Single(rows); + Assert.Equal("call", row.GetProperty("reference_kind").GetString()); + Assert.Equal(10, row.GetProperty("line").GetInt32()); + Assert.Equal("Run", row.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_NonExactJson_CSharpMultiLineCaseUsingStaticConstantPatternKeepsRows() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_multiline_case_constant_pattern_refs"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Defs.cs", "csharp", + """ + namespace Probe; + + public enum Color + { + Red + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Use.cs", "csharp", + """ + using static Probe.Color; + + namespace Probe; + + class Demo + { + void Run(object value) + { + switch (value) + { + case + Red + or + Red: + break; + } + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--path", "src/Use.cs"], + _jsonOptions)); + + var rows = ParseJsonLines(stdout) + .Select(document => document.RootElement) + .ToList(); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(2, rows.Count); + Assert.Equal([12, 14], rows.Select(row => row.GetProperty("line").GetInt32()).OrderBy(line => line).ToArray()); + Assert.All(rows, row => Assert.Equal("Run", row.GetProperty("container_name").GetString())); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_NonExactJson_CSharpCommentSeparatedMultiLineCaseUsingStaticConstantPatternKeepsRows() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_comment_separated_multiline_case_constant_pattern_refs"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Defs.cs", "csharp", + """ + namespace Probe; + + public enum Color + { + Red + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Use.cs", "csharp", + """ + using static Probe.Color; + + namespace Probe; + + class Demo + { + void Run(object value) + { + switch (value) + { + case + // formatting-only comment + Red + or + Red: + break; + } + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--path", "src/Use.cs"], + _jsonOptions)); + + var rows = ParseJsonLines(stdout) + .Select(document => document.RootElement) + .ToList(); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(2, rows.Count); + Assert.Equal([13, 15], rows.Select(row => row.GetProperty("line").GetInt32()).OrderBy(line => line).ToArray()); + Assert.All(rows, row => Assert.Equal("Run", row.GetProperty("container_name").GetString())); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpCommentSeparatedMultiLineCaseUsingStaticConstantPattern_StaysSuppressed() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_comment_separated_multiline_case_constant_pattern_exact_suppressed"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "Defs.cs"), + """ + namespace Probe; + + public enum Color + { + Red + } + """); + File.WriteAllText( + Path.Combine(projectRoot, "src", "Use.cs"), + """ + using static Probe.Color; + + namespace Probe; + + class Demo + { + void Run(object value) + { + switch (value) + { + case + // formatting-only comment + Red + or + Red: + break; + } + } + } + """); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (nonExactExitCode, nonExactStdout, nonExactStderr) = RunBuiltCli( + ["references", "Red", "--db", dbPath, "--json", "--lang", "csharp", "--path", "src/Use.cs", "--limit", "100"]); + var nonExactRows = ParseJsonLines(nonExactStdout) + .Select(document => document.RootElement) + .ToList(); + + var (exactExitCode, exactStdout, exactStderr) = RunBuiltCli( + ["references", "Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + using var exactDocument = ParseJsonOutput(exactStdout); + + var (countExitCode, countStdout, countStderr) = RunBuiltCli( + ["references", "Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name", "--count"]); + using var countDocument = ParseJsonOutput(countStdout); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + + Assert.Equal(CommandExitCodes.Success, nonExactExitCode); + Assert.Equal(string.Empty, nonExactStderr); + Assert.Equal(2, nonExactRows.Count); + Assert.Equal([13, 15], nonExactRows.Select(row => row.GetProperty("line").GetInt32()).OrderBy(line => line).ToArray()); + + Assert.Equal(CommandExitCodes.Success, exactExitCode); + Assert.Equal(string.Empty, exactStderr); + Assert.Equal(0, exactDocument.RootElement.GetProperty("count").GetInt32()); + + Assert.Equal(CommandExitCodes.Success, countExitCode); + Assert.Equal(string.Empty, countStderr); + Assert.Equal(0, countDocument.RootElement.GetProperty("count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpBlankLineSeparatedMultiLineCaseUsingStaticConstantPattern_StaysSuppressed() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_blank_line_multiline_case_constant_pattern_exact_suppressed"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "Defs.cs"), + """ + namespace Probe; + + public enum Color + { + Red + } + """); + File.WriteAllText( + Path.Combine(projectRoot, "src", "Use.cs"), + """ + using static Probe.Color; + + namespace Probe; + + class Demo + { + void Run(object value) + { + switch (value) + { + case + + Red + or + Red: + break; + } + } + } + """); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (nonExactExitCode, nonExactStdout, nonExactStderr) = RunBuiltCli( + ["references", "Red", "--db", dbPath, "--json", "--lang", "csharp", "--path", "src/Use.cs", "--limit", "100"]); + var nonExactRows = ParseJsonLines(nonExactStdout) + .Select(document => document.RootElement) + .ToList(); + + var (exactExitCode, exactStdout, exactStderr) = RunBuiltCli( + ["references", "Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + using var exactDocument = ParseJsonOutput(exactStdout); + + var (countExitCode, countStdout, countStderr) = RunBuiltCli( + ["references", "Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name", "--count"]); + using var countDocument = ParseJsonOutput(countStdout); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + + Assert.Equal(CommandExitCodes.Success, nonExactExitCode); + Assert.Equal(string.Empty, nonExactStderr); + Assert.Equal(2, nonExactRows.Count); + Assert.Equal([13, 15], nonExactRows.Select(row => row.GetProperty("line").GetInt32()).OrderBy(line => line).ToArray()); + + Assert.Equal(CommandExitCodes.Success, exactExitCode); + Assert.Equal(string.Empty, exactStderr); + Assert.Equal(0, exactDocument.RootElement.GetProperty("count").GetInt32()); + + Assert.Equal(CommandExitCodes.Success, countExitCode); + Assert.Equal(string.Empty, countStderr); + Assert.Equal(0, countDocument.RootElement.GetProperty("count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_CSharpLongMultiLineCaseUsingStaticConstantPattern_NonExactKeepsRows_ExactSuppressesAll() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_long_multiline_case_constant_pattern_suppressed"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "Defs.cs"), + """ + namespace Probe; + + public enum Color + { + Red + } + """); + var sourceBuilder = new System.Text.StringBuilder(); + sourceBuilder.AppendLine("using static Probe.Color;"); + sourceBuilder.AppendLine(); + sourceBuilder.AppendLine("namespace Probe;"); + sourceBuilder.AppendLine(); + sourceBuilder.AppendLine("class Demo"); + sourceBuilder.AppendLine("{"); + sourceBuilder.AppendLine(" void Run(object value)"); + sourceBuilder.AppendLine(" {"); + sourceBuilder.AppendLine(" switch (value)"); + sourceBuilder.AppendLine(" {"); + sourceBuilder.AppendLine(" case"); + for (var index = 0; index < 70; index++) + { + sourceBuilder.Append(" Red"); + sourceBuilder.AppendLine(index == 69 ? ":" : string.Empty); + if (index < 69) + sourceBuilder.AppendLine(" or"); + } + sourceBuilder.AppendLine(" break;"); + sourceBuilder.AppendLine(" }"); + sourceBuilder.AppendLine(" }"); + sourceBuilder.AppendLine("}"); + File.WriteAllText(Path.Combine(projectRoot, "src", "Use.cs"), sourceBuilder.ToString()); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (nonExactExitCode, nonExactStdout, nonExactStderr) = RunBuiltCli( + ["references", "Red", "--db", dbPath, "--json", "--lang", "csharp", "--path", "src/Use.cs", "--limit", "100"]); + var nonExactRows = ParseJsonLines(nonExactStdout) + .Select(document => document.RootElement) + .ToList(); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + + Assert.Equal(CommandExitCodes.Success, nonExactExitCode); + Assert.Equal(string.Empty, nonExactStderr); + Assert.Equal(70, nonExactRows.Count); + Assert.Equal(Enumerable.Range(0, 70).Select(index => 12 + (index * 2)).ToArray(), nonExactRows.Select(row => row.GetProperty("line").GetInt32()).OrderBy(line => line).ToArray()); + Assert.All(nonExactRows, row => Assert.Equal("Run", row.GetProperty("container_name").GetString())); + + var (exactExitCode, exactStdout, exactStderr) = RunBuiltCli( + ["references", "Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + using var exactDocument = ParseJsonOutput(exactStdout); + + var (countExitCode, countStdout, countStderr) = RunBuiltCli( + ["references", "Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name", "--count"]); + using var countDocument = ParseJsonOutput(countStdout); + + Assert.Equal(CommandExitCodes.Success, exactExitCode); + Assert.Equal(string.Empty, exactStderr); + Assert.Equal(0, exactDocument.RootElement.GetProperty("count").GetInt32()); + + Assert.Equal(CommandExitCodes.Success, countExitCode); + Assert.Equal(string.Empty, countStderr); + Assert.Equal(0, countDocument.RootElement.GetProperty("count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpQualifiedMultiLineCaseLogicalConstantPattern_StaysSuppressed() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_qualified_multiline_case_constant_pattern_suppressed"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Defs.cs", "csharp", + """ + namespace Probe; + + public enum Color + { + Red, + Blue + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Use.cs", "csharp", + """ + namespace Probe; + + class Demo + { + void Run(object value) + { + switch (value) + { + case Color.Red or + Color.Blue: + break; + } + } + } + """); + MarkGraphAndFoldReady(dbPath); + + foreach (var symbolName in new[] { "Red", "Blue" }) + { + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + [symbolName, "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, document.RootElement.GetProperty("count").GetInt32()); + } + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpQualifiedConstantPatternSameFileEnumMemberSitesStaySuppressed() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_qualified_constant_pattern_same_file_enum_member_sites_suppressed"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "Use.cs"), + """ + namespace Probe; + + public enum Color + { + Red, + Blue + } + + public class Red {} + + class Demo + { + void Run(object value) + { + switch (value) + { + case Color.Red or Color.Blue: + break; + } + } + } + """); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (referencesExitCode, referencesStdout, referencesStderr) = RunBuiltCli( + ["references", "Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + using var referencesDocument = ParseJsonOutput(referencesStdout); + + var (countExitCode, countStdout, countStderr) = RunBuiltCli( + ["references", "Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name", "--count"]); + using var countDocument = ParseJsonOutput(countStdout); + + var (callersExitCode, callersStdout, callersStderr) = RunBuiltCli( + ["callers", "Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + using var callersDocument = ParseJsonOutput(callersStdout); + + 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(CommandExitCodes.Success, countExitCode); + Assert.Equal(string.Empty, countStderr); + Assert.Equal(0, countDocument.RootElement.GetProperty("count").GetInt32()); + + Assert.Equal(CommandExitCodes.Success, callersExitCode); + Assert.Equal(string.Empty, callersStderr); + Assert.Equal(0, callersDocument.RootElement.GetProperty("count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpLogicalPatternsKeepLaterTypeHeads() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_logical_type_pattern_all_heads"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Probe; + + class Outer + { + public class Red {} + public class Blue {} + } + + class Demo + { + bool Match(object value) => value is Outer.Red or Outer.Blue; + + void Run(object value) + { + switch (value) + { + case Outer.Red or Outer.Blue: + break; + } + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Blue", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + var rows = ParseJsonLines(stdout); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(2, rows.Count); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpSwitchExpressionTypePatternsEmitOnlyGenuineTypeHeads() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_switch_expression_type_patterns"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Probe; + + class Point {} + class Shape {} + enum Color { Red } + + class Demo + { + int Match(object value) => value switch + { + Point => 1, + Point or Shape => 2, + Color.Red => 3, + _ => 0, + }; + } + """); + MarkGraphAndFoldReady(dbPath); + + var (pointExitCode, pointStdout, pointStderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Point", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + var pointRows = ParseJsonLines(pointStdout); + + Assert.Equal(CommandExitCodes.Success, pointExitCode); + Assert.Equal(string.Empty, pointStderr); + Assert.Equal(2, pointRows.Count); + + var (shapeExitCode, shapeStdout, shapeStderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Shape", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + var shapeRows = ParseJsonLines(shapeStdout); + + Assert.Equal(CommandExitCodes.Success, shapeExitCode); + Assert.Equal(string.Empty, shapeStderr); + Assert.Single(shapeRows); + + var (redExitCode, redStdout, redStderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + using var redDocument = ParseJsonOutput(redStdout); + var redJson = redDocument.RootElement; + + Assert.Equal(CommandExitCodes.Success, redExitCode); + Assert.Equal(string.Empty, redStderr); + Assert.Equal("call", redJson.GetProperty("reference_kind").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpSwitchExpressionGenericTypePatternsKeepOuterTypeAndArguments() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_switch_expression_generic_type_patterns"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Probe; + + class Point {} + class Shape {} + class Wrapper {} + + class Demo + { + int Match(object value) => value switch + { + Wrapper => 1, + _ => 0, + }; + } + """); + MarkGraphAndFoldReady(dbPath); + + var (wrapperExitCode, wrapperStdout, wrapperStderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Wrapper", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + var wrapperRows = ParseJsonLines(wrapperStdout); + + var (pointExitCode, pointStdout, pointStderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Point", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + var pointRows = ParseJsonLines(pointStdout); + + var (shapeExitCode, shapeStdout, shapeStderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Shape", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + var shapeRows = ParseJsonLines(shapeStdout); + + Assert.Equal(CommandExitCodes.Success, wrapperExitCode); + Assert.Equal(string.Empty, wrapperStderr); + Assert.Single(wrapperRows); + + Assert.Equal(CommandExitCodes.Success, pointExitCode); + Assert.Equal(string.Empty, pointStderr); + Assert.Single(pointRows); + + Assert.Equal(CommandExitCodes.Success, shapeExitCode); + Assert.Equal(string.Empty, shapeStderr); + Assert.Single(shapeRows); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpSwitchExpressionGenericDeclarationPatternWhenGuardKeepsArmHead() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_switch_expression_generic_when_guard"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Probe; + + class Wrapper {} + class Point { public int X { get; init; } } + class Shape {} + + class Demo + { + int Match(object value, int limit) => value switch + { + Wrapper p when p is Wrapper && limit > p.GetHashCode() => 1, + _ => 0, + }; + } + """); + MarkGraphAndFoldReady(dbPath); + + var (wrapperExitCode, wrapperStdout, wrapperStderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Wrapper", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + var wrapperRows = ParseJsonLines(wrapperStdout) + .Select(document => ( + Line: document.RootElement.GetProperty("line").GetInt32(), + Column: document.RootElement.GetProperty("column").GetInt32(), + ContainerName: document.RootElement.GetProperty("container_name").GetString())) + .OrderBy(row => row.Line) + .ThenBy(row => row.Column) + .ToArray(); + + Assert.Equal(CommandExitCodes.Success, wrapperExitCode); + Assert.Equal(string.Empty, wrapperStderr); + Assert.Equal([11, 11], wrapperRows.Select(row => row.Line).ToArray()); + Assert.Equal([9, 43], wrapperRows.Select(row => row.Column).ToArray()); + Assert.All(wrapperRows, row => Assert.Equal("Match", row.ContainerName)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpSwitchExpressionFunctionWhenGuardKeepsArmHead() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_switch_expression_function_when_guard"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Probe; + + class Wrapper {} + class Point {} + class Shape {} + + class Demo + { + static bool Check(object value) => true; + + int Match(object value) => value switch + { + Wrapper p when Check(p) => 1, + _ => 0, + }; + } + """); + MarkGraphAndFoldReady(dbPath); + + var (wrapperExitCode, wrapperStdout, wrapperStderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Wrapper", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + var wrapperRows = ParseJsonLines(wrapperStdout) + .Select(document => ( + Line: document.RootElement.GetProperty("line").GetInt32(), + Column: document.RootElement.GetProperty("column").GetInt32(), + ContainerName: document.RootElement.GetProperty("container_name").GetString())) + .OrderBy(row => row.Line) + .ThenBy(row => row.Column) + .ToArray(); + + Assert.Equal(CommandExitCodes.Success, wrapperExitCode); + Assert.Equal(string.Empty, wrapperStderr); + Assert.Equal([13], wrapperRows.Select(row => row.Line).ToArray()); + Assert.Equal([9], wrapperRows.Select(row => row.Column).ToArray()); + Assert.All(wrapperRows, row => Assert.Equal("Match", row.ContainerName)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpSwitchExpressionLaterArmAfterWhenGuardStillEmitsTypeHead() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_switch_expression_later_arm_after_when_guard"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Probe; + + class Point {} + class Shape {} + + class Demo + { + int Match(object value) => value switch + { + Point p when p.GetHashCode() > 0 => 1, + Shape => 2, + _ => 0, + }; + } + """); + MarkGraphAndFoldReady(dbPath); + + var (shapeExitCode, shapeStdout, shapeStderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Shape", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + var shapeRows = ParseJsonLines(shapeStdout) + .Select(document => document.RootElement.GetProperty("container_name").GetString()) + .ToArray(); + + Assert.Equal(CommandExitCodes.Success, shapeExitCode); + Assert.Equal(string.Empty, shapeStderr); + Assert.Equal(["Match"], shapeRows); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpVerbatimPatternTypesSurviveBareTokenFilter() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_verbatim_pattern_types"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Probe; + + class @not {} + class @default {} + + class Demo + { + bool MatchNot(object value) => value is @not; + bool MatchDefault(object value) => value is @default; + bool Guard(object value) => value is not null; + bool TypeOfNot() => typeof(@not) == typeof(@not); + bool TypeOfDefault() => typeof(@default) == typeof(@default); + + void Run(object value) + { + switch (value) + { + case @not: + break; + case @default: + break; + case default: + break; + } + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (notExitCode, notStdout, notStderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["not", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + var notRows = ParseJsonLines(notStdout); + + var (defaultExitCode, defaultStdout, defaultStderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["default", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + var defaultRows = ParseJsonLines(defaultStdout); + + Assert.Equal(CommandExitCodes.Success, notExitCode); + Assert.Equal(string.Empty, notStderr); + Assert.Equal(4, notRows.Count); + + Assert.Equal(CommandExitCodes.Success, defaultExitCode); + Assert.Equal(string.Empty, defaultStderr); + Assert.Equal(4, defaultRows.Count); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpUsingStaticConstantPatternsDoNotLeakAcrossFiles() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_using_static_constant_pattern_cross_file"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Defs.cs", "csharp", + """ + namespace Probe; + + public enum Color + { + Red, + Blue + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Use.cs", "csharp", + """ + using static Probe.Color; + + namespace Probe; + + class Point {} + + class Demo + { + bool Match(object value) => value is Red or Blue or Point; + + void Run(object value) + { + switch (value) + { + case Red: + break; + case Red or Blue: + break; + case Red or Point: + break; + } + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (redExitCode, redStdout, redStderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + using var redDocument = ParseJsonOutput(redStdout); + + var (blueExitCode, blueStdout, blueStderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Blue", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + using var blueDocument = ParseJsonOutput(blueStdout); + + var (redCountExitCode, redCountStdout, redCountStderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name", "--count"], + _jsonOptions)); + using var redCountDocument = ParseJsonOutput(redCountStdout); + + Assert.Equal(CommandExitCodes.Success, redExitCode); + Assert.Equal(string.Empty, redStderr); + Assert.Equal(0, redDocument.RootElement.GetProperty("count").GetInt32()); + + Assert.Equal(CommandExitCodes.Success, blueExitCode); + Assert.Equal(string.Empty, blueStderr); + Assert.Equal(0, blueDocument.RootElement.GetProperty("count").GetInt32()); + + Assert.Equal(CommandExitCodes.Success, redCountExitCode); + Assert.Equal(string.Empty, redCountStderr); + Assert.Equal(0, redCountDocument.RootElement.GetProperty("count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpGlobalUsingStaticConstantPatternsDoNotLeakAcrossFiles() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_global_using_static_constant_pattern_cross_file"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Defs.cs", "csharp", + """ + namespace Probe; + + public enum Color + { + Red, + Blue + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/GlobalUsings.cs", "csharp", + """ + global using static Probe.Color; + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Use.cs", "csharp", + """ + namespace Probe; + + class Demo + { + bool Match(object value) => value is Red or Blue; + } + """); + MarkGraphAndFoldReady(dbPath); + + var (redExitCode, redStdout, redStderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + using var redDocument = ParseJsonOutput(redStdout); + + var (redCountExitCode, redCountStdout, redCountStderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name", "--count"], + _jsonOptions)); + using var redCountDocument = ParseJsonOutput(redCountStdout); + + Assert.Equal(CommandExitCodes.Success, redExitCode); + Assert.Equal(string.Empty, redStderr); + Assert.Equal(0, redDocument.RootElement.GetProperty("count").GetInt32()); + + Assert.Equal(CommandExitCodes.Success, redCountExitCode); + Assert.Equal(string.Empty, redCountStderr); + Assert.Equal(0, redCountDocument.RootElement.GetProperty("count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpGlobalUsingNamespaceSameNameTypePatternStaysVisible() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_global_using_namespace_same_name_type_pattern_visible"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Defs.cs", "csharp", + """ + namespace Probe; + + public enum Color + { + Red + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/GlobalUsings.cs", "csharp", + """ + global using RealTypes; + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/RealRed.cs", "csharp", + """ + namespace RealTypes; + + public class Red {} + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Use.cs", "csharp", + """ + using static Probe.Color; + + namespace Probe; + + class Demo + { + bool Match(object value) => value is Red; + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + var row = Assert.Single(ParseJsonLines(stdout)).RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("Red", row.GetProperty("symbol_name").GetString()); + Assert.Equal("type_reference", row.GetProperty("reference_kind").GetString()); + Assert.Contains("value is Red", row.GetProperty("context").GetString(), StringComparison.Ordinal); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpSwitchExpressionLaterArmAfterWhenGuardStaysVisible() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_switch_expression_later_arm_after_when"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + namespace Probe; + + class Point {} + class Shape {} + + class Demo + { + int Match(object value) => value switch + { + Point p when p.GetHashCode() > 0 => 1, + Shape => 2, + _ => 0, + }; + } + """); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Shape", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + var row = Assert.Single(ParseJsonLines(stdout)).RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("Shape", row.GetProperty("symbol_name").GetString()); + Assert.Equal("type_reference", row.GetProperty("reference_kind").GetString()); + Assert.Contains("Shape => 2", row.GetProperty("context").GetString(), StringComparison.Ordinal); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Theory] + [InlineData("Point { X: < 0 } => 1,")] + [InlineData("Point { X: > 0 } => 1,")] + public void RunReferences_ExactJson_CSharpSwitchExpressionLaterArmAfterRelationalPatternStaysVisible(string previousArm) + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_switch_expression_later_arm_after_relational"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + $$""" + namespace Probe; + + class Point { public int X { get; init; } } + class Shape {} + + class Demo + { + int Match(object value) => value switch + { + {{previousArm}} + Shape => 2, + _ => 0, + }; + } + """); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Shape", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + var row = Assert.Single(ParseJsonLines(stdout)).RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("Shape", row.GetProperty("symbol_name").GetString()); + Assert.Equal("type_reference", row.GetProperty("reference_kind").GetString()); + Assert.Contains("Shape => 2", row.GetProperty("context").GetString(), StringComparison.Ordinal); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Theory] + [InlineData("Point p when p.GetHashCode() > 0 => 1,")] + [InlineData("Point { X: < 0 } => 1,")] + public void RunReferences_ExactJson_CSharpSwitchExpressionLaterGenericArmStaysVisible(string previousArm) + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_switch_expression_later_generic_arm"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + $$""" + namespace Probe; + + class Point { public int X { get; init; } } + class Shape {} + class Wrapper {} + + class Demo + { + int Match(object value) => value switch + { + {{previousArm}} + Wrapper => 2, + _ => 0, + }; + } + """); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Wrapper", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + var row = Assert.Single(ParseJsonLines(stdout)).RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("Wrapper", row.GetProperty("symbol_name").GetString()); + Assert.Equal("type_reference", row.GetProperty("reference_kind").GetString()); + Assert.Contains("Wrapper => 2", row.GetProperty("context").GetString(), StringComparison.Ordinal); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpCrossFileSameNamespaceTypePatternStaysVisible() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_cross_file_same_namespace_type_pattern_visible"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "Use.cs"), + """ + using static Probe.Color; + + namespace Probe; + + enum Color { Red } + + class Demo + { + bool Match(object value) => value is Red; + void ProbeType() { _ = typeof(Red); } + } + """); + File.WriteAllText( + Path.Combine(projectRoot, "src", "Other.cs"), + """ + namespace Probe; + + class Red {} + """); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + var rows = ParseJsonLines(stdout).Select(line => line.RootElement).ToList(); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(2, rows.Count); + Assert.All(rows, row => Assert.Equal("type_reference", row.GetProperty("reference_kind").GetString())); + Assert.Contains(rows, row => row.GetProperty("context").GetString()!.Contains("value is Red", StringComparison.Ordinal)); + Assert.Contains(rows, row => row.GetProperty("context").GetString()!.Contains("typeof(Red)", StringComparison.Ordinal)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpUsingAliasDoesNotRescueUnqualifiedTypePattern() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_using_alias_does_not_rescue_unqualified_type_pattern"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Defs.cs", "csharp", + """ + namespace Probe; + + public enum Color + { + Red + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Shadow.cs", "csharp", + """ + namespace Shadow; + + public class Red {} + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Use.cs", "csharp", + """ + using static Probe.Color; + using Shadow = Probe; + + namespace Real; + + class Demo + { + bool Match(object value) => value is Red; + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + using var document = ParseJsonOutput(stdout); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, document.RootElement.GetProperty("count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpUsingNamespaceImportPatternStaysVisible() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_using_namespace_import_pattern_visible"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "Repro.cs"), + """ + using static Probe.Color; + using RealTypes; + + namespace Probe + { + enum Color { Red } + + class Demo + { + bool Match(object value) => value is Red; + } + } + + namespace RealTypes + { + class Red {} + } + """); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + var row = Assert.Single(ParseJsonLines(stdout)).RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("type_reference", row.GetProperty("reference_kind").GetString()); + Assert.Contains("value is Red", row.GetProperty("context").GetString(), StringComparison.Ordinal); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpCrossFileFileTypeDoesNotRescueUnqualifiedTypePattern() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_cross_file_file_type_does_not_rescue_unqualified_type_pattern"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Defs.cs", "csharp", + """ + namespace Probe; + + public enum Color + { + Red + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/FileLocal.cs", "csharp", + """ + namespace Probe; + + file class Red {} + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Use.cs", "csharp", + """ + using static Probe.Color; + + namespace Probe; + + class Demo + { + bool Match(object value) => value is Red; + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + using var document = ParseJsonOutput(stdout); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, document.RootElement.GetProperty("count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpSameFileFileTypePatternStaysVisible() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_same_file_file_type_pattern_visible"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "Repro.cs"), + """ + using static Probe.Color; + + namespace Probe; + + public enum Color + { + Red + } + + file class Red {} + + class Demo + { + bool Match(object value) => value is Red; + } + """); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + var row = Assert.Single(ParseJsonLines(stdout)).RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("type_reference", row.GetProperty("reference_kind").GetString()); + Assert.Contains("value is Red", row.GetProperty("context").GetString(), StringComparison.Ordinal); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpUsingStaticConstantPatternsIgnoreTriviaAroundKeywords() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_using_static_constant_pattern_ignores_trivia"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Defs.cs", "csharp", + """ + namespace Probe; + + public enum Color + { + Red, + Blue + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Use.cs", "csharp", + """ + using static Probe.Color; + + namespace Probe; + + class Demo + { + bool MatchTab(object value) => value is Red; + bool MatchComment(object value) => value is/*comment*/Red; + + void Run(object value) + { + switch (value) + { + case Red: + break; + case/*comment*/Red: + break; + } + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + using var document = ParseJsonOutput(stdout); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, document.RootElement.GetProperty("count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpUsingStaticConstantPatternsUseMatchedColumnOnSharedLine() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_using_static_constant_pattern_column_sensitive"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/GlobalUsings.cs", "csharp", + """ + global using static Probe.Color; + + namespace Probe; + + enum Color { Red, Blue } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Use.cs", "csharp", + """ + namespace Probe; + + class Demo + { + string Run(object value) => nameof(Red) + (value is Red).ToString(); + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + var rows = ParseJsonLines(stdout).Select(line => line.RootElement).ToList(); + var row = Assert.Single(rows); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("type_reference", row.GetProperty("reference_kind").GetString()); + Assert.Equal(40, row.GetProperty("column").GetInt32()); + Assert.Contains("nameof(Red)", row.GetProperty("context").GetString(), StringComparison.Ordinal); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpUsingStaticConstantPatternsPreserveTypeAliasPatterns() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_using_static_constant_pattern_type_alias"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Defs.cs", "csharp", + """ + namespace Probe + { + public enum Color + { + Red + } + + namespace Real + { + public class Red {} + } + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Use.cs", "csharp", + """ + using static Probe.Color; + using Red = Probe.Real.Red; + + namespace Probe; + + class Demo + { + bool Match(object value) => value is Red; + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + var row = Assert.Single(ParseJsonLines(stdout)).RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("type_reference", row.GetProperty("reference_kind").GetString()); + Assert.Contains("value is Red", row.GetProperty("context").GetString(), StringComparison.Ordinal); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpUsingStaticConstantPatternsPreserveTypeAliasPatternAcrossNamespaces() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_using_static_constant_pattern_type_alias_across_namespaces"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Defs.cs", "csharp", + """ + namespace Probe; + + public enum Color + { + Red + } + + namespace Shapes + { + public class Red {} + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Use.cs", "csharp", + """ + using static Probe.Color; + using Red = Probe.Shapes.Red; + + namespace Probe; + + class Demo + { + bool Match(object value) => value is Red; + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + var row = Assert.Single(ParseJsonLines(stdout)).RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("type_reference", row.GetProperty("reference_kind").GetString()); + Assert.Contains("value is Red", row.GetProperty("context").GetString(), StringComparison.Ordinal); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpUsingStaticConstantPatternsStaySuppressedWhenContextClamped() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_using_static_constant_pattern_clamped"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "Defs.cs"), + """ + namespace Probe; + + public enum Color + { + Red, + Blue + } + """); + File.WriteAllText( + Path.Combine(projectRoot, "src", "Use.cs"), + """ + using static Probe.Color; + + namespace Probe; + + class Demo + { + bool Match(object value) + { + return value is Red; + } + } + """); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name", "--max-line-width", "8"]); + + using var document = ParseJsonOutput(stdout); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, document.RootElement.GetProperty("count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpUsingStaticSameNamespaceTypePatternStaysVisible() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_using_static_same_namespace_type_pattern_visible"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Defs.cs", "csharp", + """ + namespace Probe; + + public enum Color + { + Red + } + + public class Red {} + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Use.cs", "csharp", + """ + using static Probe.Color; + + namespace Probe; + + class Demo + { + bool Match(object value) => value is Red; + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + var row = Assert.Single(ParseJsonLines(stdout)).RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("Red", row.GetProperty("symbol_name").GetString()); + Assert.Equal("type_reference", row.GetProperty("reference_kind").GetString()); + Assert.Contains("value is Red", row.GetProperty("context").GetString(), StringComparison.Ordinal); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpUsingStaticSameNamespaceTypeofStaysVisible() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_using_static_same_namespace_typeof_visible"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Defs.cs", "csharp", + """ + namespace Probe; + + public enum Color + { + Red + } + + public class Red {} + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Use.cs", "csharp", + """ + using static Probe.Color; + + namespace Probe; + + class Demo + { + void Match() + { + _ = typeof(Red); + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + var row = Assert.Single(ParseJsonLines(stdout)).RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("Red", row.GetProperty("symbol_name").GetString()); + Assert.Equal("type_reference", row.GetProperty("reference_kind").GetString()); + Assert.Contains("typeof(Red)", row.GetProperty("context").GetString(), StringComparison.Ordinal); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpUsingStaticNestedSameNameTypePatternStaysVisible() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_using_static_nested_same_name_type_pattern_visible"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using static Probe.Color; + + namespace Probe; + + enum Color + { + Red + } + + class Outer + { + class Red {} + + bool Match(object value) => value is Red; + + void Run() + { + _ = typeof(Red); + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["references", "Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + var rows = ParseJsonLines(stdout).Select(doc => doc.RootElement).ToList(); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(2, rows.Count); + Assert.Contains(rows, row => row.GetProperty("context").GetString()!.Contains("value is Red", StringComparison.Ordinal)); + Assert.Contains(rows, row => row.GetProperty("context").GetString()!.Contains("typeof(Red)", StringComparison.Ordinal)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpUsingStaticTopLevelSameNameTypePatternStaysVisible() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_using_static_top_level_same_name_type_pattern_visible"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Defs.cs", "csharp", + """ + namespace Probe; + + public enum Color + { + Red + } + + public class Red {} + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Use.cs", "csharp", + """ + using static Probe.Color; + + namespace Probe; + + class Demo + { + bool Match(object value) => value is Red; + + bool Switch(object value) => value switch + { + Red => true, + _ => false, + }; + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + var rows = ParseJsonLines(stdout).Select(doc => doc.RootElement).ToList(); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(2, rows.Count); + Assert.Contains(rows, row => row.GetProperty("context").GetString()!.Contains("value is Red", StringComparison.Ordinal)); + Assert.Contains(rows, row => row.GetProperty("context").GetString()!.Contains("Red => true", StringComparison.Ordinal)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpUsingStaticTopLevelSameNameTypePatternStaysSuppressedWithoutType() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_using_static_top_level_same_name_type_pattern_suppressed_without_type"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Defs.cs", "csharp", + """ + namespace Probe; + + public enum Color + { + Red + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Use.cs", "csharp", + """ + using static Probe.Color; + + namespace Probe; + + class Demo + { + bool Match(object value) => value is Red; + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, document.RootElement.GetProperty("count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpUsingStaticInheritedProtectedNestedTypePatternStaysVisible() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_using_static_inherited_protected_nested_type_pattern_visible"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Defs.cs", "csharp", + """ + namespace Probe; + + public enum Color + { + Red + } + + public class Base + { + protected class Red {} + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Use.cs", "csharp", + """ + using static Probe.Color; + + namespace Probe; + + class Derived : Base + { + bool Match(object value) => value is Red; + + void ProbeType() + { + _ = typeof(Red); + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + var rows = ParseJsonLines(stdout).Select(doc => doc.RootElement).ToList(); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(2, rows.Count); + Assert.Contains(rows, row => row.GetProperty("context").GetString()!.Contains("value is Red", StringComparison.Ordinal)); + Assert.Contains(rows, row => row.GetProperty("context").GetString()!.Contains("typeof(Red)", StringComparison.Ordinal)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpUsingStaticInheritedConstantOnlyPatternStaysSuppressed() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_using_static_inherited_constant_only_pattern_suppressed"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Defs.cs", "csharp", + """ + namespace Probe; + + public enum Color + { + Red + } + + public class Base {} + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Use.cs", "csharp", + """ + using static Probe.Color; + + namespace Probe; + + class Derived : Base + { + bool Match(object value) => value is Red; + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, document.RootElement.GetProperty("count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpUsingStaticImplementedInterfaceNestedTypeStaysSuppressed() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_using_static_interface_nested_type_suppressed"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Defs.cs", "csharp", + """ + namespace Probe; + + public enum Color + { + Red + } + + public interface IBase + { + public class Red {} + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Use.cs", "csharp", + """ + using static Probe.Color; + + namespace Probe; + + class Derived : IBase + { + bool Match(object value) => value is Red; + + void ProbeType() + { + _ = typeof(Red); + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, document.RootElement.GetProperty("count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactCountJson_CSharpUsingStaticImplementedInterfaceNestedTypeStaysSuppressed() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_using_static_interface_nested_type_count_suppressed"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Defs.cs", "csharp", + """ + namespace Probe; + + public enum Color + { + Red + } + + public interface IBase + { + public class Red {} + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Use.cs", "csharp", + """ + using static Probe.Color; + + namespace Probe; + + class Derived : IBase + { + bool Match(object value) => value is Red; + + void ProbeType() + { + _ = typeof(Red); + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name", "--count"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Equal(0, json.GetProperty("files").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpUsingStaticInheritedProtectedNestedTypeViaTypeAliasPatternStaysVisible() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_using_static_inherited_protected_nested_type_type_alias_visible"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Defs.cs", "csharp", + """ + namespace BaseNs; + + public class Base + { + protected class Red {} + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Use.cs", "csharp", + """ + using BaseAlias = BaseNs.Base; + using static Probe.Color; + + namespace Probe; + + public enum Color + { + Red + } + + class Derived : BaseAlias + { + bool Match(object value) => value is Red; + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + var rows = ParseJsonLines(stdout).Select(doc => doc.RootElement).ToList(); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Single(rows); + Assert.Contains("value is Red", rows[0].GetProperty("context").GetString(), StringComparison.Ordinal); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactCountJson_CSharpUsingStaticInheritedProtectedNestedTypeViaTypeAliasPatternStaysVisible() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_using_static_inherited_protected_nested_type_type_alias_count_visible"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Defs.cs", "csharp", + """ + namespace BaseNs; + + public class Base + { + protected class Red {} + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Use.cs", "csharp", + """ + using BaseAlias = BaseNs.Base; + using static Probe.Color; + + namespace Probe; + + public enum Color + { + Red + } + + class Derived : BaseAlias + { + bool Match(object value) => value is Red; + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name", "--count"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(1, json.GetProperty("count").GetInt32()); + Assert.Equal(1, json.GetProperty("files").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpUsingStaticInheritedProtectedNestedTypeViaNamespaceAliasPatternStaysVisible() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_using_static_inherited_protected_nested_type_namespace_alias_visible"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Defs.cs", "csharp", + """ + namespace BaseNs; + + public class Base + { + protected class Red {} + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Use.cs", "csharp", + """ + using NsAlias = BaseNs; + using static Probe.Color; + + namespace Probe; + + public enum Color + { + Red + } + + class Derived : NsAlias.Base + { + bool Match(object value) => value is Red; + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + var rows = ParseJsonLines(stdout).Select(doc => doc.RootElement).ToList(); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Single(rows); + Assert.Contains("value is Red", rows[0].GetProperty("context").GetString(), StringComparison.Ordinal); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactCountJson_CSharpUsingStaticInheritedProtectedNestedTypeViaNamespaceAliasPatternStaysVisible() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_using_static_inherited_protected_nested_type_namespace_alias_count_visible"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Defs.cs", "csharp", + """ + namespace BaseNs; + + public class Base + { + protected class Red {} + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Use.cs", "csharp", + """ + using NsAlias = BaseNs; + using static Probe.Color; + + namespace Probe; + + public enum Color + { + Red + } + + class Derived : NsAlias.Base + { + bool Match(object value) => value is Red; + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name", "--count"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(1, json.GetProperty("count").GetInt32()); + Assert.Equal(1, json.GetProperty("files").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpUsingStaticInheritedNestedTypeViaConstructedGenericTypeAliasPatternStaysVisible() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_using_static_inherited_nested_type_generic_type_alias_visible"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Repro.cs", "csharp", + """ + using static Probe.Color; + using AliasBase = Probe.Base; + + namespace Probe; + + enum Color { Red } + + class Base + { + public class Red {} + } + + class Derived : AliasBase + { + bool Match(object value) => value is Red; + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + var rows = ParseJsonLines(stdout).Select(doc => doc.RootElement).ToList(); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Single(rows); + Assert.Contains("value is Red", rows[0].GetProperty("context").GetString(), StringComparison.Ordinal); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactCountJson_CSharpUsingStaticInheritedNestedTypeViaConstructedGenericTypeAliasPatternStaysVisible() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_using_static_inherited_nested_type_generic_type_alias_count_visible"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Repro.cs", "csharp", + """ + using static Probe.Color; + using AliasBase = Probe.Base; + + namespace Probe; + + enum Color { Red } + + class Base + { + public class Red {} + } + + class Derived : AliasBase + { + bool Match(object value) => value is Red; + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name", "--count"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(1, json.GetProperty("count").GetInt32()); + Assert.Equal(1, json.GetProperty("files").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpUsingStaticInheritedNestedTypeViaGlobalConstructedGenericTypeAliasPatternStaysVisible() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_using_static_inherited_nested_type_global_generic_type_alias_visible"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/GlobalUsings.cs", "csharp", + """ + global using AliasBase = Probe.Base; + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Repro.cs", "csharp", + """ + using static Probe.Color; + + namespace Probe; + + enum Color { Red } + + class Base + { + public class Red {} + } + + class Derived : AliasBase + { + bool Match(object value) => value is Red; + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + var rows = ParseJsonLines(stdout).Select(doc => doc.RootElement).ToList(); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Single(rows); + Assert.Contains("value is Red", rows[0].GetProperty("context").GetString(), StringComparison.Ordinal); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactCountJson_CSharpUsingStaticInheritedNestedTypeViaGlobalConstructedGenericTypeAliasPatternStaysVisible() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_using_static_inherited_nested_type_global_generic_type_alias_count_visible"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/GlobalUsings.cs", "csharp", + """ + global using AliasBase = Probe.Base; + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Repro.cs", "csharp", + """ + using static Probe.Color; + + namespace Probe; + + enum Color { Red } + + class Base + { + public class Red {} + } + + class Derived : AliasBase + { + bool Match(object value) => value is Red; + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name", "--count"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(1, json.GetProperty("count").GetInt32()); + Assert.Equal(1, json.GetProperty("files").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpUsingStaticMultilineLogicalConstantPatternStaysSuppressed() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_using_static_multiline_logical_constant_pattern_suppressed"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Defs.cs", "csharp", + """ + namespace Probe; + + public enum Color + { + Red, + Blue + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Use.cs", "csharp", + """ + using static Probe.Color; + + namespace Probe; + + class Demo + { + bool Match(object value) => value is + Red or Blue; + } + """); + MarkGraphAndFoldReady(dbPath); + + foreach (var symbolName in new[] { "Red", "Blue" }) + { + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + [symbolName, "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, document.RootElement.GetProperty("count").GetInt32()); + } + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactCountJson_CSharpUsingStaticMultilineLogicalConstantPatternStaysSuppressed() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_using_static_multiline_logical_constant_pattern_count_suppressed"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Defs.cs", "csharp", + """ + namespace Probe; + + public enum Color + { + Red, + Blue + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Use.cs", "csharp", + """ + using static Probe.Color; + + namespace Probe; + + class Demo + { + bool Match(object value) => value is + Red or Blue; + } + """); + MarkGraphAndFoldReady(dbPath); + + foreach (var symbolName in new[] { "Red", "Blue" }) + { + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + [symbolName, "--db", dbPath, "--json", "--lang", "csharp", "--exact-name", "--count"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Equal(0, json.GetProperty("files").GetInt32()); + } + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpUsingStaticLongMultilineCaseConstantPatternStaysSuppressed() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_using_static_long_multiline_case_constant_pattern_suppressed"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Defs.cs", "csharp", + """ + namespace Probe; + + public enum Color + { + Red + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Use.cs", "csharp", + """ + using static Probe.Color; + + namespace Probe; + + class Demo + { + void Run(object value) + { + switch (value) + { + case + Red + or + Red + or + Red + or + Red: + break; + } + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactCountJson_CSharpUsingStaticLongMultilineCaseConstantPatternStaysSuppressed() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_using_static_long_multiline_case_constant_pattern_count_suppressed"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Defs.cs", "csharp", + """ + namespace Probe; + + public enum Color + { + Red + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Use.cs", "csharp", + """ + using static Probe.Color; + + namespace Probe; + + class Demo + { + void Run(object value) + { + switch (value) + { + case + Red + or + Red + or + Red + or + Red: + break; + } + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name", "--count"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Equal(0, json.GetProperty("files").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpGlobalQualifiedUsingAliasNameDoesNotCreateReference() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_global_alias_name_invalid"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public enum Color + { + Red + } + + using Color = Demo.Color; + + class C + { + void M() + { + _ = global::Color.Red; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactCountJson_PathScopeDoesNotInheritOutOfScopeEnumMemberMetadata() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_references_scoped_js"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "web/app.js", "javascript", + """ + function Ready() { + } + + Ready(); + """); + TestProjectHelper.InsertIndexedFile(dbPath, "cs/status.cs", "csharp", + """ + public enum Status { Ready } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "javascript", "--exact-name", "--path", "web/", "--count"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(1, json.GetProperty("count").GetInt32()); + Assert.Equal(1, json.GetProperty("files").GetInt32()); + Assert.False(json.TryGetProperty("graph_degraded", out _)); + Assert.False(json.TryGetProperty("unsupported_symbol_kind", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunImpact_ClassSymbolJsonReturnsHeuristicFileDependencyHints() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_impact_class_fallback"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/FolderDiffService.cs", "csharp", + """ + public class FolderDiffService + { + public void ExecuteFolderDiffAsync() { } + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/App.cs", "csharp", + """ + public class App + { + public void Run(FolderDiffService service) + { + service.ExecuteFolderDiffAsync(); + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( + ["FolderDiffService", "--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("file_dependency_hints", json.GetProperty("impact_mode").GetString()); + Assert.True(json.GetProperty("heuristic").GetBoolean()); + Assert.Equal(1, json.GetProperty("count").GetInt32()); + Assert.Equal(0, json.GetProperty("confirmed_count").GetInt32()); + Assert.Equal(0, json.GetProperty("confirmed_file_count").GetInt32()); + Assert.Equal(1, json.GetProperty("hint_count").GetInt32()); + Assert.Equal(1, json.GetProperty("hint_file_count").GetInt32()); + Assert.False(json.GetProperty("has_multiple_definitions").GetBoolean()); + Assert.True(json.GetProperty("has_class_like_definitions").GetBoolean()); + Assert.Equal("src/App.cs", json.GetProperty("file_impacts")[0].GetProperty("source_path").GetString()); + Assert.Equal("src/FolderDiffService.cs", json.GetProperty("file_impacts")[0].GetProperty("target_path").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunImpact_ClassAndNamespaceWithSameNameJsonStillReturnsHeuristicHints() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_impact_namespace_sibling"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/FooService.cs", "csharp", + """ + namespace FooService; + + public class FooService + { + public void Run() { } + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/App.cs", "csharp", + """ + public class App + { + public void Boot(FooService service) + { + service.Run(); + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( + ["FooService", "--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("file_dependency_hints", json.GetProperty("impact_mode").GetString()); + Assert.True(json.GetProperty("heuristic").GetBoolean()); + Assert.True(json.GetProperty("has_multiple_definitions").GetBoolean()); + Assert.False(json.GetProperty("has_multiple_definition_files").GetBoolean()); + Assert.Equal(2, json.GetProperty("definition_count").GetInt32()); + Assert.Equal(1, json.GetProperty("count").GetInt32()); + Assert.Equal(0, json.GetProperty("confirmed_count").GetInt32()); + Assert.Equal(1, json.GetProperty("hint_count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunImpact_HeuristicHintsCountOnlyJsonUsesVisibleResultCount() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_impact_hint_count_only"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/FolderDiffService.cs", "csharp", + """ + public class FolderDiffService + { + public void ExecuteFolderDiffAsync() { } + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/App.cs", "csharp", + """ + public class App + { + public void Run(FolderDiffService service) + { + service.ExecuteFolderDiffAsync(); + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( + ["FolderDiffService", "--db", dbPath, "--json", "--count"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("file_dependency_hints", json.GetProperty("impact_mode").GetString()); + Assert.Equal(1, json.GetProperty("count").GetInt32()); + Assert.Equal(1, json.GetProperty("file_count").GetInt32()); + Assert.Equal(0, json.GetProperty("confirmed_count").GetInt32()); + Assert.Equal(0, json.GetProperty("confirmed_file_count").GetInt32()); + Assert.Equal(1, json.GetProperty("hint_count").GetInt32()); + Assert.Equal(1, json.GetProperty("hint_file_count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunImpact_UserLimitTruncation_EmitsTruncatedReasonInJson() + { + // #1533: when impact truncates because the user-supplied --limit was reached, + // the JSON payload exposes truncated_reason="user_limit" so AI/MCP consumers + // can offer the correct retry advice (raise --limit). + // #1533: --limit による打ち切り時、JSON に truncated_reason="user_limit" を含めて + // AI/MCP クライアントが「--limit を上げる」適切な再試行案内を出せるようにする。 + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_impact_user_limit_reason"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/lib.py", "python", + """ + def target(): + return 0 + """); + for (int i = 0; i < 6; i++) + { + TestProjectHelper.InsertIndexedFile(dbPath, $"src/caller_{i:D2}.py", "python", + $$""" + def caller_{{i:D2}}(): + return target() + """); + } + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( + ["target", "--db", dbPath, "--json", "--limit", "2"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.True(json.GetProperty("truncated").GetBoolean()); + Assert.Equal("user_limit", json.GetProperty("truncated_reason").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunImpact_NotTruncated_OmitsTruncatedReasonFromJson() + { + // #1533: when truncated=false the truncated_reason field is omitted, so + // schema consumers can rely on its presence to mean an actionable truncation. + // #1533: truncated=false のときは truncated_reason フィールドを省略し、 + // スキーマ利用側が「フィールドが存在する=対応すべき打ち切り」と判断できるようにする。 + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_impact_no_truncate_reason"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/lib.py", "python", + """ + def target(): + return 0 + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/single_caller.py", "python", + """ + def caller(): + return target() + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( + ["target", "--db", dbPath, "--json", "--limit", "10"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.False(json.GetProperty("truncated").GetBoolean()); + Assert.False(json.TryGetProperty("truncated_reason", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunImpact_CycleJsonEmitsTerminationFields() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_impact_cycle_reason"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/impact_cycle.cs", "csharp", + """ + public static class ImpactCycle + { + public static void A() { B(); } + public static void B() { C(); } + public static void C() { A(); } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( + ["C", "--db", dbPath, "--json", "--limit", "20"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.False(json.GetProperty("truncated").GetBoolean()); + Assert.Equal("cycle_detected", json.GetProperty("termination_reason").GetString()); + Assert.True(json.GetProperty("cycle_detected").GetBoolean()); + var cycle = Assert.Single(json.GetProperty("cycles").EnumerateArray()); + Assert.Equal(new[] { "A", "B", "C" }, cycle.GetProperty("members").EnumerateArray().Select(member => member.GetString()).ToArray()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunImpact_FoldEquivalentClassDefinitionsJsonReportAmbiguity() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_impact_fold_siblings"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/FooService.cs", "csharp", + """ + public class FooService + { + public void Run() { } + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/FullwidthFooService.cs", "csharp", + """ + public class FooService + { + public void Run() { } + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/App.cs", "csharp", + """ + public class App + { + public void Boot(FooService service) + { + service.Run(); + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( + ["FooService", "--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("none", json.GetProperty("impact_mode").GetString()); + Assert.True(json.GetProperty("has_multiple_definitions").GetBoolean()); + Assert.Equal(2, json.GetProperty("definition_count").GetInt32()); + Assert.Equal("multiple_definition_files", json.GetProperty("zero_result_reason").GetString()); + Assert.Equal(0, json.GetProperty("hint_count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunImpact_PartialClassJsonReturnsResolutionHintPayload() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_impact_partial_hint"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Worker.Part1.cs", "csharp", + """ + public partial class Worker + { + public void Start() { } + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Worker.Part2.cs", "csharp", + """ + public partial class Worker + { + public void Stop() { } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( + ["Worker", "--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("none", json.GetProperty("impact_mode").GetString()); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.True(json.GetProperty("has_multiple_definitions").GetBoolean()); + Assert.True(json.GetProperty("has_multiple_definition_files").GetBoolean()); + Assert.Equal("multiple_definition_files", json.GetProperty("zero_result_reason").GetString()); + Assert.Contains("deps --path --reverse", json.GetProperty("suggestion").GetString()); + Assert.Equal(2, json.GetProperty("definition_file_count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunImpact_ClassCollisionWithoutTypeEvidenceReturnsNoHints() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_impact_collision"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/FooService.cs", "csharp", + """ + public class FooService + { + public void Run() { } + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/BarService.cs", "csharp", + """ + public class BarService + { + public void Run() { } + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/App.cs", "csharp", + """ + public class App + { + public void Boot(BarService service) + { + service.Run(); + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( + ["FooService", "--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("none", json.GetProperty("impact_mode").GetString()); + Assert.False(json.GetProperty("heuristic").GetBoolean()); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Equal(0, json.GetProperty("hint_count").GetInt32()); + Assert.Equal(0, json.GetProperty("file_impacts").GetArrayLength()); + Assert.Equal("class_symbol_no_symbol_callers", json.GetProperty("zero_result_reason").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunImpact_CommentOnlyTypeMentionDoesNotProduceHeuristicHints() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_impact_comment_only_type_name"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/FooService.cs", "csharp", + """ + public class FooService + { + public void Run() { } + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/OtherService.cs", "csharp", + """ + public class OtherService + { + public void Run() { } + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/App.cs", "csharp", + """ + public class App + { + public void Boot(OtherService service) + { + service.Run(); // TODO: maybe replace with FooService later + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( + ["FooService", "--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("none", json.GetProperty("impact_mode").GetString()); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Equal(0, json.GetProperty("hint_count").GetInt32()); + Assert.Equal("class_symbol_no_symbol_callers", json.GetProperty("zero_result_reason").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunImpact_StringLiteralTypeMentionDoesNotProduceHeuristicHints() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_impact_string_only_type_name"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/FooService.cs", "csharp", + """ + public class FooService + { + public void Execute() { } + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Worker.cs", "csharp", + """ + public class Worker + { + public void Execute() { } + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/App.cs", "csharp", + """ + public class App + { + public void Boot(Worker worker) + { + var label = "FooService"; + worker.Execute(); + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( + ["FooService", "--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("none", json.GetProperty("impact_mode").GetString()); + Assert.Equal(0, json.GetProperty("hint_count").GetInt32()); + Assert.Equal("class_symbol_no_symbol_callers", json.GetProperty("zero_result_reason").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunImpact_NamespaceJsonDoesNotFallbackToFileDependencies() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_impact_namespace"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Services.cs", "csharp", + """ + namespace Acme; + + public class FooService + { + public void Run() { } + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/App.cs", "csharp", + """ + namespace Acme; + + public class App + { + public void Boot(FooService service) + { + service.Run(); + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( + ["Acme", "--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("none", json.GetProperty("impact_mode").GetString()); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Equal("non_callable_symbol_kind", json.GetProperty("zero_result_reason").GetString()); + Assert.Equal(0, json.GetProperty("file_impacts").GetArrayLength()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunImpact_ImportOnlyQueryJsonReportsNonCallableSymbolKind() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_impact_import_only"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.py", "python", + """ + import requests + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( + ["requests", "--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("none", json.GetProperty("impact_mode").GetString()); + Assert.Equal(1, json.GetProperty("definition_count").GetInt32()); + Assert.Equal("non_callable_symbol_kind", json.GetProperty("zero_result_reason").GetString()); + Assert.Contains("definition ", json.GetProperty("suggestion").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunImpact_ZeroResultJsonPayloadRemainsStableAcrossRepeatedTempProjects() + { + for (var iteration = 0; iteration < 10; iteration++) + { + RunImpactPartialClassZeroResultIteration(iteration); + RunImpactImportOnlyZeroResultIteration(iteration); + } + } + + [Fact] + public void RunImpact_UnicodeTypeEvidenceStillReturnsHeuristicHints() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_impact_unicode_type_evidence"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/FooService.cs", "csharp", + """ + public class FooService + { + public void Run() { } + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/App.cs", "csharp", + """ + public class App + { + public void Boot(FooService service) + { + service.Run(); + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( + ["FooService", "--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("file_dependency_hints", json.GetProperty("impact_mode").GetString()); + Assert.Equal(1, json.GetProperty("hint_count").GetInt32()); + Assert.Equal("file_heuristic", json.GetProperty("file_impacts")[0].GetProperty("result_kind").GetString()); + Assert.Equal("src/App.cs", json.GetProperty("file_impacts")[0].GetProperty("source_path").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunImpact_ExcludeTestsJsonIgnoresOutOfScopeDuplicateDefinitions() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_impact_exclude_tests_duplicate"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/FooService.cs", "csharp", + """ + public class FooService + { + public void Run() { } + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "tests/FooServiceTests.cs", "csharp", + """ + public class FooService + { + public void Run() { } + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/App.cs", "csharp", + """ + public class App + { + public void Boot(FooService service) + { + service.Run(); + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( + ["FooService", "--db", dbPath, "--exclude-tests", "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("file_dependency_hints", json.GetProperty("impact_mode").GetString()); + Assert.True(json.GetProperty("heuristic").GetBoolean()); + Assert.False(json.GetProperty("has_multiple_definitions").GetBoolean()); + Assert.False(json.GetProperty("has_multiple_definition_files").GetBoolean()); + Assert.Equal(1, json.GetProperty("definition_file_count").GetInt32()); + Assert.Equal(1, json.GetProperty("hint_count").GetInt32()); + Assert.Equal(1, json.GetProperty("count").GetInt32()); + Assert.Equal("src/FooService.cs", json.GetProperty("definitions")[0].GetProperty("path").GetString()); + Assert.Equal("src/App.cs", json.GetProperty("file_impacts")[0].GetProperty("source_path").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunImpact_UnsupportedLanguageDuplicateDoesNotTriggerMultipleDefinitions() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_impact_unsupported_duplicate"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/FooService.cs", "csharp", + """ + public class FooService + { + public void Run() { } + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/tools.txt", "text", + """ + FooService() { + : + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/App.cs", "csharp", + """ + public class App + { + public void Boot(FooService service) + { + service.Run(); + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( + ["FooService", "--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("file_dependency_hints", json.GetProperty("impact_mode").GetString()); + Assert.True(json.GetProperty("heuristic").GetBoolean()); + Assert.False(json.GetProperty("has_multiple_definitions").GetBoolean()); + Assert.False(json.GetProperty("has_multiple_definition_files").GetBoolean()); + Assert.Equal(1, json.GetProperty("definition_file_count").GetInt32()); + Assert.Equal(1, json.GetProperty("count").GetInt32()); + Assert.Equal("src/FooService.cs", json.GetProperty("definitions")[0].GetProperty("path").GetString()); + Assert.Equal("src/App.cs", json.GetProperty("file_impacts")[0].GetProperty("source_path").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunImpact_ExactDefinitionResolutionSkipsUnsupportedMatchesBeforeLimit() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_impact_unsupported_overflow"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + for (int i = 0; i < 60; i++) + { + TestProjectHelper.InsertIndexedFile(dbPath, $"scripts/Foo{i:D2}.txt", "text", + """ + Foo() { + : + } + """); + } + TestProjectHelper.InsertIndexedFile(dbPath, "src/Foo.cs", "csharp", + """ + public class Foo + { + public void Run() { } + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/App.cs", "csharp", + """ + public class App + { + public void Boot(Foo service) + { + service.Run(); + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( + ["Foo", "--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("file_dependency_hints", json.GetProperty("impact_mode").GetString()); + Assert.Equal(1, json.GetProperty("definition_count").GetInt32()); + Assert.Equal("src/Foo.cs", json.GetProperty("definitions")[0].GetProperty("path").GetString()); + Assert.Equal("src/App.cs", json.GetProperty("file_impacts")[0].GetProperty("source_path").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunImpact_SubstringTypeEvidenceDoesNotProduceHeuristicHints() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_impact_substring_type_evidence"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Foo.cs", "csharp", + """ + public class Foo + { + public void Run() { } + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/FooService.cs", "csharp", + """ + public class FooService + { + public void Run() { } + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/App.cs", "csharp", + """ + public class App + { + public void Handle(FooService service) + { + service.Run(); + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( + ["Foo", "--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("none", json.GetProperty("impact_mode").GetString()); + Assert.Equal(0, json.GetProperty("hint_count").GetInt32()); + Assert.Equal("class_symbol_no_symbol_callers", json.GetProperty("zero_result_reason").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunImpact_DuplicateDefinitionsInOneFileJsonReportsAmbiguity() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_impact_same_file_duplicate"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Services.cs", "csharp", + """ + namespace A + { + public class FooService + { + public void Run() { } + } + } + + namespace B + { + public class FooService + { + public void Run() { } + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( + ["FooService", "--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("none", json.GetProperty("impact_mode").GetString()); + Assert.Equal(2, json.GetProperty("definition_count").GetInt32()); + Assert.Equal(1, json.GetProperty("definition_file_count").GetInt32()); + Assert.True(json.GetProperty("has_multiple_definitions").GetBoolean()); + Assert.False(json.GetProperty("has_multiple_definition_files").GetBoolean()); + Assert.Equal("multiple_definitions", json.GetProperty("zero_result_reason").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunImpact_DuplicateDefinitionsInOneFileHumanOutputMentionsDefinitionAndFileCounts() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_impact_same_file_duplicate_human"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Services.cs", "csharp", + """ + namespace A + { + public class FooService + { + public void Run() { } + } + } + + namespace B + { + public class FooService + { + public void Run() { } + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( + ["FooService", "--db", dbPath], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.DoesNotContain("file_dependency_hints", stdout); + Assert.Contains("2 definitions across 1 file", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunImpact_HeuristicHintsJsonSetTruncatedAndReturnSuccess() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_impact_hint_truncated"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/FolderDiffService.cs", "csharp", + """ + public class FolderDiffService + { + public void ExecuteFolderDiffAsync() { } + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/App1.cs", "csharp", + """ + public class App1 + { + public void Boot(FolderDiffService service) + { + service.ExecuteFolderDiffAsync(); + } + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/App2.cs", "csharp", + """ + public class App2 + { + public void Boot(FolderDiffService service) + { + service.ExecuteFolderDiffAsync(); + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( + ["FolderDiffService", "--db", dbPath, "--limit", "1", "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("file_dependency_hints", json.GetProperty("impact_mode").GetString()); + Assert.True(json.GetProperty("truncated").GetBoolean()); + Assert.Equal(1, json.GetProperty("count").GetInt32()); + Assert.Equal(1, json.GetProperty("hint_count").GetInt32()); + Assert.Equal(1, json.GetProperty("file_impacts").GetArrayLength()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunImpact_HeuristicHintsJsonKeepActualReferenceCount() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_impact_hint_refcount"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/FolderDiffService.cs", "csharp", + """ + public class FolderDiffService + { + public void ExecuteFolderDiffAsync() { } + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/App.cs", "csharp", + """ + public class App + { + public void Boot(FolderDiffService service) + { + service.ExecuteFolderDiffAsync(); + service.ExecuteFolderDiffAsync(); + service.ExecuteFolderDiffAsync(); + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( + ["FolderDiffService", "--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("file_dependency_hints", json.GetProperty("impact_mode").GetString()); + Assert.Equal(1, json.GetProperty("count").GetInt32()); + Assert.Equal(4, json.GetProperty("file_impacts")[0].GetProperty("reference_count").GetInt32()); + Assert.Equal("ExecuteFolderDiffAsync,FolderDiffService", json.GetProperty("file_impacts")[0].GetProperty("symbols").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunImpact_UnresolvedExternalCallWithoutTypeEvidenceReturnsNoHints() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_impact_unresolved_external"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/FolderDiffService.cs", "csharp", + """ + public class FolderDiffService + { + public void ExecuteFolderDiffAsync() { } + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/ExternalConsumer.cs", "csharp", + """ + public class ExternalConsumer + { + public void Boot() + { + ExecuteFolderDiffAsync(); + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( + ["FolderDiffService", "--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("none", json.GetProperty("impact_mode").GetString()); + Assert.Equal(0, json.GetProperty("hint_count").GetInt32()); + Assert.Equal("class_symbol_no_symbol_callers", json.GetProperty("zero_result_reason").GetString()); + Assert.Equal(0, json.GetProperty("file_impacts").GetArrayLength()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunImpact_CSharpVerbatimQueryMissKeepsOriginalInputInJsonAndHumanOutput() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_impact_verbatim_miss"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Verbatim.cs", "csharp", + """ + public class @class + { + } + """); + MarkGraphAndFoldReady(dbPath); + + var (jsonExitCode, jsonStdout, jsonStderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( + ["@missing", "--db", dbPath, "--lang", "csharp", "--json"], + _jsonOptions)); + + using var jsonDocument = ParseJsonOutput(jsonStdout); + var json = jsonDocument.RootElement; + + Assert.Equal(CommandExitCodes.Success, jsonExitCode); + Assert.Equal(string.Empty, jsonStderr); + Assert.Equal("@missing", json.GetProperty("query").GetString()); + Assert.Equal("@missing", json.GetProperty("resolved_name").GetString()); + Assert.Equal("no_matching_definition", json.GetProperty("zero_result_reason").GetString()); + + var (humanExitCode, humanStdout, humanStderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( + ["@missing", "--db", dbPath, "--lang", "csharp"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, humanExitCode); + Assert.Equal(string.Empty, humanStdout); + Assert.Contains("No impact found for '@missing'.", humanStderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Theory] + [InlineData("references")] + [InlineData("callers")] + [InlineData("callees")] + public void GraphCommands_ExactZeroJson_RespectRequestedLimitAndCapSamples(string command) + { + var projectRoot = TestProjectHelper.CreateTempProject($"cdidx_query_runner_{command}_exact_zero_limit"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + SeedGraphExactZeroFixture(dbPath, command); + + var (exitCode, stdout, stderr) = CaptureConsole(() => RunGraphCommand(command, + GetExactZeroArgs(command, dbPath, limit: 6, queryOverride: null, countOnly: true), + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Equal(6, json.GetProperty("exact_zero_hint").GetProperty("relaxed_count").GetInt32()); + Assert.Equal(5, json.GetProperty("exact_zero_hint").GetProperty("sample_names").GetArrayLength()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Theory] + [InlineData("references")] + [InlineData("callers")] + [InlineData("callees")] + public void GraphCommands_ExactZeroJson_OmitHintWhenRelaxedQueryStillReturnsZero(string command) + { + var projectRoot = TestProjectHelper.CreateTempProject($"cdidx_query_runner_{command}_exact_zero_miss"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + SeedGraphExactZeroFixture(dbPath, command); + + var (exitCode, stdout, stderr) = CaptureConsole(() => RunGraphCommand(command, + GetExactZeroArgs(command, dbPath, limit: 6, queryOverride: "DefinitelyMissing", countOnly: true), + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.False(json.TryGetProperty("exact_zero_hint", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactOnReadOnlyLegacyDb_WarnsAboutMissingIndex() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_exact_warn"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/session.py", "python", "def login(user, password):\n return Run(user)\n"); + DropGraphExactFallbackIndexes(dbPath); + + var readOnlyUri = new Uri(dbPath).AbsoluteUri + "?immutable=1"; + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Run", "--db", readOnlyUri, "--exact"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Contains("src/session.py:2:12", stdout); + Assert.Contains("WARN: --exact graph query ran without the supporting index", stderr); + Assert.Contains("idx_symbol_refs_name_nocase", stderr); + Assert.Contains("re-index with `cdidx index `", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunCallees_ExactJsonOnReadOnlyLegacyDb_IncludesExactIndexSignal() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_exact_json"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/session.py", "python", "def login(user, password):\n return Run(user)\n"); + DropGraphExactFallbackIndexes(dbPath); + + var readOnlyUri = new Uri(dbPath).AbsoluteUri + "?immutable=1"; + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunCallees( + ["login", "--db", readOnlyUri, "--exact", "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("Run", json.GetProperty("callee_name").GetString()); + Assert.False(json.GetProperty("exact_index_available").GetBoolean()); + Assert.Contains("idx_symbol_refs_container_nocase", json.GetProperty("degraded_reason").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactZeroHumanOutput_PrintsExactZeroHint() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_refs_exact_zero"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/app.cs", + "csharp", + """ + public class App + { + public void HandleRequest() { } + public void HandleRequestAsync() { HandleRequest(); } + } + """); + using (var db = new DbContext(dbPath)) + { + var writer = new DbWriter(db.Connection); + writer.MarkGraphReady(); + } + + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Handle", "--db", dbPath, "--exact"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Contains("No references found.", stderr); + Assert.Contains("--exact found 0 matches, but substring matching would return 1", stderr); + Assert.Contains("`HandleRequest`", stderr); + Assert.Contains("Drop --exact or use the exact indexed name.", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactWithoutGraphTable_DoesNotClaimSlowButCorrect() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_missing_graph"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Run", "--db", dbPath, "--exact"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.DoesNotContain("Results are correct but may be slow", stderr); + Assert.Contains("symbol_references table missing", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactCountWithoutGraphTable_WarnsCountIsDegraded() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_missing_graph_count"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Run", "--db", dbPath, "--exact", "--count"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal("0", stdout.Trim()); + Assert.DoesNotContain("Results are correct but may be slow", stderr); + Assert.Contains("count result is degraded, not authoritative", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_JavaModuleInfoDirectivesReturnModuleEdges() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_java_module_references"); + try + { + File.WriteAllText( + Path.Combine(projectRoot, "module-info.java"), + """ + module com.example.app { + requires java.base; + requires transitive java.logging; + uses com.example.spi.MyService; + provides com.example.spi.MyService with com.example.impl.DefaultService; + } + """); + + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + + var (javaBaseExitCode, javaBaseStdout, javaBaseStderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["java.base", "--db", dbPath, "--json", "--lang", "java", "--exact-name"], + _jsonOptions)); + var javaBaseRows = ParseJsonLines(javaBaseStdout) + .Select(document => document.RootElement) + .ToList(); + + var (javaLoggingExitCode, javaLoggingStdout, javaLoggingStderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["java.logging", "--db", dbPath, "--json", "--lang", "java", "--exact-name"], + _jsonOptions)); + var javaLoggingRows = ParseJsonLines(javaLoggingStdout) + .Select(document => document.RootElement) + .ToList(); + + var (serviceExitCode, serviceStdout, serviceStderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["com.example.spi.MyService", "--db", dbPath, "--json", "--lang", "java", "--exact-name"], + _jsonOptions)); + var serviceRows = ParseJsonLines(serviceStdout) + .Select(document => document.RootElement) + .ToList(); + + var (implementationExitCode, implementationStdout, implementationStderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["com.example.impl.DefaultService", "--db", dbPath, "--json", "--lang", "java", "--exact-name"], + _jsonOptions)); + var implementationRows = ParseJsonLines(implementationStdout) + .Select(document => document.RootElement) + .ToList(); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + + Assert.Equal(CommandExitCodes.Success, javaBaseExitCode); + Assert.Equal(string.Empty, javaBaseStderr); + var javaBaseRow = Assert.Single(javaBaseRows); + Assert.Equal("type_reference", javaBaseRow.GetProperty("reference_kind").GetString()); + Assert.Equal("com.example.app", javaBaseRow.GetProperty("container_name").GetString()); + + Assert.Equal(CommandExitCodes.Success, javaLoggingExitCode); + Assert.Equal(string.Empty, javaLoggingStderr); + var javaLoggingRow = Assert.Single(javaLoggingRows); + Assert.Equal("type_reference", javaLoggingRow.GetProperty("reference_kind").GetString()); + Assert.Equal("com.example.app", javaLoggingRow.GetProperty("container_name").GetString()); + + Assert.Equal(CommandExitCodes.Success, serviceExitCode); + Assert.Equal(string.Empty, serviceStderr); + Assert.Equal(2, serviceRows.Count); + Assert.All(serviceRows, row => + { + Assert.Equal("type_reference", row.GetProperty("reference_kind").GetString()); + Assert.Equal("com.example.app", row.GetProperty("container_name").GetString()); + }); + + Assert.Equal(CommandExitCodes.Success, implementationExitCode); + Assert.Equal(string.Empty, implementationStderr); + var implementationRow = Assert.Single(implementationRows); + Assert.Equal("type_reference", implementationRow.GetProperty("reference_kind").GetString()); + Assert.Equal("com.example.app", implementationRow.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_Json_CSharpNestedRawStringInsideInterpolationDoesNotCreatePhantomReference() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_references_csharp_nested_raw_fixture"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "app.cs"), + """" + public class App + { + private int Run() => 1; + private string Id(string value) => value; + + public int Render() + { + return $""" + value = {Id(""" + Execute(); + public class Phantom + { + public void Go() { } + } + """) + Run()} + """.Length; + } + } + """"); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Execute", "--db", dbPath, "--json", "--exact-name", "--lang", "csharp"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Equal(0, json.GetProperty("references").GetArrayLength()); + Assert.True(json.GetProperty("exact_index_available").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpUsingStaticSingleLineConstantPattern_StaysSuppressed() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_using_static_single_line_constant_pattern_suppressed"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Defs.cs", "csharp", + """ + namespace Probe; + + public enum Color + { + Red, + Blue + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Use.cs", "csharp", + """ + using static Probe.Color; + + namespace Probe; + + class Demo + { + bool Match(object value) => value is Red or Blue; + } + """); + MarkGraphAndFoldReady(dbPath); + + foreach (var symbolName in new[] { "Red", "Blue" }) + { + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + [symbolName, "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, document.RootElement.GetProperty("count").GetInt32()); + } + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_ExactJson_CSharpUsingStaticMultilineConstantPattern_StaysSuppressed() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_using_static_multiline_constant_pattern_suppressed"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Defs.cs", "csharp", + """ + namespace Probe; + + public enum Color + { + Red, + Blue + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Use.cs", "csharp", + """ + using static Probe.Color; + + namespace Probe; + + class Demo + { + bool Match(object value) => value is + Red or Blue; + } + """); + MarkGraphAndFoldReady(dbPath); + + foreach (var symbolName in new[] { "Red", "Blue" }) + { + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + [symbolName, "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, document.RootElement.GetProperty("count").GetInt32()); + } + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunReferences_FuzzyJson_CSharpUsingStaticConstantPattern_RemainsSearchable() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_using_static_fuzzy_constant_pattern_searchable"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Defs.cs", "csharp", + """ + namespace Probe; + + public enum Color + { + Red, + Blue + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Use.cs", "csharp", + """ + using static Probe.Color; + + namespace Probe; + + class Demo + { + bool Match(object value) => value is Red or Blue; + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Re", "--db", dbPath, "--json", "--lang", "csharp"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("Red", document.RootElement.GetProperty("symbol_name").GetString()); + Assert.Equal("type_reference", document.RootElement.GetProperty("reference_kind").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } +} diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerInspectTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerInspectTests.cs new file mode 100644 index 0000000000..30b4c9933c --- /dev/null +++ b/tests/CodeIndex.Tests/QueryCommandRunnerInspectTests.cs @@ -0,0 +1,4376 @@ +using System.Text.Json; +using CodeIndex.Cli; +using CodeIndex.Database; +using CodeIndex.Models; +using Microsoft.Data.Sqlite; + +namespace CodeIndex.Tests; + +public partial class QueryCommandRunnerTests +{ + [Fact] + public void RunInspect_StrictNotFoundReturnsNotFoundForEmptyAnalysis_Issue1425() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_inspect_strict_not_found"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/auth.cs", + "csharp", + "public class AuthFixture { }\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Missing", "--db", dbPath, "--json", "--strict-not-found"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.NotFound, exitCode); + Assert.Equal(string.Empty, stderr); + using var document = ParseJsonOutput(stdout); + Assert.Equal("Missing", document.RootElement.GetProperty("query").GetString()); + Assert.Empty(document.RootElement.GetProperty("definitions").EnumerateArray()); + Assert.Empty(document.RootElement.GetProperty("references").EnumerateArray()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_AllowsPathValueThatLooksLikePreviewOption() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_inspect_preview_like_path_value"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["target", "--db", dbPath, "--path=--max-line-width", "--json"], + _jsonOptions)); + + Assert.NotEqual(CommandExitCodes.UsageError, exitCode); + Assert.DoesNotContain("is not supported", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunOutline_IndentsByContainerDepth() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_outline_depth"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/deep.cs", + "csharp", + """ + namespace OuterNs + { + namespace InnerNs + { + public class OuterClass + { + public class NestedClass + { + public class DeeplyNested + { + public void Method() { } + } + } + } + } + } + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunOutline( + ["src/deep.cs", "--db", dbPath], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + + var lines = stdout.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + var outer = lines.Single(line => line.Contains("namespace OuterNs", StringComparison.Ordinal)); + var inner = lines.Single(line => line.Contains("namespace InnerNs", StringComparison.Ordinal)); + var outerClass = lines.Single(line => line.Contains("public class OuterClass", StringComparison.Ordinal)); + var nestedClass = lines.Single(line => line.Contains("public class NestedClass", StringComparison.Ordinal)); + var deeplyNested = lines.Single(line => line.Contains("public class DeeplyNested", StringComparison.Ordinal)); + var method = lines.Single(line => line.Contains("public void Method()", StringComparison.Ordinal)); + + var outerIndex = outer.IndexOf("namespace OuterNs", StringComparison.Ordinal); + Assert.Equal(4, inner.IndexOf("namespace InnerNs", StringComparison.Ordinal) - outerIndex); + Assert.Equal(4, outerClass.IndexOf("public class OuterClass", StringComparison.Ordinal) - inner.IndexOf("namespace InnerNs", StringComparison.Ordinal)); + Assert.Equal(4, nestedClass.IndexOf("public class NestedClass", StringComparison.Ordinal) - outerClass.IndexOf("public class OuterClass", StringComparison.Ordinal)); + Assert.Equal(4, deeplyNested.IndexOf("public class DeeplyNested", StringComparison.Ordinal) - nestedClass.IndexOf("public class NestedClass", StringComparison.Ordinal)); + Assert.Equal(4, method.IndexOf("public void Method()", StringComparison.Ordinal) - deeplyNested.IndexOf("public class DeeplyNested", StringComparison.Ordinal)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunOutline_UsesNestedSymbolDepthInHumanOutput() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_outline_depth"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/deep.cs", + "csharp", + """ + namespace OuterNs + { + namespace InnerNs + { + public class OuterClass + { + public class NestedClass + { + public class DeeplyNested + { + public void Method() { } + } + } + } + } + } + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunOutline( + ["src/deep.cs", "--db", dbPath], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + var outerIndex = stdout.IndexOf("public class OuterClass", StringComparison.Ordinal); + var nestedIndex = stdout.IndexOf("public class NestedClass", StringComparison.Ordinal); + var deepIndex = stdout.IndexOf("public class DeeplyNested", StringComparison.Ordinal); + + Assert.True(outerIndex >= 0); + Assert.True(nestedIndex > outerIndex); + Assert.True(deepIndex > nestedIndex); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_RejectsMissingMaxLineWidthValue() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_inspect_missing_max_line_width"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["target", "--db", dbPath, "--max-line-width", "--json"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + // Missing-value guard short-circuits before TryParsePositiveInt; see + // RunExcerpt_RejectsMissingFocusColumnValue for the matching contract note. + // TryParsePositiveInt より前で値欠如として短絡する。契約の詳細は上記テスト参照。 + Assert.Contains("--max-line-width requires a value", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunOutline_HumanDisplaysCompactOverloadSignatures() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_outline_overload_human"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/worker.cs", + "csharp", + """ + using System.Threading; + + public class Worker + { + public void Process(string input) { } + public void Process(int count, CancellationToken cancellationToken = default) { } + } + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunOutline( + ["src/worker.cs", "--db", dbPath], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Contains("Process(string)", stdout); + Assert.Contains("Process(int, CancellationToken)", stdout); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunOutline_Human_CSharpTopLevelStatementsFile_WritesHelpfulNote() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_outline_csharp_toplevel_human"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/program.cs", + "csharp", + """"" + using System; + using System.Linq; + + var values = new[] { 1, 2, 3 }; + foreach (var value in values) + { + Console.WriteLine(value); + } + + Console.WriteLine(Sum(values)); + + static int Sum(int[] values) + { + var total = 0; + foreach (var value in values) + { + total += value; + } + + return total; + } + + Console.WriteLine("done"); + Console.WriteLine(values.Length); + """""); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunOutline( + ["src/program.cs", "--db", dbPath], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Contains("# src/program.cs", stdout); + Assert.Contains("Note: no type/namespace declarations found; this file likely uses C# top-level statements.", stderr); + Assert.Contains("Outline lists imports and local functions only; the executable body is not indexed as symbols.", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunOutline_Json_CSharpTopLevelStatementsFile_LeavesJsonContractUnchanged() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_outline_csharp_toplevel_json"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/program.cs", + "csharp", + """ + using System; + + Console.WriteLine("boot"); + Console.WriteLine("run"); + Console.WriteLine("more"); + Console.WriteLine("lines"); + Console.WriteLine("to"); + Console.WriteLine("cross"); + Console.WriteLine("the"); + Console.WriteLine("top-level"); + Console.WriteLine("statement"); + Console.WriteLine("threshold"); + Console.WriteLine("without"); + Console.WriteLine("declaring"); + Console.WriteLine("types"); + Console.WriteLine("or"); + Console.WriteLine("namespaces"); + Console.WriteLine("in"); + Console.WriteLine("this"); + Console.WriteLine("file"); + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunOutline( + ["src/program.cs", "--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("src/program.cs", json.GetProperty("path").GetString()); + Assert.Equal("csharp", json.GetProperty("lang").GetString()); + Assert.True(json.TryGetProperty("symbols", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunOutline_Json_AcceptsAbsolutePathWithExplicitDbOutsideProjectRoot() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_outline_absolute_path"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Sample.cs", "csharp", "namespace Demo;\npublic class Svc { }\n"); + var absolutePath = Path.Combine(projectRoot, "src", "Sample.cs"); + + var (relativeExitCode, relativeStdout, relativeStderr) = CaptureConsole(() => QueryCommandRunner.RunOutline( + ["src/Sample.cs", "--db", dbPath, "--json"], + _jsonOptions)); + var (absoluteExitCode, absoluteStdout, absoluteStderr) = CaptureConsole(() => QueryCommandRunner.RunOutline( + [absolutePath, "--db", dbPath, "--json"], + _jsonOptions)); + + using var relativeDocument = ParseJsonOutput(relativeStdout); + using var absoluteDocument = ParseJsonOutput(absoluteStdout); + + Assert.Equal(CommandExitCodes.Success, relativeExitCode); + Assert.Equal(CommandExitCodes.Success, absoluteExitCode); + Assert.Equal(string.Empty, relativeStderr); + Assert.Equal(string.Empty, absoluteStderr); + Assert.Equal("src/Sample.cs", relativeDocument.RootElement.GetProperty("path").GetString()); + Assert.Equal(relativeDocument.RootElement.GetProperty("path").GetString(), absoluteDocument.RootElement.GetProperty("path").GetString()); + Assert.Equal(relativeDocument.RootElement.GetProperty("symbol_count").GetInt32(), absoluteDocument.RootElement.GetProperty("symbol_count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunOutline_Human_CSharpStatementOnlyTopLevelFile_WritesHelpfulNote() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_outline_csharp_statement_only_human"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/program.cs", + "csharp", + """ + using System; + + Console.WriteLine("boot"); + Console.WriteLine("run"); + Console.WriteLine("more"); + Console.WriteLine("lines"); + Console.WriteLine("to"); + Console.WriteLine("cross"); + Console.WriteLine("the"); + Console.WriteLine("top-level"); + Console.WriteLine("statement"); + Console.WriteLine("threshold"); + Console.WriteLine("without"); + Console.WriteLine("declaring"); + Console.WriteLine("types"); + Console.WriteLine("or"); + Console.WriteLine("namespaces"); + Console.WriteLine("in"); + Console.WriteLine("this"); + Console.WriteLine("file"); + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunOutline( + ["src/program.cs", "--db", dbPath], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Contains("# src/program.cs", stdout); + Assert.Contains("Note: no type/namespace declarations found; this file likely uses C# top-level statements.", stderr); + Assert.Contains("Outline lists imports and local functions only; the executable body is not indexed as symbols.", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunOutline_Human_CSharpUsingVarTopLevelFile_WritesHelpfulNote() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_outline_csharp_using_var_human"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/program.cs", + "csharp", + """ + using System; + using System.IO; + + using var stream01 = new MemoryStream(); + using var stream02 = new MemoryStream(); + using var stream03 = new MemoryStream(); + using var stream04 = new MemoryStream(); + using var stream05 = new MemoryStream(); + using var stream06 = new MemoryStream(); + using var stream07 = new MemoryStream(); + using var stream08 = new MemoryStream(); + using var stream09 = new MemoryStream(); + using var stream10 = new MemoryStream(); + using var stream11 = new MemoryStream(); + using var stream12 = new MemoryStream(); + using var stream13 = new MemoryStream(); + using var stream14 = new MemoryStream(); + using var stream15 = new MemoryStream(); + using var stream16 = new MemoryStream(); + using var stream17 = new MemoryStream(); + using var stream18 = new MemoryStream(); + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunOutline( + ["src/program.cs", "--db", dbPath], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Contains("# src/program.cs", stdout); + Assert.Contains("Note: no type/namespace declarations found; this file likely uses C# top-level statements.", stderr); + Assert.Contains("Outline lists imports and local functions only; the executable body is not indexed as symbols.", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunOutline_Human_CSharpUsingStatementTopLevelFile_WritesHelpfulNote() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_outline_csharp_using_statement_human"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/program.cs", + "csharp", + """ + using System; + using System.IO; + + using (var stream01 = new MemoryStream()) + { + Console.WriteLine(stream01.Length); + } + using (var stream02 = new MemoryStream()) + { + Console.WriteLine(stream02.Length); + } + using (var stream03 = new MemoryStream()) + { + Console.WriteLine(stream03.Length); + } + using (var stream04 = new MemoryStream()) + { + Console.WriteLine(stream04.Length); + } + using (var stream05 = new MemoryStream()) + { + Console.WriteLine(stream05.Length); + } + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunOutline( + ["src/program.cs", "--db", dbPath], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Contains("# src/program.cs", stdout); + Assert.Contains("Note: no type/namespace declarations found; this file likely uses C# top-level statements.", stderr); + Assert.Contains("Outline lists imports and local functions only; the executable body is not indexed as symbols.", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunOutline_Human_CSharpLocalFunctionsOnlyFile_DoesNotWriteTopLevelStatementsHint() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_outline_csharp_local_functions_only_human"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/program.cs", + "csharp", + """ + using System; + + static int Sum(int left, int right) + { + return left + right; + } + + static int Diff(int left, int right) + { + return left - right; + } + + static int Mul(int left, int right) + { + return left * right; + } + + static int Div(int left, int right) + { + return left / right; + } + + static void Print(int value) + { + Console.WriteLine(value); + } + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunOutline( + ["src/program.cs", "--db", dbPath], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Contains("# src/program.cs", stdout); + Assert.DoesNotContain("likely uses C# top-level statements", stderr); + Assert.DoesNotContain("executable body is not indexed as symbols", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunOutline_Human_GlobalUsingsFile_DoesNotWriteTopLevelStatementsHint() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_outline_global_usings_human"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/GlobalUsings.cs", + "csharp", + """ + global using System; + global using System.IO; + global using System.Linq; + global using System.Text; + global using System.Text.Json; + global using System.Text.Json.Serialization; + global using System.Collections.Generic; + global using System.Collections.Immutable; + global using System.Diagnostics; + global using System.Globalization; + global using System.Net.Http; + global using System.Net.Http.Json; + global using System.Threading; + global using System.Threading.Tasks; + global using Microsoft.Extensions.Logging; + global using Microsoft.Extensions.Options; + global using Microsoft.Data.Sqlite; + global using CodeIndex.Models; + global using CodeIndex.Database; + global using CodeIndex.Indexer; + global using CodeIndex.Cli; + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunOutline( + ["src/GlobalUsings.cs", "--db", dbPath], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Contains("# src/GlobalUsings.cs", stdout); + Assert.DoesNotContain("likely uses C# top-level statements", stderr); + Assert.DoesNotContain("executable body is not indexed as symbols", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunOutline_Human_AssemblyInfoFile_DoesNotWriteTopLevelStatementsHint() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_outline_assembly_info_human"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/Properties/AssemblyInfo.cs", + "csharp", + """ + using System.Reflection; + + [assembly: AssemblyTitle("CodeIndex")] + [assembly: AssemblyDescription("Tests top-level hint suppression")] + [assembly: AssemblyConfiguration("Debug")] + [assembly: AssemblyCompany("Widthdom")] + [assembly: AssemblyProduct("CodeIndex")] + [assembly: AssemblyCopyright("Copyright Widthdom")] + [assembly: AssemblyTrademark("")] + [assembly: AssemblyCulture("")] + [assembly: AssemblyVersion("1.0.0.0")] + [assembly: AssemblyFileVersion("1.0.0.0")] + [assembly: AssemblyMetadata("Build", "Local")] + [assembly: AssemblyMetadata("Environment", "Test")] + [assembly: AssemblyMetadata("Flavor", "Debug")] + [assembly: AssemblyMetadata("Feature", "OutlineHint")] + [assembly: AssemblyMetadata("Branch", "pr-149")] + [assembly: AssemblyMetadata("Commit", "local")] + [assembly: AssemblyMetadata("Purpose", "Negative coverage")] + [assembly: AssemblyMetadata("Owner", "CodeIndex Tests")] + [assembly: AssemblyMetadata("Language", "csharp")] + [assembly: AssemblyMetadata("FileKind", "AssemblyInfo")] + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunOutline( + ["src/Properties/AssemblyInfo.cs", "--db", dbPath], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Contains("# src/Properties/AssemblyInfo.cs", stdout); + Assert.DoesNotContain("likely uses C# top-level statements", stderr); + Assert.DoesNotContain("executable body is not indexed as symbols", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunOutline_Human_GeneratedGlobalUsingsFile_DoesNotWriteTopLevelStatementsHint() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_outline_generated_global_usings_human"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/GlobalUsings.g.cs", + "csharp", + """ + // + #pragma warning disable 1591 + global using System; + global using System.IO; + global using System.Linq; + global using System.Text; + global using System.Text.Json; + global using System.Text.Json.Serialization; + global using System.Collections.Generic; + global using System.Collections.Immutable; + global using System.Diagnostics; + global using System.Globalization; + global using System.Net.Http; + global using System.Net.Http.Json; + global using System.Threading; + global using System.Threading.Tasks; + global using Microsoft.Extensions.Logging; + global using Microsoft.Extensions.Options; + global using Microsoft.Data.Sqlite; + global using CodeIndex.Models; + global using CodeIndex.Database; + global using CodeIndex.Indexer; + global using CodeIndex.Cli; + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunOutline( + ["src/GlobalUsings.g.cs", "--db", dbPath], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Contains("# src/GlobalUsings.g.cs", stdout); + Assert.DoesNotContain("likely uses C# top-level statements", stderr); + Assert.DoesNotContain("executable body is not indexed as symbols", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunOutline_Human_GeneratedAssemblyInfoFile_DoesNotWriteTopLevelStatementsHint() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_outline_generated_assembly_info_human"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/Properties/AssemblyInfo.cs", + "csharp", + """ + // + #pragma warning disable 1591 + using System.Reflection; + + [assembly: AssemblyTitle("CodeIndex")] + [assembly: AssemblyDescription("Tests top-level hint suppression")] + [assembly: AssemblyConfiguration("Debug")] + [assembly: AssemblyCompany("Widthdom")] + [assembly: AssemblyProduct("CodeIndex")] + [assembly: AssemblyCopyright("Copyright Widthdom")] + [assembly: AssemblyTrademark("")] + [assembly: AssemblyCulture("")] + [assembly: AssemblyVersion("1.0.0.0")] + [assembly: AssemblyFileVersion("1.0.0.0")] + [assembly: AssemblyMetadata("Build", "Local")] + [assembly: AssemblyMetadata("Environment", "Test")] + [assembly: AssemblyMetadata("Flavor", "Debug")] + [assembly: AssemblyMetadata("Feature", "OutlineHint")] + [assembly: AssemblyMetadata("Branch", "pr-149")] + [assembly: AssemblyMetadata("Commit", "local")] + [assembly: AssemblyMetadata("Purpose", "Negative coverage")] + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunOutline( + ["src/Properties/AssemblyInfo.cs", "--db", dbPath], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Contains("# src/Properties/AssemblyInfo.cs", stdout); + Assert.DoesNotContain("likely uses C# top-level statements", stderr); + Assert.DoesNotContain("executable body is not indexed as symbols", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunOutline_Human_BlockCommentGlobalUsingsFile_DoesNotWriteTopLevelStatementsHint() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_outline_block_comment_global_usings_human"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/GlobalUsings.cs", + "csharp", + """ + /* + * + * This file is generated during build. + */ + #pragma warning disable 1591 + global using System; + global using System.IO; + global using System.Linq; + global using System.Text; + global using System.Text.Json; + global using System.Text.Json.Serialization; + global using System.Collections.Generic; + global using System.Collections.Immutable; + global using System.Diagnostics; + global using System.Globalization; + global using System.Net.Http; + global using System.Net.Http.Json; + global using System.Threading; + global using System.Threading.Tasks; + global using Microsoft.Extensions.Logging; + global using Microsoft.Extensions.Options; + global using Microsoft.Data.Sqlite; + global using CodeIndex.Models; + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunOutline( + ["src/GlobalUsings.cs", "--db", dbPath], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Contains("# src/GlobalUsings.cs", stdout); + Assert.DoesNotContain("likely uses C# top-level statements", stderr); + Assert.DoesNotContain("executable body is not indexed as symbols", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunOutline_Human_BlockCommentAssemblyInfoFile_DoesNotWriteTopLevelStatementsHint() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_outline_block_comment_assembly_info_human"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/Properties/AssemblyInfo.cs", + "csharp", + """ + /* + * + * This file is generated during build. + */ + #pragma warning disable 1591 + using System.Reflection; + + [assembly: AssemblyTitle("CodeIndex")] + [assembly: AssemblyDescription("Tests top-level hint suppression")] + [assembly: AssemblyConfiguration("Debug")] + [assembly: AssemblyCompany("Widthdom")] + [assembly: AssemblyProduct("CodeIndex")] + [assembly: AssemblyCopyright("Copyright Widthdom")] + [assembly: AssemblyTrademark("")] + [assembly: AssemblyCulture("")] + [assembly: AssemblyVersion("1.0.0.0")] + [assembly: AssemblyFileVersion("1.0.0.0")] + [assembly: AssemblyMetadata("Build", "Local")] + [assembly: AssemblyMetadata("Environment", "Test")] + [assembly: AssemblyMetadata("Flavor", "Debug")] + [assembly: AssemblyMetadata("Feature", "OutlineHint")] + [assembly: AssemblyMetadata("Branch", "pr-149")] + [assembly: AssemblyMetadata("Commit", "local")] + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunOutline( + ["src/Properties/AssemblyInfo.cs", "--db", dbPath], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Contains("# src/Properties/AssemblyInfo.cs", stdout); + Assert.DoesNotContain("likely uses C# top-level statements", stderr); + Assert.DoesNotContain("executable body is not indexed as symbols", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunOutline_Human_ExternAliasOnlyFile_DoesNotWriteTopLevelStatementsHint() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_outline_extern_alias_only_human"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/Aliases.cs", + "csharp", + """ + extern alias Foo01; + extern alias Foo02; + extern alias Foo03; + extern alias Foo04; + extern alias Foo05; + extern alias Foo06; + extern alias Foo07; + extern alias Foo08; + extern alias Foo09; + extern alias Foo10; + extern alias Foo11; + extern alias Foo12; + extern alias Foo13; + extern alias Foo14; + extern alias Foo15; + extern alias Foo16; + extern alias Foo17; + extern alias Foo18; + extern alias Foo19; + extern alias Foo20; + extern alias Foo21; + extern alias Foo22; + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunOutline( + ["src/Aliases.cs", "--db", dbPath], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Contains("# src/Aliases.cs", stdout); + Assert.DoesNotContain("likely uses C# top-level statements", stderr); + Assert.DoesNotContain("executable body is not indexed as symbols", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunOutline_CssFamilyLessFontFaceSameLineStillFindsFollowingRule() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_outline_css_familyless_fontface_inline"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "styles.css"), + """ + @font-face { src: url("no-family.woff2"); } .after { color: red; } + """); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + var (outlineExitCode, outlineStdout, outlineStderr) = CaptureConsole(() => QueryCommandRunner.RunOutline( + ["src/styles.css", "--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(outlineStdout); + var names = document.RootElement + .GetProperty("symbols") + .EnumerateArray() + .Select(symbol => symbol.GetProperty("name").GetString()) + .Where(name => name != null) + .ToHashSet(StringComparer.Ordinal); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, outlineExitCode); + Assert.Equal(string.Empty, outlineStderr); + Assert.Contains(".after", names); + Assert.DoesNotContain("@font-face", names); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunOutline_CSharpSameLineSiblingEnumsIncludesBothEnumsAndMembers() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_outline_csharp_enum_same_line_siblings"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/mode.cs", + "csharp", + """ + namespace Demo; + + public enum InlineA { A1 } public enum InlineB { B1 } + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunOutline( + ["src/mode.cs", "--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var names = document.RootElement + .GetProperty("symbols") + .EnumerateArray() + .Select(symbol => symbol.GetProperty("name").GetString()) + .Where(name => name != null) + .ToHashSet(StringComparer.Ordinal); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Contains("InlineA", names); + Assert.Contains("InlineB", names); + Assert.Contains("A1", names); + Assert.Contains("B1", names); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunOutline_CSharpSameLineNamespaceBodyIncludesNestedEnumAndMembers() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_outline_csharp_enum_same_line_namespace"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/mode.cs", + "csharp", + """ + namespace Demo { public enum E { A } } + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunOutline( + ["src/mode.cs", "--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var names = document.RootElement + .GetProperty("symbols") + .EnumerateArray() + .Select(symbol => symbol.GetProperty("name").GetString()) + .Where(name => name != null) + .ToHashSet(StringComparer.Ordinal); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Contains("Demo", names); + Assert.Contains("E", names); + Assert.Contains("A", names); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_CSharpBraceCharLiteralKeepsMethodInsideClass() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_inspect_csharp_brace_char"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "fixture.cs"), + """"" + namespace Demo; + + public class FixtureHost + { + public bool IsClosingBrace(char c) + { + return c is not '}'; + } + + public void AfterBraceLiteral() + { + } + } + """""); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + var (inspectExitCode, inspectStdout, inspectStderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["AfterBraceLiteral", "--db", dbPath, "--json", "--exact-name", "--lang", "csharp"], + _jsonOptions)); + + using var document = ParseJsonOutput(inspectStdout); + var json = document.RootElement; + var definition = Assert.Single(json.GetProperty("definitions").EnumerateArray()); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, inspectExitCode); + Assert.Equal(string.Empty, inspectStderr); + Assert.Equal("AfterBraceLiteral", definition.GetProperty("name").GetString()); + Assert.Equal("class", definition.GetProperty("container_kind").GetString()); + Assert.Equal("FixtureHost", definition.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_CSharpMultilineRawStringBraceKeepsMethodInsideClass() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_inspect_csharp_raw_string_brace"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "fixture.cs"), + """" + namespace Demo; + + public class FixtureHost + { + public string UsesRawFixture() + { + return """ + } + """; + } + + public void AfterRawString() + { + } + } + """"); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + var (inspectExitCode, inspectStdout, inspectStderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["AfterRawString", "--db", dbPath, "--json", "--exact-name", "--lang", "csharp"], + _jsonOptions)); + + using var document = ParseJsonOutput(inspectStdout); + var json = document.RootElement; + var definition = Assert.Single(json.GetProperty("definitions").EnumerateArray()); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, inspectExitCode); + Assert.Equal(string.Empty, inspectStderr); + Assert.Equal("AfterRawString", definition.GetProperty("name").GetString()); + Assert.Equal("class", definition.GetProperty("container_kind").GetString()); + Assert.Equal("FixtureHost", definition.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_CSharpMultilineVerbatimStringBraceKeepsMethodInsideClass() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_inspect_csharp_verbatim_string_brace"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "fixture.cs"), + """" + namespace Demo; + + public class FixtureHost + { + public string UsesVerbatimFixture() + { + return @" + { + "; + } + + public void AfterVerbatimString() + { + } + } + """"); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + var (inspectExitCode, inspectStdout, inspectStderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["AfterVerbatimString", "--db", dbPath, "--json", "--exact-name", "--lang", "csharp"], + _jsonOptions)); + + using var document = ParseJsonOutput(inspectStdout); + var json = document.RootElement; + var definition = Assert.Single(json.GetProperty("definitions").EnumerateArray()); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, inspectExitCode); + Assert.Equal(string.Empty, inspectStderr); + Assert.Equal("AfterVerbatimString", definition.GetProperty("name").GetString()); + Assert.Equal("class", definition.GetProperty("container_kind").GetString()); + Assert.Equal("FixtureHost", definition.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunOutline_RustLifetimeAnnotationsKeepBodyRangesIntact() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_outline_rust_lifetime"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "fixture.rs"), + """ + pub struct Holder<'a> { + value: &'a str, + } + + impl<'a> Holder<'a> { + pub fn get(&self) -> &'a str { + self.value + } + } + """); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + var (outlineExitCode, outlineStdout, outlineStderr) = CaptureConsole(() => QueryCommandRunner.RunOutline( + ["src/fixture.rs", "--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(outlineStdout); + var json = document.RootElement; + var symbols = json.GetProperty("symbols").EnumerateArray().ToArray(); + var holder = Assert.Single(symbols.Where(symbol => symbol.GetProperty("kind").GetString() == "struct" && symbol.GetProperty("name").GetString() == "Holder")); + var get = Assert.Single(symbols.Where(symbol => symbol.GetProperty("kind").GetString() == "function" && symbol.GetProperty("name").GetString() == "get")); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, outlineExitCode); + Assert.Equal(string.Empty, outlineStderr); + Assert.Equal(3, holder.GetProperty("end_line").GetInt32()); + Assert.Equal(8, get.GetProperty("end_line").GetInt32()); + Assert.Equal("class", get.GetProperty("container_kind").GetString()); + Assert.Equal("Holder", get.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunOutline_CSharpSplitPropertyHeader_DoesNotEmitPhantomFunctionAndKeepsSignature() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_outline_csharp_split_property_header"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "fixture.cs"), + """ + namespace Demo; + + public class Model + { + public string + SplitName + => "x"; + } + """); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + var (outlineExitCode, outlineStdout, outlineStderr) = CaptureConsole(() => QueryCommandRunner.RunOutline( + ["src/fixture.cs", "--db", dbPath, "--json"], + _jsonOptions)); + + using var outlineDocument = ParseJsonOutput(outlineStdout); + var outlineJson = outlineDocument.RootElement; + var splitNameSymbols = outlineJson.GetProperty("symbols").EnumerateArray() + .Where(symbol => symbol.GetProperty("name").GetString() == "SplitName") + .ToArray(); + var property = Assert.Single(splitNameSymbols.Where(symbol => symbol.GetProperty("kind").GetString() == "property")); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, outlineExitCode); + Assert.Equal(string.Empty, outlineStderr); + Assert.Single(splitNameSymbols); + Assert.Equal("public string SplitName => \"x\";", property.GetProperty("signature").GetString()); + Assert.Equal(5, property.GetProperty("start_line").GetInt32()); + Assert.Equal(7, property.GetProperty("end_line").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunOutline_CSharpLongGenericMultilinePropertyHeader_KeepsReturnTypeAndSignature() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_outline_csharp_long_generic_property_header"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "fixture.cs"), + """ + namespace Demo; + + public class Model + { + public Dictionary< + string, + List< + int + >> + Count + => new(); + } + """); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + var (outlineExitCode, outlineStdout, outlineStderr) = CaptureConsole(() => QueryCommandRunner.RunOutline( + ["src/fixture.cs", "--db", dbPath, "--json"], + _jsonOptions)); + + using var outlineDocument = ParseJsonOutput(outlineStdout); + var outlineJson = outlineDocument.RootElement; + var property = Assert.Single(outlineJson.GetProperty("symbols").EnumerateArray().Where(symbol => + symbol.GetProperty("kind").GetString() == "property" + && symbol.GetProperty("name").GetString() == "Count")); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, outlineExitCode); + Assert.Equal(string.Empty, outlineStderr); + Assert.Equal("Dictionary>", property.GetProperty("return_type").GetString()); + Assert.Equal("public Dictionary< string, List< int >> Count => new();", property.GetProperty("signature").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunOutline_CSharpBraceOnNextLinePropertyHeader_KeepsHeaderSignature() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_outline_csharp_brace_property_header"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "fixture.cs"), + """ + namespace Demo; + + public class Model + { + public string SplitName + { + get; + set; + } + + public int Count + { get => 1; } + } + """); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + var (outlineExitCode, outlineStdout, outlineStderr) = CaptureConsole(() => QueryCommandRunner.RunOutline( + ["src/fixture.cs", "--db", dbPath, "--json"], + _jsonOptions)); + + using var outlineDocument = ParseJsonOutput(outlineStdout); + var outlineJson = outlineDocument.RootElement; + var splitName = Assert.Single(outlineJson.GetProperty("symbols").EnumerateArray().Where(symbol => + symbol.GetProperty("kind").GetString() == "property" + && symbol.GetProperty("name").GetString() == "SplitName")); + var count = Assert.Single(outlineJson.GetProperty("symbols").EnumerateArray().Where(symbol => + symbol.GetProperty("kind").GetString() == "property" + && symbol.GetProperty("name").GetString() == "Count")); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, outlineExitCode); + Assert.Equal(string.Empty, outlineStderr); + Assert.Equal("public string SplitName {", splitName.GetProperty("signature").GetString()); + Assert.Equal("public int Count {", count.GetProperty("signature").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunOutline_CSharpPartialPropertyImplementation_WithAccessorAttribute_IsDetected() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_outline_csharp_accessor_attribute_property"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "fixture.cs"), + """ + namespace Demo; + + public partial class Model + { + public partial string Name { get; set; } + } + + public partial class Model + { + public partial string Name { [System.Obsolete] get => "x"; set { } } + } + """); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + var (outlineExitCode, outlineStdout, outlineStderr) = CaptureConsole(() => QueryCommandRunner.RunOutline( + ["src/fixture.cs", "--db", dbPath, "--json"], + _jsonOptions)); + + using var outlineDocument = ParseJsonOutput(outlineStdout); + var outlineJson = outlineDocument.RootElement; + var names = outlineJson.GetProperty("symbols").EnumerateArray() + .Where(symbol => symbol.GetProperty("kind").GetString() == "property" + && symbol.GetProperty("name").GetString() == "Name") + .ToArray(); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, outlineExitCode); + Assert.Equal(string.Empty, outlineStderr); + Assert.Equal(2, names.Length); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunOutline_CSharpPartialPropertyImplementation_WithMultilineAccessorAttribute_IsDetected() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_outline_csharp_multiline_accessor_attribute_property"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "fixture.cs"), + """ + namespace Demo; + + public partial class Model + { + public partial string Name + { + [System.Obsolete( + "x" + )] + get => "x"; + set { } + } + + public int Other => 1; + } + """); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + var (outlineExitCode, outlineStdout, outlineStderr) = CaptureConsole(() => QueryCommandRunner.RunOutline( + ["src/fixture.cs", "--db", dbPath, "--json"], + _jsonOptions)); + + using var outlineDocument = ParseJsonOutput(outlineStdout); + var outlineJson = outlineDocument.RootElement; + var symbols = outlineJson.GetProperty("symbols").EnumerateArray().ToArray(); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, outlineExitCode); + Assert.Equal(string.Empty, outlineStderr); + Assert.Contains(symbols, symbol => symbol.GetProperty("kind").GetString() == "property" && symbol.GetProperty("name").GetString() == "Name"); + Assert.Contains(symbols, symbol => symbol.GetProperty("kind").GetString() == "property" && symbol.GetProperty("name").GetString() == "Other"); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunOutline_JavaScriptStringBraceKeepsFollowingMethodInsideClass() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_outline_js_string_brace"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "example.js"), + """ + export class Example { + foo() { + const value = "}"; + return value; + } + + bar() { + return 1; + } + } + """); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + var (outlineExitCode, outlineStdout, outlineStderr) = CaptureConsole(() => QueryCommandRunner.RunOutline( + ["src/example.js", "--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(outlineStdout); + var json = document.RootElement; + var symbols = json.GetProperty("symbols").EnumerateArray().ToArray(); + var example = Assert.Single(symbols.Where(symbol => symbol.GetProperty("kind").GetString() == "class" && symbol.GetProperty("name").GetString() == "Example")); + var foo = Assert.Single(symbols.Where(symbol => symbol.GetProperty("kind").GetString() == "function" && symbol.GetProperty("name").GetString() == "foo")); + var bar = Assert.Single(symbols.Where(symbol => symbol.GetProperty("kind").GetString() == "function" && symbol.GetProperty("name").GetString() == "bar")); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, outlineExitCode); + Assert.Equal(string.Empty, outlineStderr); + Assert.Equal(10, example.GetProperty("end_line").GetInt32()); + Assert.Equal(5, foo.GetProperty("end_line").GetInt32()); + Assert.Equal("class", bar.GetProperty("container_kind").GetString()); + Assert.Equal("Example", bar.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunOutline_JavaScriptTemplateLiteralBraceKeepsFollowingMethodInsideClass() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_outline_js_template_brace"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "example.js"), + """ + export class Example { + foo() { + const value = `}`; + return value; + } + + bar() { + return 1; + } + } + """); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + var (outlineExitCode, outlineStdout, outlineStderr) = CaptureConsole(() => QueryCommandRunner.RunOutline( + ["src/example.js", "--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(outlineStdout); + var json = document.RootElement; + var symbols = json.GetProperty("symbols").EnumerateArray().ToArray(); + var example = Assert.Single(symbols.Where(symbol => symbol.GetProperty("kind").GetString() == "class" && symbol.GetProperty("name").GetString() == "Example")); + var foo = Assert.Single(symbols.Where(symbol => symbol.GetProperty("kind").GetString() == "function" && symbol.GetProperty("name").GetString() == "foo")); + var bar = Assert.Single(symbols.Where(symbol => symbol.GetProperty("kind").GetString() == "function" && symbol.GetProperty("name").GetString() == "bar")); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, outlineExitCode); + Assert.Equal(string.Empty, outlineStderr); + Assert.Equal(10, example.GetProperty("end_line").GetInt32()); + Assert.Equal(5, foo.GetProperty("end_line").GetInt32()); + Assert.Equal("class", bar.GetProperty("container_kind").GetString()); + Assert.Equal("Example", bar.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunOutline_TypeScriptTemplateInterpolationBracesKeepFollowingMethodInsideClass() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_outline_ts_template_interp"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "example.ts"), + """ + export class Example { + foo() { + const value = `${format({ answer: 42 })}`; + return value; + } + + bar() { + return 1; + } + } + """); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + var (outlineExitCode, outlineStdout, outlineStderr) = CaptureConsole(() => QueryCommandRunner.RunOutline( + ["src/example.ts", "--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(outlineStdout); + var json = document.RootElement; + var symbols = json.GetProperty("symbols").EnumerateArray().ToArray(); + var example = Assert.Single(symbols.Where(symbol => symbol.GetProperty("kind").GetString() == "class" && symbol.GetProperty("name").GetString() == "Example")); + var foo = Assert.Single(symbols.Where(symbol => symbol.GetProperty("kind").GetString() == "function" && symbol.GetProperty("name").GetString() == "foo")); + var bar = Assert.Single(symbols.Where(symbol => symbol.GetProperty("kind").GetString() == "function" && symbol.GetProperty("name").GetString() == "bar")); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, outlineExitCode); + Assert.Equal(string.Empty, outlineStderr); + Assert.Equal(10, example.GetProperty("end_line").GetInt32()); + Assert.Equal(5, foo.GetProperty("end_line").GetInt32()); + Assert.Equal("class", bar.GetProperty("container_kind").GetString()); + Assert.Equal("Example", bar.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_Json_StaleSqlGraphContractIncludesDegradedState() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_inspect_sql_graph_contract"); + try + { + var dbPath = CreateSqlGraphContractFixtureDb(projectRoot); + DowngradeSqlGraphContractRows(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["fn_Target", "--db", dbPath, "--json", "--lang", "sql", "--exact"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.False(json.GetProperty("sql_graph_contract_ready").GetBoolean()); + Assert.Contains("sql_graph_contract_ready=false", json.GetProperty("sql_graph_contract_degraded_reason").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_ExactJson_MixedRepoStaleSqlGraphContractDoesNotDegradePureCSharpBundle() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_inspect_mixed_sql_graph_contract"); + try + { + var dbPath = CreateMixedSqlGraphContractFixtureDb(projectRoot); + DowngradeSqlGraphContractRows(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["N", "--db", dbPath, "--json", "--exact"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("N", json.GetProperty("query").GetString()); + Assert.False(json.TryGetProperty("sql_graph_contract_ready", out _)); + Assert.False(json.TryGetProperty("sql_graph_contract_degraded_reason", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_JsonKeepsSubscribeReferencesVisibleInBundle() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_inspect_subscribe_bundle"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Publisher.cs", "csharp", + """ + using System; + + public class Publisher + { + public event EventHandler? Changed; + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Subscriber.cs", "csharp", + """ + using System; + + public class Subscriber + { + public void Hook(Publisher publisher) + { + publisher.Changed += OnChanged; + } + + private void OnChanged(object? sender, EventArgs e) { } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Changed", "--db", dbPath, "--json", "--lang", "csharp", "--exact"], + _jsonOptions)); + + 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("subscribe", reference.GetProperty("reference_kind").GetString()); + Assert.Equal("Hook", reference.GetProperty("container_name").GetString()); + Assert.Equal("Hook", caller.GetProperty("caller_name").GetString()); + Assert.Equal("Changed", caller.GetProperty("callee_name").GetString()); + Assert.Empty(json.GetProperty("callees").EnumerateArray()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_JsonIncludesResolvedEnumMemberReferences() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_inspect_enum_member_bundle"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public enum Nested + { + A = 1, + B = A + } + + public class UsesEnum + { + public void Use() + { + _ = Nested.A; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["A", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + var definition = Assert.Single(json.GetProperty("definitions").EnumerateArray()); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("A", definition.GetProperty("name").GetString()); + Assert.Equal("enum", definition.GetProperty("container_kind").GetString()); + Assert.Equal("Nested", definition.GetProperty("container_name").GetString()); + Assert.Equal("csharp", json.GetProperty("graph_language").GetString()); + Assert.True(json.GetProperty("graph_supported").GetBoolean()); + var reference = Assert.Single(json.GetProperty("references").EnumerateArray()); + Assert.Equal("A", reference.GetProperty("symbol_name").GetString()); + Assert.Equal("Use", reference.GetProperty("container_name").GetString()); + Assert.False(json.TryGetProperty("graph_degraded", out _)); + Assert.False(json.TryGetProperty("unsupported_symbol_kind", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_ExactJson_CSharpMultiLineConstantPatternKeepsFileReferenceCountWithoutPhantomProperties() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_inspect_multiline_constant_pattern"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/A/Color.cs", "csharp", + """ + namespace A; + + public enum Color + { + Red + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/A/UseDeep.cs", "csharp", + """ + using static A.Color; + + namespace A; + + public class DemoDeep + { + public bool Match(object value) => value is + Red + or + Red; + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Match", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name", "--path", "src/A/UseDeep.cs", "--body"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + var file = json.GetProperty("file"); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(4, file.GetProperty("symbol_count").GetInt32()); + Assert.Equal(2, file.GetProperty("reference_count").GetInt32()); + Assert.DoesNotContain( + json.GetProperty("nearby_symbols").EnumerateArray(), + symbol => symbol.GetProperty("kind").GetString() == "property" + && symbol.GetProperty("name").GetString() == "Red"); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunOutline_Json_CSharpMultiLineExpressionBodiedMethod_KeepsFullSignatureAndSiblingField() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_outline_csharp_multiline_expr_body_signature"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Use.cs", "csharp", + """ + namespace Demo; + + public enum Color + { + Red + } + + public sealed class Uses + { + public bool Match(object value) => value is + Red; public int X; + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunOutline( + ["src/Use.cs", "--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var symbols = document.RootElement.GetProperty("symbols").EnumerateArray().ToList(); + var match = Assert.Single(symbols.Where(symbol => + symbol.GetProperty("kind").GetString() == "function" + && symbol.GetProperty("name").GetString() == "Match")); + var x = Assert.Single(symbols.Where(symbol => + symbol.GetProperty("kind").GetString() == "property" + && symbol.GetProperty("name").GetString() == "X")); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("public bool Match(object value) => value is Red;", match.GetProperty("signature").GetString()); + Assert.Equal("public int X;", x.GetProperty("signature").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_NonExactJson_TreatsEnumMembersAsGraphSupported() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_inspect_enum_member_bundle_non_exact"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public enum Color + { + Red, + Green + } + + public class UsesColor + { + public Color Shade => Color.Red; + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Red", "--db", dbPath, "--json", "--lang", "csharp"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + var definition = Assert.Single(json.GetProperty("definitions").EnumerateArray()); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("Red", definition.GetProperty("name").GetString()); + Assert.Equal("enum", definition.GetProperty("container_kind").GetString()); + Assert.Equal("Color", definition.GetProperty("container_name").GetString()); + Assert.Equal("csharp", json.GetProperty("graph_language").GetString()); + Assert.True(json.GetProperty("graph_supported").GetBoolean()); + var reference = Assert.Single(json.GetProperty("references").EnumerateArray()); + Assert.Equal("Shade", reference.GetProperty("container_name").GetString()); + Assert.False(json.TryGetProperty("graph_degraded", out _)); + Assert.False(json.TryGetProperty("unsupported_symbol_kind", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_ExactJson_CSharpCompactSameLineTypeBody_PrefersInnermostMethodContainer() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_inspect_compact_same_line_type_body"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace N; + enum Color { Red } + class C { int N => 0; void M() { var x = global::N.Color.Red; } } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + 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()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_NonExactJson_MixedCallableAndEnumMemberKeepsGraphSupported() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_inspect_mixed_callable_enum"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public class Worker + { + public void A() { } + + public void Use() + { + A(); + } + } + + public enum Status + { + A + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["A", "--db", dbPath, "--json", "--lang", "csharp"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.True(json.GetProperty("graph_supported").GetBoolean()); + Assert.NotEmpty(json.GetProperty("references").EnumerateArray()); + Assert.False(json.TryGetProperty("graph_degraded", out _)); + Assert.False(json.TryGetProperty("unsupported_symbol_kind", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_ExactJson_LargeMixedCandidateSetKeepsGraphClean() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_inspect_large_mixed_exact"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + for (var i = 0; i < 40; i++) + { + TestProjectHelper.InsertIndexedFile(dbPath, $"src/Worker{i}.cs", "csharp", + $$""" + namespace Demo; + + public class Worker{{i}} + { + public void Ready() { } + } + """); + } + + TestProjectHelper.InsertIndexedFile(dbPath, "src/Status.cs", "csharp", + """ + namespace Demo; + + public enum Status + { + Ready + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.True(json.GetProperty("graph_supported").GetBoolean()); + Assert.False(json.TryGetProperty("graph_degraded", out _)); + Assert.False(json.TryGetProperty("unsupported_symbol_kind", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_ExactJson_MixedCSharpEnumMemberHitsKeepGraphSupported() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_inspect_mixed_enum_member_bundle"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public enum E + { + A + } + + public class Holder + { + public int A { get; } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["A", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(2, json.GetProperty("definitions").GetArrayLength()); + Assert.True(json.GetProperty("graph_supported").GetBoolean()); + Assert.False(json.TryGetProperty("graph_degraded", out _)); + Assert.False(json.TryGetProperty("unsupported_symbol_kind", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_ExactJson_CrossLanguageMixedHitCombinesPrimaryGraphAndEnumGapReason() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_inspect_cross_language_mixed_exact"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "web/app.js", "javascript", + """ + export function Ready() {} + + Ready(); + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/status.cs", "csharp", + """ + namespace Demo; + + public enum Status + { + Ready + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Ready", "--db", dbPath, "--json", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("javascript", json.GetProperty("graph_language").GetString()); + Assert.True(json.GetProperty("graph_supported").GetBoolean()); + Assert.False(json.TryGetProperty("graph_degraded", out _)); + Assert.False(json.TryGetProperty("unsupported_symbol_kind", out _)); + Assert.Equal("Call-graph extraction is indexed for 'javascript'.", json.GetProperty("graph_support_reason").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_ExactJson_CrossLanguageMixedHitPrefersGraphCapablePrimaryDefinition() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_inspect_cross_language_primary_alignment"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "web/app.js", "javascript", + """ + function Ready() {} + + function Helper() {} + + Ready(); + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/status.cs", "csharp", + """ + namespace Demo; + + public enum Status + { + Ready + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Ready", "--db", dbPath, "--json", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + var nearbyPaths = json.GetProperty("nearby_symbols") + .EnumerateArray() + .Select(symbol => symbol.GetProperty("path").GetString()) + .Where(path => path != null) + .Distinct(StringComparer.Ordinal) + .ToList(); + var references = json.GetProperty("references").EnumerateArray().ToList(); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("javascript", json.GetProperty("graph_language").GetString()); + Assert.True(json.GetProperty("graph_supported").GetBoolean()); + Assert.False(json.TryGetProperty("graph_degraded", out _)); + Assert.False(json.TryGetProperty("unsupported_symbol_kind", out _)); + Assert.Equal("web/app.js", json.GetProperty("file").GetProperty("path").GetString()); + Assert.Contains("web/app.js", nearbyPaths); + Assert.DoesNotContain("src/status.cs", nearbyPaths); + Assert.Contains(json.GetProperty("nearby_symbols").EnumerateArray(), + symbol => symbol.GetProperty("name").GetString() == "Helper"); + Assert.NotEmpty(references); + Assert.All(references, reference => + Assert.Equal("javascript", reference.GetProperty("lang").GetString())); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_ExactJson_CSharpEnumMemberShortNameCollisionDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_short_name_collision"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace A; + + public enum Status + { + Ready + } + + namespace B; + + public static class Status + { + public static int Ready = 1; + } + + public class Uses + { + public int Read() + { + return Status.Ready; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Empty(json.GetProperty("references").EnumerateArray()); + Assert.Empty(json.GetProperty("callers").EnumerateArray()); + Assert.Equal("csharp", json.GetProperty("graph_language").GetString()); + Assert.True(json.GetProperty("graph_supported").GetBoolean()); + Assert.Equal("Call-graph extraction is indexed for 'csharp'.", json.GetProperty("graph_support_reason").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_ExactJson_CSharpEnumMemberUsingAliasCollisionDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_alias_collision"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace A; + + public enum Status + { + Ready + } + + public static class Values + { + public static int Ready = 1; + } + + namespace B; + + using Status = A.Values; + + public class UsesAlias + { + public int Read() + { + return Status.Ready; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Empty(json.GetProperty("references").EnumerateArray()); + Assert.Empty(json.GetProperty("callers").EnumerateArray()); + Assert.Equal("csharp", json.GetProperty("graph_language").GetString()); + Assert.True(json.GetProperty("graph_supported").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_ExactJson_CSharpEnumMemberAttributeArgumentUsesMetadataKindWithoutCallerEdge() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_attribute_argument"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + using System; + + namespace Demo; + + public enum ConverterStrategy + { + AllowNumbers, + Strict + } + + [AttributeUsage(AttributeTargets.Class)] + public sealed class JsonConverterAttribute : Attribute + { + public JsonConverterAttribute(ConverterStrategy strategy) { } + } + + [JsonConverter(ConverterStrategy.AllowNumbers)] + public class UsesAttribute + { + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["AllowNumbers", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + var reference = Assert.Single(json.GetProperty("references").EnumerateArray()); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("attribute", reference.GetProperty("reference_kind").GetString()); + Assert.Empty(json.GetProperty("callers").EnumerateArray()); + Assert.Equal("csharp", json.GetProperty("graph_language").GetString()); + Assert.True(json.GetProperty("graph_supported").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_ExactJson_CSharpValueReceiverNamedLikeEnumDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_value_receiver_collision"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Holder Status { get; } = new(); + + public int Read() + { + return Status.Ready; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Empty(json.GetProperty("references").EnumerateArray()); + Assert.Empty(json.GetProperty("callers").EnumerateArray()); + Assert.Equal("csharp", json.GetProperty("graph_language").GetString()); + Assert.True(json.GetProperty("graph_supported").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_ExactJson_CSharpStaticPropertyShadowStillSuppressesReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_static_property_collision"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public static Holder Status { get; } = new(); + + public static int Read() + { + return Status.Ready; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Empty(json.GetProperty("references").EnumerateArray()); + Assert.Empty(json.GetProperty("callers").EnumerateArray()); + Assert.Equal("csharp", json.GetProperty("graph_language").GetString()); + Assert.True(json.GetProperty("graph_supported").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_ExactJson_CSharpGlobalQualifiedEnumMemberSurvivesPropertyShadowing() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_global_property_shadow"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + enum Color + { + Red + } + + class C + { + int Color => 0; + + void M() + { + var x = global::Color.Red; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + var reference = Assert.Single(json.GetProperty("references").EnumerateArray()); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("M", reference.GetProperty("container_name").GetString()); + Assert.Equal(12, reference.GetProperty("line").GetInt32()); + Assert.Equal("csharp", json.GetProperty("graph_language").GetString()); + Assert.True(json.GetProperty("graph_supported").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_ExactJson_CSharpGlobalQualifiedEnumMemberSurvivesConflictingUsingAlias() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_global_alias_shadow"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo + { + public enum Color + { + Red + } + } + + namespace Shadow + { + public static class Demo + { + public static int Red => 0; + } + } + + using Demo = Shadow; + + class C + { + Demo.Color M() + { + return global::Demo.Color.Red; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + var reference = Assert.Single(json.GetProperty("references").EnumerateArray()); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("M", reference.GetProperty("container_name").GetString()); + Assert.Equal(23, reference.GetProperty("line").GetInt32()); + Assert.Equal("csharp", json.GetProperty("graph_language").GetString()); + Assert.True(json.GetProperty("graph_supported").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_ExactJson_CSharpGlobalQualifiedUsingAliasNameDoesNotCreateReference() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_global_alias_name_invalid"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public enum Color + { + Red + } + + using Color = Demo.Color; + + class C + { + void M() + { + _ = global::Color.Red; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Red", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Empty(json.GetProperty("references").EnumerateArray()); + Assert.Empty(json.GetProperty("callers").EnumerateArray()); + Assert.Equal("csharp", json.GetProperty("graph_language").GetString()); + Assert.True(json.GetProperty("graph_supported").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_ExactJson_CSharpIndentedLocalNamedLikeEnumDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_indented_local_collision"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Demo.Status Read(bool flag) + { + if (flag) + { + Holder Status = new(); + _ = Status.Ready; + } + + return Demo.Status.Ready; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("call", json.GetProperty("reference_kind").GetString()); + Assert.Equal(23, json.GetProperty("line").GetInt32()); + Assert.Equal("Read", json.GetProperty("container_name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_ExactJson_CSharpLambdaParameterNamedLikeEnumDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_lambda_parameter_collision"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + using System; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Func Build() + { + return Status => Status.Ready; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Empty(json.GetProperty("references").EnumerateArray()); + Assert.Empty(json.GetProperty("callers").EnumerateArray()); + Assert.Equal("csharp", json.GetProperty("graph_language").GetString()); + Assert.True(json.GetProperty("graph_supported").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_ExactJson_CSharpMultiLineLambdaParameterNamedLikeEnumDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_multiline_lambda_collision"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + using System; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Func Build() + { + return + (Status) => + Status.Ready; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Empty(json.GetProperty("references").EnumerateArray()); + Assert.Empty(json.GetProperty("callers").EnumerateArray()); + Assert.Equal("csharp", json.GetProperty("graph_language").GetString()); + Assert.True(json.GetProperty("graph_supported").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_ExactJson_CSharpSwitchExpressionRecursivePatternVariableDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_switch_expression_recursive_pattern_collision"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Demo.Status Read(object value) + { + return value switch + { + Holder { Ready: > 0 } Status => (Demo.Status)Status.Ready, + _ => Demo.Status.Ready + }; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + var referenceLines = json.GetProperty("references") + .EnumerateArray() + .Select(reference => reference.GetProperty("line").GetInt32()) + .ToArray(); + var callerReferenceCounts = json.GetProperty("callers") + .EnumerateArray() + .Select(caller => caller.GetProperty("reference_count").GetInt32()) + .ToArray(); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal([20], referenceLines); + Assert.Equal([1], callerReferenceCounts); + Assert.Equal("csharp", json.GetProperty("graph_language").GetString()); + Assert.True(json.GetProperty("graph_supported").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_ExactJson_CSharpQueryRangeVariableNamedLikeEnumDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_query_range_collision"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public IEnumerable Read(IEnumerable items) + { + return from Status in items + select Status.Ready; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Empty(json.GetProperty("references").EnumerateArray()); + Assert.Empty(json.GetProperty("callers").EnumerateArray()); + Assert.Equal("csharp", json.GetProperty("graph_language").GetString()); + Assert.True(json.GetProperty("graph_supported").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_ExactJson_CSharpQueryRangeVariableNamedLikeEnum_OrderByDirectionalCommaDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_query_orderby_directional_collision"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public IEnumerable Read(IEnumerable items) + { + return from Status in items + orderby Status descending, items.Count() ascending + select Status.Ready; + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.True(string.IsNullOrWhiteSpace(stderr)); + Assert.Empty(json.GetProperty("references").EnumerateArray()); + Assert.Empty(json.GetProperty("callers").EnumerateArray()); + Assert.Equal("csharp", json.GetProperty("graph_language").GetString()); + Assert.True(json.GetProperty("graph_supported").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_ExactJson_CSharpQueryRangeVariableMemberNamedSelectSeparatedBySpacesDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_query_orderby_member_named_select_spaced_dot_collision"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + public int select { get; set; } + } + + public sealed class Uses + { + public IEnumerable Read(IEnumerable items) + { + return from Status in items + orderby Status . select, items.Count() + select Status.Ready; + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.True(string.IsNullOrWhiteSpace(stderr)); + Assert.Empty(json.GetProperty("references").EnumerateArray()); + Assert.Empty(json.GetProperty("callers").EnumerateArray()); + Assert.Equal("csharp", json.GetProperty("graph_language").GetString()); + Assert.True(json.GetProperty("graph_supported").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_ExactJson_CSharpQueryRangeVariableOrderByObjectInitializerCommaDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_query_orderby_object_initializer_collision"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Key + { + public int A { get; set; } + public int B { get; set; } + } + + public sealed class Uses + { + public IEnumerable Read(IEnumerable items) + { + return from Status in items + orderby new Key { A = Status.Ready, B = items.Count() }, items.Count() + select Status.Ready; + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.True(string.IsNullOrWhiteSpace(stderr)); + Assert.Empty(json.GetProperty("references").EnumerateArray()); + Assert.Empty(json.GetProperty("callers").EnumerateArray()); + Assert.Equal("csharp", json.GetProperty("graph_language").GetString()); + Assert.True(json.GetProperty("graph_supported").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_ExactJson_CSharpNestedQueryBeforeParenthesizedOrderByCommaPreservesOnlyTrailingEnumReferenceBundle() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_inspect_nested_query_parenthesized_orderby_collision"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "cases.cs"), + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public static class Sink + { + public static Status Pick(object left, Status right) => right; + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Status Read(IEnumerable items, IEnumerable others) + { + return Sink.Pick(from Status in items + let nested = from x in others select x + orderby(items.Count()), nested.Count() + select Status.Ready, + Status.Ready); + } + } + """); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = RunBuiltCli([projectRoot, "--json", "--quiet"]); + var (exitCode, stdout, stderr) = RunBuiltCli(["inspect", "Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"]); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + var reference = Assert.Single(json.GetProperty("references").EnumerateArray()); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("Read", reference.GetProperty("container_name").GetString()); + Assert.Contains("Status.Ready", reference.GetProperty("context").GetString(), StringComparison.Ordinal); + Assert.True(json.GetProperty("graph_supported").GetBoolean()); + Assert.False(json.TryGetProperty("graph_degraded", out _)); + Assert.False(json.TryGetProperty("unsupported_symbol_kind", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_ExactJson_CSharpParameterNamedLikeEnumDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_parameter_collision"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public int Read(Holder Status) + { + return Status.Ready; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Empty(json.GetProperty("references").EnumerateArray()); + Assert.Empty(json.GetProperty("callers").EnumerateArray()); + Assert.Equal("csharp", json.GetProperty("graph_language").GetString()); + Assert.True(json.GetProperty("graph_supported").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_ExactJson_CSharpQueryRangeVariableGenericSelectorDoesNotLeakEnumReferenceBundle() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_inspect_query_generic_selector"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public static class Sink + { + public static int Wrap(int value) => value; + } + + public sealed class Uses + { + public IEnumerable Read(IEnumerable items) + { + return from Status in items + select Sink.Wrap(Status.Ready); + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(2, json.GetProperty("definitions").GetArrayLength()); + Assert.Empty(json.GetProperty("references").EnumerateArray()); + Assert.True(json.GetProperty("graph_supported").GetBoolean()); + Assert.False(json.TryGetProperty("graph_degraded", out _)); + Assert.False(json.TryGetProperty("unsupported_symbol_kind", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_ExactJson_CSharpQueryRangeVariableShiftSelectorPreservesOnlyTrailingEnumReferenceBundle() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_inspect_query_shift_selector"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public static class Sink + { + public static IEnumerable Pick(IEnumerable left, Status right) => left; + } + + public sealed class Uses + { + public IEnumerable Read(IEnumerable items) + { + return Sink.Pick( + from Status in items + select (Status.Ready << 1) >> (1 + Status.Ready), + Status.Ready); + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + var reference = Assert.Single(json.GetProperty("references").EnumerateArray()); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(2, json.GetProperty("definitions").GetArrayLength()); + Assert.Equal(28, reference.GetProperty("line").GetInt32()); + Assert.Equal("Read", reference.GetProperty("container_name").GetString()); + Assert.True(json.GetProperty("graph_supported").GetBoolean()); + Assert.False(json.TryGetProperty("graph_degraded", out _)); + Assert.False(json.TryGetProperty("unsupported_symbol_kind", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_ExactJson_CSharpQueryRangeVariableGenericTypePatternDesignationDoesNotLeakEnumReferenceBundle() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_inspect_query_generic_type_pattern_designation"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public int Read(IEnumerable items) + { + return (from Status in items + select Status is Dictionary dict ? Status.Ready : 0).First(); + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(2, json.GetProperty("definitions").GetArrayLength()); + Assert.Empty(json.GetProperty("references").EnumerateArray()); + Assert.True(json.GetProperty("graph_supported").GetBoolean()); + Assert.False(json.TryGetProperty("graph_degraded", out _)); + Assert.False(json.TryGetProperty("unsupported_symbol_kind", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_ExactJson_CSharpQueryRangeVariableGenericTypePatternWithoutDesignationDoesNotLeakEnumReferenceBundle() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_inspect_query_generic_type_pattern_without_designation"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public int Read(IEnumerable items) + { + return (from Status in items + select Status is Dictionary ? Status.Ready : 0).First(); + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(2, json.GetProperty("definitions").GetArrayLength()); + Assert.Empty(json.GetProperty("references").EnumerateArray()); + Assert.True(json.GetProperty("graph_supported").GetBoolean()); + Assert.False(json.TryGetProperty("graph_degraded", out _)); + Assert.False(json.TryGetProperty("unsupported_symbol_kind", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Theory] + [InlineData("!=")] + [InlineData("==")] + public void RunInspect_ExactJson_CSharpQueryRangeVariableGenericAsNullComparisonDoesNotLeakEnumReferenceBundle(string comparisonOperator) + { + var projectRoot = TestProjectHelper.CreateTempProject($"cdidx_query_runner_inspect_query_generic_as_null_{comparisonOperator switch { "!=" => "not_equal", "==" => "equal_equal", _ => "unknown" }}"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + $$""" + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public int Read(IEnumerable items) + { + return (from Status in items + select Status as Dictionary {{comparisonOperator}} null ? Status.Ready : 0).First(); + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(2, json.GetProperty("definitions").GetArrayLength()); + Assert.Empty(json.GetProperty("references").EnumerateArray()); + Assert.True(json.GetProperty("graph_supported").GetBoolean()); + Assert.False(json.TryGetProperty("graph_degraded", out _)); + Assert.False(json.TryGetProperty("unsupported_symbol_kind", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_ExactJson_CSharpQueryRangeVariableTupleGenericSelectorDoesNotLeakEnumReferenceBundle() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_inspect_query_tuple_generic_selector"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + using System.Collections.Generic; + using System.Linq; + + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public static class Sink + { + public static int Wrap(int value) => value; + } + + public sealed class Uses + { + public IEnumerable Read(IEnumerable items) + { + return from Status in items + select Sink.Wrap<(int, List)>(Status.Ready); + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(2, json.GetProperty("definitions").GetArrayLength()); + Assert.Empty(json.GetProperty("references").EnumerateArray()); + Assert.True(json.GetProperty("graph_supported").GetBoolean()); + Assert.False(json.TryGetProperty("graph_degraded", out _)); + Assert.False(json.TryGetProperty("unsupported_symbol_kind", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_ExactJson_CSharpSwitchExpressionDeclarationPatternWhenInCommentDoesNotLeakReferenceContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_enum_member_switch_expression_declaration_pattern_when_comment_collision"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/cases.cs", "csharp", + """ + namespace Demo; + + public enum Status + { + Ready + } + + public sealed class Holder + { + public int Ready { get; set; } + } + + public sealed class Uses + { + public Demo.Status Read(object value) + { + return value switch + { + Holder Status /* when comment */ => (Demo.Status)Status.Ready, + _ => Demo.Status.Ready + }; + } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Ready", "--db", dbPath, "--json", "--lang", "csharp", "--exact-name"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + var referenceLines = json.GetProperty("references") + .EnumerateArray() + .Select(reference => reference.GetProperty("line").GetInt32()) + .ToArray(); + var callerReferenceCounts = json.GetProperty("callers") + .EnumerateArray() + .Select(caller => caller.GetProperty("reference_count").GetInt32()) + .ToArray(); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal([20], referenceLines); + Assert.Equal([1], callerReferenceCounts); + Assert.Equal("csharp", json.GetProperty("graph_language").GetString()); + Assert.True(json.GetProperty("graph_supported").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_Json_CrossLanguageMixedHitPrefersGraphCapablePrimaryDefinition() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_inspect_cross_language_primary_alignment_non_exact"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "web/app.js", "javascript", + """ + function Ready() {} + + function Helper() {} + + Ready(); + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/status.cs", "csharp", + """ + namespace Demo; + + public enum Status + { + Ready + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Ready", "--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + var nearbyPaths = json.GetProperty("nearby_symbols") + .EnumerateArray() + .Select(symbol => symbol.GetProperty("path").GetString()) + .Where(path => path != null) + .Distinct(StringComparer.Ordinal) + .ToList(); + var references = json.GetProperty("references").EnumerateArray().ToList(); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("javascript", json.GetProperty("graph_language").GetString()); + Assert.True(json.GetProperty("graph_supported").GetBoolean()); + Assert.False(json.TryGetProperty("graph_degraded", out _)); + Assert.False(json.TryGetProperty("unsupported_symbol_kind", out _)); + Assert.Equal("web/app.js", json.GetProperty("file").GetProperty("path").GetString()); + Assert.Contains("web/app.js", nearbyPaths); + Assert.DoesNotContain("src/status.cs", nearbyPaths); + Assert.Contains(json.GetProperty("nearby_symbols").EnumerateArray(), + symbol => symbol.GetProperty("name").GetString() == "Helper"); + Assert.NotEmpty(references); + Assert.All(references, reference => + Assert.Equal("javascript", reference.GetProperty("lang").GetString())); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_ExactJson_PathScopeDoesNotInheritOutOfScopeEnumMemberMetadata() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_inspect_scoped_js"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "web/app.js", "javascript", + """ + function Ready() { + } + + Ready(); + """); + TestProjectHelper.InsertIndexedFile(dbPath, "cs/status.cs", "csharp", + """ + public enum Status { Ready } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Ready", "--db", dbPath, "--json", "--exact-name", "--path", "web/"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("javascript", json.GetProperty("file").GetProperty("lang").GetString()); + Assert.Equal("javascript", json.GetProperty("graph_language").GetString()); + Assert.True(json.GetProperty("graph_supported").GetBoolean()); + Assert.False(json.TryGetProperty("graph_degraded", out _)); + Assert.False(json.TryGetProperty("unsupported_symbol_kind", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_JsonKeepsSubscribeCalleesVisibleForCallerSymbols() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_inspect_subscribe_callee_bundle"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Publisher.cs", "csharp", + """ + using System; + + public class Publisher + { + public event EventHandler? Changed; + } + """); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Subscriber.cs", "csharp", + """ + using System; + + public class Subscriber + { + public void Hook(Publisher publisher) + { + publisher.Changed += OnChanged; + } + + private void OnChanged(object? sender, EventArgs e) { } + } + """); + MarkGraphAndFoldReady(dbPath); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Hook", "--db", dbPath, "--json", "--lang", "csharp", "--exact"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + var callee = Assert.Single(json.GetProperty("callees").EnumerateArray()); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("Hook", callee.GetProperty("caller_name").GetString()); + Assert.Equal("Changed", callee.GetProperty("callee_name").GetString()); + Assert.Equal("event", callee.GetProperty("reference_kind").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_NonExactJsonOnReadOnlyLegacyDb_OmitsExactDegradedFields() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_inspect_nonexact"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/session.py", "python", "def login(user, password):\n return Run(user)\n"); + DropGraphExactFallbackIndexes(dbPath); + + var readOnlyUri = new Uri(dbPath).AbsoluteUri + "?immutable=1"; + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Run", "--db", readOnlyUri, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.False(json.TryGetProperty("exact_index_available", out _)); + Assert.False(json.TryGetProperty("degraded_reason", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_WithJsonIncludesWorkspaceMetadataForCustomDbUnderCdidx() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_inspect_custom_db"); + var dbContainerRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_inspect_custom_container"); + var dbPath = Path.Combine(dbContainerRoot, ".cdidx", "shared.db"); + try + { + TestProjectHelper.InitializeGitRepo(projectRoot); + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText(Path.Combine(projectRoot, "src", "app.cs"), "class App {}\n"); + TestProjectHelper.RunGit(projectRoot, "add", "src/app.cs"); + TestProjectHelper.RunGit(projectRoot, "commit", "-m", "initial"); + + var expectedHead = TestProjectHelper.RunGit(projectRoot, "rev-parse", "HEAD").Trim(); + Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!); + using (var db = new DbContext(dbPath)) + { + db.InitializeSchema(); + var writer = new DbWriter(db.Connection); + writer.SetMeta(DbContext.IndexedProjectRootMetaKey, projectRoot); + } + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", "class App {}\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["App", "--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(projectRoot, json.GetProperty("project_root").GetString()); + Assert.Equal(expectedHead, json.GetProperty("git_head").GetString()); + Assert.False(json.GetProperty("git_is_dirty").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + TestProjectHelper.DeleteDirectory(dbContainerRoot); + } + } + + [Fact] + public void RunInspect_WithJsonUsesProjectLocalCdidxPathForExplicitProjectDb() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_inspect_project_local_explicit"); + var staleRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_inspect_project_local_stale"); + try + { + TestProjectHelper.InitializeGitRepo(projectRoot); + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText(Path.Combine(projectRoot, "src", "app.cs"), "class App {}\n"); + TestProjectHelper.RunGit(projectRoot, "add", "src/app.cs"); + TestProjectHelper.RunGit(projectRoot, "commit", "-m", "initial"); + var expectedHead = TestProjectHelper.RunGit(projectRoot, "rev-parse", "HEAD").Trim(); + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + + using (var db = new DbContext(dbPath)) + { + var writer = new DbWriter(db.Connection); + writer.SetMeta(DbContext.IndexedProjectRootMetaKey, staleRoot); + } + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", "class App {}\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["App", "--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(projectRoot, json.GetProperty("project_root").GetString()); + Assert.Equal(expectedHead, json.GetProperty("git_head").GetString()); + Assert.False(json.GetProperty("git_is_dirty").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + TestProjectHelper.DeleteDirectory(staleRoot); + } + } + + [Fact] + public void RunInspect_WithJsonIncludesWorkspaceMetadataForExplicitExternalCodeIndexDb() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_inspect_codeindex_db"); + var dbContainerRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_inspect_codeindex_container"); + var dbPath = Path.Combine(dbContainerRoot, ".cdidx", "codeindex.db"); + try + { + TestProjectHelper.InitializeGitRepo(projectRoot); + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText(Path.Combine(projectRoot, "src", "app.cs"), "class App {}\n"); + TestProjectHelper.RunGit(projectRoot, "add", "src/app.cs"); + TestProjectHelper.RunGit(projectRoot, "commit", "-m", "initial"); + + var expectedHead = TestProjectHelper.RunGit(projectRoot, "rev-parse", "HEAD").Trim(); + Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!); + using (var db = new DbContext(dbPath)) + { + db.InitializeSchema(); + var writer = new DbWriter(db.Connection); + writer.SetMeta(DbContext.IndexedProjectRootMetaKey, projectRoot); + } + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", "class App {}\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["App", "--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(projectRoot, json.GetProperty("project_root").GetString()); + Assert.Equal(expectedHead, json.GetProperty("git_head").GetString()); + Assert.False(json.GetProperty("git_is_dirty").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + TestProjectHelper.DeleteDirectory(dbContainerRoot); + } + } + + [Fact] + public void RunInspect_ExactOnReadOnlyLegacyDb_WithMissingSymbolFallbackIndex_WarnsAtBundleLevel() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_inspect_symbol_exact_warn"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/session.py", + "python", + "def Run(user):\n return user\n\ndef login(user, password):\n return Run(user)\n"); + ForceLegacyExactFallbackMode(dbPath); + DropSymbolExactFallbackIndex(dbPath); + + var readOnlyUri = new Uri(dbPath).AbsoluteUri + "?immutable=1"; + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Run", "--db", readOnlyUri, "--exact"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Contains("Exact Index : DEGRADED", stdout); + Assert.Contains("idx_symbols_name_nocase", stdout); + Assert.Contains("WARN: --exact inspect bundle ran without all supporting indexes", stderr); + Assert.Contains("idx_symbols_name_nocase", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_ExactOnReadOnlyLegacyDb_WithMissingSymbolIndexAndGraphTable_StillWarnsAboutIndex() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_inspect_symbol_and_table_missing"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/session.py", + "python", + "def Run(user):\n return user\n\ndef login(user, password):\n return Run(user)\n"); + ForceLegacyExactFallbackMode(dbPath); + DropSymbolExactFallbackIndex(dbPath); + DropGraphTable(dbPath); + + var readOnlyUri = new Uri(dbPath).AbsoluteUri + "?immutable=1"; + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Run", "--db", readOnlyUri, "--exact"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Contains("Graph Table : MISSING", stdout); + Assert.Contains("Exact Index : DEGRADED", stdout); + Assert.Contains("idx_symbols_name_nocase", stdout); + Assert.Contains("WARN: --exact inspect bundle ran without all supporting indexes", stderr); + Assert.Contains("idx_symbols_name_nocase", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_ExactOnReadOnlyLegacyDb_UnsupportedGraphLanguage_DoesNotReportFalseDegradedSignal() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_inspect_markdown_exact_ok"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "docs/guide.md", + "markdown", + "# Heading\n\nSee also `Run`.\n"); + ForceLegacyExactFallbackMode(dbPath); + DropGraphExactFallbackIndexes(dbPath); + + var readOnlyUri = new Uri(dbPath).AbsoluteUri + "?immutable=1"; + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Heading", "--db", readOnlyUri, "--exact", "--lang", "markdown", "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.False(json.GetProperty("graph_supported").GetBoolean()); + Assert.True(json.GetProperty("exact_index_available").GetBoolean()); + Assert.False(json.TryGetProperty("degraded_reason", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_ExactOnReadOnlyLegacyDb_PathOnlyUnsupportedSlice_DoesNotReportFalseDegradedSignal() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_inspect_path_only_exact_ok"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "docs/guide.md", + "markdown", + "# Heading\n\nSee also `Run`.\n"); + ForceLegacyExactFallbackMode(dbPath); + DropGraphExactFallbackIndexes(dbPath); + + var readOnlyUri = new Uri(dbPath).AbsoluteUri + "?immutable=1"; + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["Run", "--db", readOnlyUri, "--exact", "--path", "docs/", "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.True(json.GetProperty("exact_index_available").GetBoolean()); + Assert.False(json.TryGetProperty("degraded_reason", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_PrefersExactDefinitionFileWhenSubstringDefinitionsOverlap() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_inspect_exact_anchor"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/Services/ILoggerService.cs", + "csharp", + """ + public interface ILoggerService + { + void Log(string message); + } + """); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/Services/LoggerService.cs", + "csharp", + """ + public class LoggerService : ILoggerService + { + public void Log(string message) { } + public void Execute() { } + } + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["loggerservice", "--db", dbPath, "--lang", "csharp", "--limit", "1"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Contains("File : src/Services/LoggerService.cs", stdout); + Assert.Contains("class LoggerService", stdout); + Assert.DoesNotContain("File : src/Services/ILoggerService.cs", stdout); + Assert.DoesNotContain("interface ILoggerService", stdout); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_ExactZeroHumanOutput_PrintsExactZeroHint() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_inspect_exact_zero"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/app.cs", + "csharp", + """ + public class App + { + public void HandleRequest() { } + public void HandleRequestAsync() { HandleRequest(); } + } + """); + using (var db = new DbContext(dbPath)) + { + var writer = new DbWriter(db.Connection); + writer.MarkGraphReady(); + } + + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["HandleRe", "--db", dbPath, "--exact"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Contains("--exact found 0 matches, but substring matching would return 2", stderr); + Assert.Contains("`HandleRequest`", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunInspect_BlankQueryReturnsDistinctUsageError() + { + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + [" "], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("Error: inspect query cannot be empty or whitespace-only", stderr); + Assert.DoesNotContain("inspect requires a symbol query argument", stderr); + Assert.Contains("empty or whitespace-only arguments", stderr); + Assert.Contains($"Usage: {ConsoleUi.GetUsageLine("inspect")}", stderr); + } + + [Fact] + public void RunInspect_BareVerbatimQueryWithExactReturnsUsageError() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_inspect_bare_verbatim_exact"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["@", "--db", dbPath, "--exact", "--json"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("bare verbatim prefixes", stderr); + Assert.Equal(string.Empty, stdout); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunOutline_WithJson_JavaModuleInfoWithMultilineDirectivesIncludesImports() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_outline_java_module_multiline"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src", "main", "java")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "main", "java", "module-info.java"), + """ + module com.example.app { + requires /*comment*/ java.base; + exports com.example.internal + to com.example.plugin, + com.example.tools; + opens com.example.model + to com.example.viewer, + com.example.editor; + uses com.example.spi.MyService; + provides com.example.spi.MyService + with com.example.impl.DefaultService, + com.example.impl.BackupService; + } + """); + + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunOutline( + ["src/main/java/module-info.java", "--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var symbols = document.RootElement.GetProperty("symbols").EnumerateArray().ToList(); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(6, symbols.Count); + Assert.Contains(symbols, symbol => symbol.GetProperty("kind").GetString() == "import" + && symbol.GetProperty("name").GetString() == "java.base"); + Assert.Contains(symbols, symbol => symbol.GetProperty("kind").GetString() == "import" + && symbol.GetProperty("name").GetString() == "com.example.internal"); + Assert.Contains(symbols, symbol => symbol.GetProperty("kind").GetString() == "import" + && symbol.GetProperty("name").GetString() == "com.example.model"); + Assert.Equal(2, symbols.Count(symbol => symbol.GetProperty("kind").GetString() == "import" + && symbol.GetProperty("name").GetString() == "com.example.spi.MyService")); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunOutline_WithJson_JavaModuleInfoWithAllmanBraceLineDirectiveIncludesImports() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_outline_java_module_allman_brace_line"); + try + { + File.WriteAllText( + Path.Combine(projectRoot, "module-info.java"), + """ + module com.example.app + { requires java.base; + exports com.example.api; + } + """); + + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunOutline( + ["module-info.java", "--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var symbols = document.RootElement.GetProperty("symbols").EnumerateArray().ToList(); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(3, symbols.Count); + Assert.Contains(symbols, symbol => symbol.GetProperty("kind").GetString() == "import" + && symbol.GetProperty("name").GetString() == "java.base"); + Assert.Contains(symbols, symbol => symbol.GetProperty("kind").GetString() == "import" + && symbol.GetProperty("name").GetString() == "com.example.api"); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } +} diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerMapTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerMapTests.cs new file mode 100644 index 0000000000..017063cda7 --- /dev/null +++ b/tests/CodeIndex.Tests/QueryCommandRunnerMapTests.cs @@ -0,0 +1,445 @@ +using System.Text.Json; +using CodeIndex.Cli; +using CodeIndex.Database; +using CodeIndex.Models; +using Microsoft.Data.Sqlite; + +namespace CodeIndex.Tests; + +public partial class QueryCommandRunnerTests +{ + [Fact] + public void RunMap_ParseSectionsAndDepth_StoresSelectors() + { + var options = QueryCommandRunner.ParseArgs( + ["--json", "--sections", "tree,languages", "--depth", "2"], + jsonDefault: false, + validateDefaultSnippetLines: false, + validateDefaultMaxLineWidth: false); + + Assert.True(options.Json); + Assert.Equal(["tree", "languages"], options.MapSections); + Assert.True(options.ContextAfterExplicit); + Assert.Equal(2, options.ContextAfter); + Assert.Null(options.ParseError); + } + + [Fact] + public void RunMap_WithJsonIncludesWorkspaceMetadataForProjectDb() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_map"); + try + { + TestProjectHelper.InitializeGitRepo(projectRoot); + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText(Path.Combine(projectRoot, "src", "app.cs"), "class App {}\n"); + TestProjectHelper.RunGit(projectRoot, "add", "src/app.cs"); + TestProjectHelper.RunGit(projectRoot, "commit", "-m", "initial"); + + var expectedHead = TestProjectHelper.RunGit(projectRoot, "rev-parse", "HEAD").Trim(); + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", "class App {}\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunMap( + ["--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(projectRoot, json.GetProperty("project_root").GetString()); + Assert.Equal(expectedHead, json.GetProperty("git_head").GetString()); + Assert.False(json.GetProperty("git_is_dirty").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunMap_WithJson_CSharpRawStringFixturesDoNotCreatePhantomEntrypoints() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_map_raw_string"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/fixture.cs", + "csharp", + """" + public class FixtureHost + { + public void UsesRawFixture() + { + const string fixture = """ + function main() + end + + public class App + { + } + """; + } + } + """"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunMap( + ["--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var entrypoints = document.RootElement.GetProperty("entrypoints"); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Empty(entrypoints.EnumerateArray()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunMap_WithJson_JavaModuleInfoUsesModuleDeclarationAsModuleKey() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_map_java_module"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "com", "example", "app")); + File.WriteAllText( + Path.Combine(projectRoot, "module-info.java"), + """ + module com.example.app { + requires java.base; + exports com.example.api; + } + """); + File.WriteAllText( + Path.Combine(projectRoot, "com", "example", "app", "App.java"), + """ + package com.example.app; + + public class App + { + public static void main(String[] args) {} + } + """); + + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunMap( + ["--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var modules = document.RootElement.GetProperty("modules").EnumerateArray().ToList(); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + var javaModule = Assert.Single(modules.Where(module => module.GetProperty("module").GetString() == "com.example.app")); + Assert.Equal(2, javaModule.GetProperty("files").GetInt32()); + Assert.DoesNotContain(modules, module => module.GetProperty("module").GetString() == "com"); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunMap_WithJson_JavaModuleInfoWithAllmanBraceUsesModuleDeclarationAsModuleKey() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_map_java_module_allman"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "com", "example", "app")); + File.WriteAllText( + Path.Combine(projectRoot, "module-info.java"), + """ + module com.example.app + { + requires java.base; + exports com.example.api; + } + """); + File.WriteAllText( + Path.Combine(projectRoot, "com", "example", "app", "App.java"), + """ + package com.example.app; + + public class App + { + public static void main(String[] args) {} + } + """); + + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunMap( + ["--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var modules = document.RootElement.GetProperty("modules").EnumerateArray().ToList(); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + var javaModule = Assert.Single(modules.Where(module => module.GetProperty("module").GetString() == "com.example.app")); + Assert.Equal(2, javaModule.GetProperty("files").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunMap_WithJson_NonJavaNamespaceDoesNotOverridePathBasedModuleKey() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_map_non_java_namespace"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src", "App")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "App", "App.cs"), + """ + namespace My.Company.App; + + public class App {} + """); + + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunMap( + ["--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var modules = document.RootElement.GetProperty("modules").EnumerateArray().ToList(); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Contains(modules, module => module.GetProperty("module").GetString() == "src/App"); + Assert.DoesNotContain(modules, module => module.GetProperty("module").GetString() == "My.Company.App"); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunMap_WithJson_PathFilteredJavaModuleFileKeepsModuleDeclarationAsModuleKey() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_map_java_module_filtered"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "com", "example", "app")); + File.WriteAllText( + Path.Combine(projectRoot, "module-info.java"), + """ + module com.example.app { + requires java.base; + exports com.example.api; + } + """); + File.WriteAllText( + Path.Combine(projectRoot, "com", "example", "app", "App.java"), + """ + package com.example.app; + + public class App + { + public static void main(String[] args) {} + } + """); + + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunMap( + ["--db", dbPath, "--json", "--path", "com/example/app/App.java"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var modules = document.RootElement.GetProperty("modules").EnumerateArray().ToList(); + var topFiles = document.RootElement.GetProperty("top_files").EnumerateArray().ToList(); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + var javaModule = Assert.Single(modules); + Assert.Equal("com.example.app", javaModule.GetProperty("module").GetString()); + Assert.Equal(1, javaModule.GetProperty("files").GetInt32()); + var topFile = Assert.Single(topFiles); + Assert.Equal("com/example/app/App.java", topFile.GetProperty("path").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunMap_WithJsonIncludesWorkspaceMetadataForCustomDbUnderCdidx() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_map_custom_db"); + var dbContainerRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_map_custom_container"); + var dbPath = Path.Combine(dbContainerRoot, ".cdidx", "shared.db"); + try + { + TestProjectHelper.InitializeGitRepo(projectRoot); + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText(Path.Combine(projectRoot, "src", "app.cs"), "class App {}\n"); + TestProjectHelper.RunGit(projectRoot, "add", "src/app.cs"); + TestProjectHelper.RunGit(projectRoot, "commit", "-m", "initial"); + + var expectedHead = TestProjectHelper.RunGit(projectRoot, "rev-parse", "HEAD").Trim(); + Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!); + using (var db = new DbContext(dbPath)) + { + db.InitializeSchema(); + var writer = new DbWriter(db.Connection); + writer.SetMeta(DbContext.IndexedProjectRootMetaKey, projectRoot); + } + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", "class App {}\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunMap( + ["--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(projectRoot, json.GetProperty("project_root").GetString()); + Assert.Equal(expectedHead, json.GetProperty("git_head").GetString()); + Assert.False(json.GetProperty("git_is_dirty").GetBoolean()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + TestProjectHelper.DeleteDirectory(dbContainerRoot); + } + } + + [Fact] + public void RunMap_NonexistentPathReturnsNotFound() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_map_notfound"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", "class App {}\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunMap( + ["--db", dbPath, "--path", "nonexistent/"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Contains("No files found", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunMap_NonexistentPathJsonReturnsNotFoundWithPayload() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_map_notfound_json"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", "class App {}\n"); + + var (exitCode, stdout, _) = CaptureConsole(() => QueryCommandRunner.RunMap( + ["--db", dbPath, "--path", "nonexistent/", "--json"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + using var document = ParseJsonOutput(stdout); + Assert.Equal(0, document.RootElement.GetProperty("file_count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunMap_EmptyDbWithoutFiltersReturnsSuccess() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_map_empty_ok"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + // No files inserted — empty but valid index / ファイル未挿入 — 空だが有効なインデックス + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunMap( + ["--db", dbPath], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Contains("Files : 0", stdout); + Assert.Equal(string.Empty, stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunMap_HumanLargestFilesFormatsSizesAndBytesFlagKeepsRawCounts() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_map_size_units"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/big.cs", "csharp", "class Big {}\n"); + SetIndexedFileSize(dbPath, "src/big.cs", 5L * 1024 * 1024 * 1024); + + var (formattedExit, formattedStdout, formattedStderr) = CaptureConsole(() => QueryCommandRunner.RunMap( + ["--db", dbPath], + _jsonOptions)); + var (rawExit, rawStdout, rawStderr) = CaptureConsole(() => QueryCommandRunner.RunMap( + ["--db", dbPath, "--bytes"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, formattedExit); + Assert.Equal(CommandExitCodes.Success, rawExit); + Assert.Equal(string.Empty, formattedStderr); + Assert.Equal(string.Empty, rawStderr); + Assert.Contains("Largest files:", formattedStdout); + Assert.Contains("src/big.cs", formattedStdout); + Assert.Contains("5.0 GiB", formattedStdout); + Assert.Contains("5368709120 bytes", rawStdout); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } +} diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerSearchGuardTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerSearchGuardTests.cs new file mode 100644 index 0000000000..db0ad5cbb3 --- /dev/null +++ b/tests/CodeIndex.Tests/QueryCommandRunnerSearchGuardTests.cs @@ -0,0 +1,239 @@ +using CodeIndex.Cli; +using CodeIndex.Database; +using CodeIndex.Models; + +namespace CodeIndex.Tests; + +public partial class QueryCommandRunnerTests +{ + [Fact] + public void ParseArgs_ParsesSearchGuardFlags_Issue2852() + { + var options = QueryCommandRunner.ParseArgs( + [ + "RunSearch", + "--require-before", "Length", + "--require-after", "File.Move", + "--reject-before", "NoSizeCap", + "--reject-after", "FileMode.Create", + "--guard-window", "12", + ], jsonDefault: true); + + Assert.Equal("RunSearch", options.Query); + Assert.Equal(4, options.GuardFilters.Count); + Assert.Equal(SearchGuardRole.Require, options.GuardFilters[0].Role); + Assert.Equal(SearchGuardDirection.Before, options.GuardFilters[0].Direction); + Assert.Equal("Length", options.GuardFilters[0].Query); + Assert.Equal(SearchGuardRole.Require, options.GuardFilters[1].Role); + Assert.Equal(SearchGuardDirection.After, options.GuardFilters[1].Direction); + Assert.Equal("File.Move", options.GuardFilters[1].Query); + Assert.Equal(SearchGuardRole.Reject, options.GuardFilters[2].Role); + Assert.Equal(SearchGuardDirection.Before, options.GuardFilters[2].Direction); + Assert.Equal("NoSizeCap", options.GuardFilters[2].Query); + Assert.Equal(SearchGuardRole.Reject, options.GuardFilters[3].Role); + Assert.Equal(SearchGuardDirection.After, options.GuardFilters[3].Direction); + Assert.Equal("FileMode.Create", options.GuardFilters[3].Query); + Assert.Equal(12, options.GuardWindow); + } + + [Fact] + public void RunSearch_GuardFiltersReturnUnguardedCallSites_Issue2852() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_guard_filters"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/app.cs", + "csharp", + """ + using System.IO; + + public class App + { + public void Guarded(string path) + { + var length = new FileInfo(path).Length; + var text = File.ReadAllText(path); + } + + public void Unguarded(string path) + { + var text = File.ReadAllText(path); + } + } + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["File.ReadAllText", "--db", dbPath, "--exact-substring", "--reject-before", "Length", "--guard-window", "2", "--json=array"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + using var document = ParseJsonOutput(stdout); + var row = Assert.Single(document.RootElement.EnumerateArray()); + Assert.Equal("src/app.cs", row.GetProperty("path").GetString()); + Assert.Equal(13, row.GetProperty("chunk_start_line").GetInt32()); + Assert.Equal(" var text = File.ReadAllText(path);", row.GetProperty("snippet").GetString()); + Assert.False(row.TryGetProperty("guard_evidence", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_GuardFiltersRequireAllNonExactTokensOnFocusLine_Issue2852() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_guard_non_exact_terms"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/app.cs", + "csharp", + """ + using System.IO; + + public class App + { + public void Guarded(string path) + { + var length = new FileInfo(path).Length; + var text = File.ReadAllText(path); + } + + public void Unguarded(string path) + { + var text = File.ReadAllText(path); + } + } + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["File ReadAllText", "--db", dbPath, "--reject-before", "Length", "--guard-window", "2", "--json=array"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + using var document = ParseJsonOutput(stdout); + var row = Assert.Single(document.RootElement.EnumerateArray()); + Assert.Equal("src/app.cs", row.GetProperty("path").GetString()); + Assert.Equal(13, row.GetProperty("chunk_start_line").GetInt32()); + Assert.Equal(" var text = File.ReadAllText(path);", row.GetProperty("snippet").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_GuardRequireEvidenceAppearsInJson_Issue2852() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_guard_require_json"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/app.cs", + "csharp", + """ + using System.IO; + + public class App + { + public void Atomic(string path, string tempPath) + { + using var stream = new FileStream(path, FileMode.Create); + File.Move(tempPath, path, overwrite: true); + } + + public void NonAtomic(string path) + { + using var stream = new FileStream(path, FileMode.Create); + } + } + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["FileMode.Create", "--db", dbPath, "--exact-substring", "--require-after", "File.Move", "--guard-window", "2", "--json=array"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + using var document = ParseJsonOutput(stdout); + var row = Assert.Single(document.RootElement.EnumerateArray()); + Assert.Equal(7, row.GetProperty("chunk_start_line").GetInt32()); + var evidence = Assert.Single(row.GetProperty("guard_evidence").EnumerateArray()); + Assert.Equal("require", evidence.GetProperty("role").GetString()); + Assert.Equal("after", evidence.GetProperty("direction").GetString()); + Assert.Equal("File.Move", evidence.GetProperty("query").GetString()); + Assert.Equal(8, evidence.GetProperty("line").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_GuardFiltersApplyToCount_Issue2852() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_guard_count"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/app.cs", + "csharp", + """ + using System.IO; + + public class App + { + public void Guarded(string path) + { + var length = new FileInfo(path).Length; + var text = File.ReadAllText(path); + } + + public void Unguarded(string path) + { + var text = File.ReadAllText(path); + } + } + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["File.ReadAllText", "--db", dbPath, "--exact-substring", "--reject-before", "Length", "--guard-window", "2", "--count"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal("1", stdout.Trim()); + Assert.Equal(string.Empty, stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void SearchUsageLineListsGuardFlags_Issue2852() + { + var usage = ConsoleUi.GetUsageLine("search"); + + Assert.NotNull(usage); + Assert.Contains("--require-before ", usage); + Assert.Contains("--require-after ", usage); + Assert.Contains("--reject-before ", usage); + Assert.Contains("--reject-after ", usage); + Assert.Contains("--guard-window ", usage); + } +} diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerSearchHintTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerSearchHintTests.cs new file mode 100644 index 0000000000..adf04d51c3 --- /dev/null +++ b/tests/CodeIndex.Tests/QueryCommandRunnerSearchHintTests.cs @@ -0,0 +1,101 @@ +using System.Text.Json; +using CodeIndex.Cli; + +namespace CodeIndex.Tests; + +public partial class QueryCommandRunnerTests +{ + [Fact] + public void RunSearch_ExactSubstringJsonOutputsLiteralHighlightMetadata() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_search_exact_literal_highlight"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/sql.cs", + "csharp", + "var CommandText = $\"SELECT 1\";\nvar CommandText = other;"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["CommandText = $", "--db", dbPath, "--json", "--exact-substring", "--snippet-lines", "2"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var highlight = document.RootElement.GetProperty("highlights")[0]; + var literalOccurrence = highlight.GetProperty("literal_term_occurrences")[0]; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("CommandText = $", highlight.GetProperty("literal_terms")[0].GetString()); + Assert.Equal("CommandText = $", literalOccurrence.GetProperty("term").GetString()); + Assert.Equal(1, literalOccurrence.GetProperty("line").GetInt32()); + Assert.Equal(5, literalOccurrence.GetProperty("column").GetInt32()); + Assert.Equal("CommandText = $".Length, literalOccurrence.GetProperty("length").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_PunctuationHeavyTextSuggestsExactSubstring() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_search_exact_substring_hint_text"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/sql.cs", + "csharp", + "var CommandText = $\"SELECT 1\";\nvar CommandText = other;"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["CommandText = $", "--db", dbPath, "--limit", "1"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Contains("src/sql.cs", stdout); + Assert.Contains("Hint: This looks like a literal code phrase; try --exact-substring for punctuation-sensitive matching.", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_PunctuationHeavyJsonAddsExactSubstringHint() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_search_exact_substring_hint_json"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/sql.cs", + "csharp", + "var CommandText = $\"SELECT 1\";\nvar CommandText = other;"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["CommandText = $", "--db", dbPath, "--json", "--limit", "1"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var hint = document.RootElement.GetProperty("exact_substring_hint"); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("punctuation_heavy_query", hint.GetProperty("reason").GetString()); + Assert.Equal("--exact-substring", hint.GetProperty("flag").GetString()); + Assert.Equal("exactSubstring", hint.GetProperty("mcp_argument").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } +} diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs new file mode 100644 index 0000000000..02517f02c6 --- /dev/null +++ b/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs @@ -0,0 +1,3401 @@ +using System.Text.Json; +using CodeIndex.Cli; +using CodeIndex.Database; +using CodeIndex.Models; +using Microsoft.Data.Sqlite; + +namespace CodeIndex.Tests; + +public partial class QueryCommandRunnerTests +{ + [Fact] + public void RunSearch_FormatCompactEmitsFileLineOnly_Issue1642() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_format_compact"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/app.cs", + "csharp", + "public class App { void Run() { Authenticate(); } }"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["Authenticate", "--db", dbPath, "--format", "compact"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + using var document = ParseJsonOutput(stdout); + var row = Assert.Single(document.RootElement.EnumerateArray()); + Assert.Equal("src/app.cs", row.GetProperty("file").GetString()); + Assert.True(row.GetProperty("line").GetInt32() > 0); + Assert.False(row.TryGetProperty("snippet", out _)); + Assert.False(row.TryGetProperty("name", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_FormatCsvEmitsDelimitedRows_Issue1941() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_format_csv"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/app.cs", + "csharp", + "public class App { void Run() { Authenticate(); } }"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["Authenticate", "--db", dbPath, "--format", "csv"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + var lines = stdout.Trim().Split(Environment.NewLine); + Assert.Equal("file,line,column,label", lines[0]); + Assert.Contains("src/app.cs", lines[1]); + Assert.Contains("search match: Authenticate", lines[1]); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_EmitsVisibilityInJsonAndHumanOutput_Issue1868() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_visibility_output"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/private-auth.cs", + "csharp", + """ + public class AuthFixture + { + private void Authenticate() { } + } + """); + + var (jsonExitCode, jsonStdout, jsonStderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["Authenticate", "--db", dbPath, "--lang", "csharp", "--exact", "--json"], + _jsonOptions)); + var (humanExitCode, humanStdout, humanStderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["Authenticate", "--db", dbPath, "--lang", "csharp", "--exact"], + _jsonOptions)); + + using var document = ParseJsonOutput(jsonStdout); + + Assert.Equal(CommandExitCodes.Success, jsonExitCode); + Assert.Equal(string.Empty, jsonStderr); + Assert.Equal("private", document.RootElement.GetProperty("visibility").GetString()); + Assert.Equal(CommandExitCodes.Success, humanExitCode); + Assert.Contains("src/private-auth.cs:1-4 [private]", humanStdout); + Assert.Contains("1 results in 1 files", humanStderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_JsonArrayEmitsSingleArray_Issue1850() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_json_array"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/auth.cs", + "csharp", + """ + public class AuthFixture + { + public void Authenticate() { } + } + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["Authenticate", "--db", dbPath, "--lang", "csharp", "--exact", "--json=array"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + using var document = JsonDocument.Parse(stdout); + Assert.Equal(JsonValueKind.Array, document.RootElement.ValueKind); + Assert.Single(document.RootElement.EnumerateArray()); + Assert.DoesNotContain("\"done\"", stdout, StringComparison.Ordinal); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_JsonArrayNoResultsEmitsEmptyArray_Issue1850() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_json_array_empty"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/auth.cs", + "csharp", + "public class AuthFixture { }\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["Missing", "--db", dbPath, "--json=array"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + using var document = JsonDocument.Parse(stdout); + Assert.Equal(JsonValueKind.Array, document.RootElement.ValueKind); + Assert.Empty(document.RootElement.EnumerateArray()); + Assert.DoesNotContain("\"done\"", stdout, StringComparison.Ordinal); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_StrictNotFoundReturnsNotFoundForZeroResults_Issue1425() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_strict_not_found"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/auth.cs", + "csharp", + "public class AuthFixture { }\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["Missing", "--db", dbPath, "--strict-not-found"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.NotFound, exitCode); + Assert.Equal(string.Empty, stdout); + Assert.Contains("No results found", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_JsonFormatRejectsUnknownValue_Issue1850() + { + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["Authenticate", "--json=pretty"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("--json format must be one of ndjson or array", stderr); + } + + [Fact] + public void RunSearch_ProfileEmitsSqlPhasesAndQueryPlan_Issue1643() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_profile"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/auth.cs", + "csharp", + """ + public class AuthFixture + { + public void Authenticate() { } + } + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["Authenticate", "--db", dbPath, "--json", "--profile", "--slow-query-ms", "0"], + _jsonOptions)); + var rawLines = stdout.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var lines = stdout + .Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Where(line => + { + using var document = JsonDocument.Parse(line); + return !IsJsonStreamDoneSentinel(document.RootElement); + }) + .ToArray(); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + using (var doneDocument = JsonDocument.Parse(rawLines[^1])) + { + Assert.True(IsJsonStreamDoneSentinel(doneDocument.RootElement)); + } + Assert.Equal(2, lines.Length); + + using var resultDocument = JsonDocument.Parse(lines[0]); + Assert.Equal("src/auth.cs", resultDocument.RootElement.GetProperty("path").GetString()); + + using var profileDocument = JsonDocument.Parse(lines[1]); + var profile = profileDocument.RootElement.GetProperty("profile"); + var phases = profile.GetProperty("phases"); + var queryPlan = profile.GetProperty("query_plan"); + var queries = profile.GetProperty("queries"); + + Assert.True(phases.GetArrayLength() > 0); + Assert.True(queryPlan.GetArrayLength() > 0); + Assert.True(queries.GetArrayLength() > 0); + Assert.Equal("sql_1", phases[0].GetProperty("name").GetString()); + Assert.True(phases[0].GetProperty("elapsed_ms").GetDouble() >= 0); + Assert.True(phases[0].GetProperty("rows_scanned").GetInt32() >= 0); + Assert.False(string.IsNullOrWhiteSpace(queryPlan[0].GetProperty("detail").GetString())); + Assert.Contains(queries.EnumerateArray(), query => + query.GetProperty("sql").GetString()?.Contains("SELECT", StringComparison.OrdinalIgnoreCase) == true); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_VerboseEmitsDebugToStderrOnly_Issue1899() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_verbose"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/auth.cs", + "csharp", + """ + public class AuthFixture + { + public void Authenticate() { } + } + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["Authenticate", "--db", dbPath, "--verbose"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Contains("src/auth.cs", stdout); + Assert.Contains("DEBUG query: sql_statements=", stderr); + Assert.Contains("rows_scanned=", stderr); + Assert.DoesNotContain("\"_debug\"", stdout); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_VerboseJsonAppendsDebugObjectAndKeepsStderrClean_Issue1899() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_verbose_json"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/auth.cs", + "csharp", + """ + public class AuthFixture + { + public void Authenticate() { } + } + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["Authenticate", "--db", dbPath, "--json", "--verbose"], + _jsonOptions)); + var lines = ParseJsonLines(stdout); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(2, lines.Count); + using var resultDocument = lines[0]; + using var debugDocument = lines[1]; + Assert.Equal("src/auth.cs", resultDocument.RootElement.GetProperty("path").GetString()); + var debug = debugDocument.RootElement.GetProperty("_debug"); + Assert.True(debug.GetProperty("sql_statement_count").GetInt32() > 0); + Assert.True(debug.GetProperty("elapsed_ms").GetDouble() >= 0); + Assert.True(debug.GetProperty("rows_scanned").GetInt32() >= 0); + Assert.Contains("omitted", debug.GetProperty("redaction").GetString()); + Assert.DoesNotContain("SELECT", stdout, StringComparison.OrdinalIgnoreCase); + Assert.DoesNotContain(dbPath, stdout, StringComparison.Ordinal); + Assert.DoesNotContain("StackTrace", stdout, StringComparison.OrdinalIgnoreCase); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_RecognizesMsbuildProjectFiles() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_msbuild_lang"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + var queryToken = "msbuild_lang_search_4f9c2a"; + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/App.csproj", + "msbuild", + $$""" + + + {{queryToken}} + + + """); + + var (msbuildExitCode, msbuildStdout, msbuildStderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + [queryToken, "--db", dbPath, "--lang", "msbuild", "--json", "--count"], + _jsonOptions)); + var (xmlExitCode, xmlStdout, xmlStderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + [queryToken, "--db", dbPath, "--lang", "xml", "--count"], + _jsonOptions)); + + using var msbuildDocument = ParseJsonOutput(msbuildStdout); + var msbuildJson = msbuildDocument.RootElement; + + Assert.Equal(CommandExitCodes.Success, msbuildExitCode); + Assert.Equal(1, msbuildJson.GetProperty("count").GetInt32()); + Assert.Equal(1, msbuildJson.GetProperty("files").GetInt32()); + Assert.Equal(queryToken, msbuildJson.GetProperty("query").GetString()); + Assert.Equal(string.Empty, msbuildStderr); + + Assert.Equal(CommandExitCodes.Success, xmlExitCode); + Assert.Equal("0", xmlStdout.Trim()); + Assert.Equal(string.Empty, xmlStderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Theory] + [InlineData("xaml")] + [InlineData("axaml")] + public void RunSearch_RecognizesXamlLanguageAliases(string lang) + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_xaml_lang_alias"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + var queryToken = $"xaml_lang_alias_{Guid.NewGuid():N}"; + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/MainWindow.xaml", + "xml", + $$""" + + + + + + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + [queryToken, "--db", dbPath, "--lang", lang, "--count"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal("1", stdout.Trim()); + Assert.Equal(string.Empty, stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Theory] + [InlineData("rs")] + [InlineData("r-s")] + [InlineData("r s")] + public void RunSearch_RecognizesRustLanguageAlias(string lang) + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_rust_lang_alias"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + var queryToken = $"rust_lang_alias_{Guid.NewGuid():N}"; + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/lib.rs", + "rust", + $$""" + pub fn hit() { + let _ = "{{queryToken}}"; + } + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + [queryToken, "--db", dbPath, "--lang", lang, "--count"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal("1", stdout.Trim()); + Assert.Equal(string.Empty, stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Theory] + [InlineData("c#")] + [InlineData("cs")] + [InlineData("cshtml")] + [InlineData("js")] + [InlineData("JSX")] + [InlineData("cjs")] + [InlineData("MJS")] + [InlineData("Java")] + [InlineData("kt")] + [InlineData("kts")] + [InlineData("razor")] + public void RunSearch_NormalizesCommonLanguageAliases(string input) + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_lang_alias"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + var queryToken = "lang_alias_91d4b3"; + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/App.cs", + "csharp", + $@"public class App +{{ + public void Run() + {{ + var marker = ""{queryToken}""; + }} +}}"); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/App.kt", + "kotlin", + $@"class App {{ + fun run() {{ + val marker = ""{queryToken}"" + }} +}}"); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/App.java", + "java", + $@"class App {{ + void run() {{ + String marker = ""{queryToken}""; + }} +}}"); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/App.js", + "javascript", + $@"function run() {{ + const marker = ""{queryToken}""; +}}"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + [queryToken, "--db", dbPath, "--lang", input, "--count"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal("1", stdout.Trim()); + Assert.Equal(string.Empty, stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Theory] + [InlineData("js")] + [InlineData("jsx")] + [InlineData("JS")] + [InlineData("JSX")] + public void RunSearch_NormalizesJavascriptLangAliases(string lang) + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_javascript_lang_alias"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + var queryToken = $"javascript_lang_alias_{Guid.NewGuid():N}"; + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/App.js", + "javascript", + $@"const marker = ""{queryToken}"";"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + [queryToken, "--db", dbPath, "--lang", lang, "--count"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal("1", stdout.Trim()); + Assert.Equal(string.Empty, stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Theory] + [InlineData("cjs")] + [InlineData("mjs")] + [InlineData("CJS")] + [InlineData("MJS")] + public void RunSearch_NormalizesJavascriptExtensionStyleLangAliases(string lang) + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_javascript_extension_lang_alias"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + var queryToken = $"javascript_extension_lang_alias_{Guid.NewGuid():N}"; + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/App.mjs", + "javascript", + $@"const marker = ""{queryToken}"";"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + [queryToken, "--db", dbPath, "--lang", lang, "--count"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal("1", stdout.Trim()); + Assert.Equal(string.Empty, stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Theory] + [InlineData("yml")] + [InlineData("YML")] + public void RunSearch_NormalizesYamlLangAlias(string input) + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_yaml_lang_alias"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + var queryToken = "yaml_lang_alias_3d5a19"; + TestProjectHelper.InsertIndexedFile( + dbPath, + "config/workflow.yml", + "yaml", + $@"name: demo +jobs: + build: + runs-on: ubuntu-latest + steps: + - run: echo ""{queryToken}"""); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + [queryToken, "--db", dbPath, "--lang", input, "--count"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal("1", stdout.Trim()); + Assert.Equal(string.Empty, stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Theory] + [InlineData("bat")] + [InlineData("cmd")] + public void RunSearch_NormalizesBatchLangAliases(string input) + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_batch_lang_alias"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + var queryToken = "batch_lang_alias_7a24d1"; + TestProjectHelper.InsertIndexedFile( + dbPath, + "scripts/run.bat", + "batch", + $"echo {queryToken}\r\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + [queryToken, "--db", dbPath, "--lang", input, "--count"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal("1", stdout.Trim()); + Assert.Equal(string.Empty, stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Theory] + [InlineData("T-SQL")] + [InlineData("transact-sql")] + [InlineData("transact sql")] + public void RunSearch_NormalizesSqlDialectLangAliases(string input) + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_sql_lang_alias"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + var queryToken = "sql_lang_alias_3f7d21"; + TestProjectHelper.InsertIndexedFile( + dbPath, + "sql/repro.sql", + "sql", + $"SELECT '{queryToken}';"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + [queryToken, "--db", dbPath, "--lang", input, "--count"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal("1", stdout.Trim()); + Assert.Equal(string.Empty, stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Theory] + [InlineData("tokio::spawn", "column qualifier")] + [InlineData("AND OR", "literal-safe search")] + [InlineData("foo\"bar", "literal-safe search")] + public void RunSearch_RawFtsQuerySyntaxErrorsReturnUsageError(string query, string expectedHint) + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_raw_fts_error"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", "public class App { public void spawn() { } }"); + + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + [query, "--db", dbPath, "--fts"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("Error [E006_FTS_QUERY_SYNTAX]: FTS5 query syntax:", stderr); + Assert.Contains(expectedHint, stderr); + Assert.DoesNotContain("database error:", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_RawFtsValidQueryStillWorks() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_raw_fts_success"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", "public class App { public void spawn() { } }"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["spawn", "--db", dbPath, "--fts", "--count"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.NotEqual("0", stdout.Trim()); + Assert.DoesNotContain("Error:", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_RawFtsTooLongQueryReturnsUsageError() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_raw_fts_too_long"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", "public class App { public void spawn() { } }"); + var query = new string('a', QueryLimits.MaxQueryLength + 1); + + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + [query, "--db", dbPath, "--fts"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains(QueryLimits.FormatQueryTooLongError(), stderr); + Assert.Contains("Usage:", stderr); + Assert.DoesNotContain("database error:", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_TooLongQueryReturnsUsageError_Issue1468() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_query_too_long"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + var query = new string('a', QueryLimits.MaxQueryLength + 1); + + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + [query, "--db", dbPath], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains(QueryLimits.FormatQueryTooLongError(), stderr); + Assert.Contains("Shorten the search text", stderr); + Assert.DoesNotContain("database error:", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_RawFtsTooManyNearOperatorsReturnsUsageError() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_raw_fts_too_many_near"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", "public class App { public void spawn() { } }"); + var query = string.Join(" OR ", Enumerable.Repeat("NEAR(spawn app, 5)", DbReader.MaxRawFtsNearOperators + 1)); + + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + [query, "--db", dbPath, "--fts", "--count"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("raw FTS5 query is too complex", stderr); + Assert.Contains("NEAR operators", stderr); + Assert.DoesNotContain("database error:", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_RawFtsLowercaseOperatorWordsAreTerms() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_raw_fts_lowercase_terms"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", "and or not near"); + var query = string.Join(" ", Enumerable.Repeat("and", DbReader.MaxRawFtsBooleanOperators + 1)); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + [query, "--db", dbPath, "--fts", "--count"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal("1", stdout.Trim()); + Assert.DoesNotContain("too complex", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_TrailingWildcardActsAsPrefixShorthand() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_prefix_shorthand"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/auth.cs", + "csharp", + "public class Authenticator { public bool AuthenticateUser() => true; }\n"); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/other.cs", + "csharp", + "public class Other { public void Idle() { } }\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["auth*", "--db", dbPath, "--count"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal("1", stdout.Trim()); + Assert.Equal(string.Empty, stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Theory] + [InlineData("rb", "ruby", "package/example.rb")] + [InlineData("fs", "fsharp", "Module.fs")] + public void RunSearch_AcceptsRubyAndFsharpLangAliases(string alias, string canonicalLang, string filePath) + { + var projectRoot = TestProjectHelper.CreateTempProject($"cdidx_{canonicalLang}_{alias}_lang_alias"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + filePath, + canonicalLang, + """ + public_api + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["public_api", "--db", dbPath, "--lang", alias, "--exact", "--count"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal("1", stdout.Trim()); + Assert.Equal(string.Empty, stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_ZeroResultsHumanOutputIncludesQueryFilterContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_zero_context_human"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/App.cs", + "csharp", + "public sealed class App { }"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["missing-token", "--db", dbPath, "--path", "src/**", "--lang", "csharp", "--limit", "7"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stdout); + Assert.Contains("No results found. (query: \"missing-token\", path: src/**, lang: csharp, limit: 7)", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_ZeroResultsJsonIncludesStructuredQueryContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_zero_context_json"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/App.cs", + "csharp", + "public sealed class App { }"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["missing-token", "--db", dbPath, "--path", "src/**", "--lang", "csharp", "--limit", "7", "--json"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + using var document = ParseJsonOutput(stdout); + var root = document.RootElement; + var queryContext = root.GetProperty("query_context"); + + Assert.Equal(0, root.GetProperty("count").GetInt32()); + Assert.Equal("missing-token", root.GetProperty("query").GetString()); + Assert.Equal("missing-token", queryContext.GetProperty("text").GetString()); + Assert.Equal("src/**", queryContext.GetProperty("path")[0].GetString()); + Assert.Equal("csharp", queryContext.GetProperty("lang").GetString()); + Assert.Equal(7, queryContext.GetProperty("limit").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_AllowsPathValueThatLooksLikePreviewOption() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_preview_like_path_value"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["foo", "--db", dbPath, "--path=--max-line-width", "--count"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal("0", stdout.Trim()); + Assert.DoesNotContain("is not supported", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunExcerpt_RejectsMissingFocusColumnValue() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_excerpt_missing_focus_column"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "README.md", "markdown", "sample"); + + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunExcerpt( + ["README.md", "--db", dbPath, "--start", "1", "--focus-column", "--json"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + // The recognized-option guard in TryReadRawOptionValue short-circuits `--focus-column --json` + // as a missing-value case before TryParsePositiveInt runs, so the error is "requires a value" + // rather than the older TryParsePositiveInt-level "requires a positive integer" message. + // `--focus-column --json` は TryReadRawOptionValue の既知オプション判定で TryParsePositiveInt + // 実行前に値欠如として短絡するため、旧メッセージではなく "requires a value" となる。 + Assert.Contains("--focus-column requires a value", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_RawFtsSyntaxErrorsAreReportedAsUsageErrors() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_raw_fts_syntax_error"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/demo.cs", "csharp", "class Demo {}\n"); + + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["title:foo", "--db", dbPath, "--fts", "--count"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("Error [E006_FTS_QUERY_SYNTAX]: FTS5 query syntax:", stderr); + Assert.Contains("raw FTS5 syntax", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_ExplicitMissingDbReturnsUsageErrorBeforeOpeningReader_Issue2073() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_issue2073_missing_db"); + try + { + var missingDb = Path.Combine(projectRoot, "missing-dir", "codeindex.db"); + + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["QueryCommandRunner", "--db", missingDb], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("Error [E001_DB_NOT_FOUND]: --db", stderr); + Assert.Contains("does not point to an existing database file", stderr); + Assert.Contains("Hint: create or refresh the index with `cdidx index `", stderr); + Assert.Contains($"Usage: {ConsoleUi.GetUsageLine("search")}", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Theory] + [InlineData("--path")] + [InlineData("--exclude-path")] + public void RunSearch_InvalidPathGlobReturnsUsageErrorBeforeQuery_Issue2073(string optionName) + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_issue2073_invalid_glob"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["QueryCommandRunner", "--db", dbPath, optionName, "[*-z]"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains($"Error: {optionName} '[*-z]' is not a valid glob", stderr); + Assert.Contains("character classes are not supported", stderr); + Assert.Contains("Hint: fix the invalid or missing option value", stderr); + Assert.Contains($"Usage: {ConsoleUi.GetUsageLine("search")}", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + // Issue #1507: missing-value errors for CLI flags must append a per-flag `Hint:` line that + // shows the expected value type or range (e.g. positive integer, glob pattern, language id), + // so users do not need to consult `--help` for trivial mistakes. The hint is sourced from + // a single per-flag metadata table so every command surfaces consistent guidance. + // Issue #1507: 値欠如エラーには、フラグごとに期待する値の型/範囲を示す `Hint:` 行を + // 追記する(例: 正の整数、glob、言語識別子)。`--help` を見なくてもユーザーが復旧できる。 + // ヒントは単一のメタデータテーブルから供給され、コマンド間で一貫した案内を出す。 + [Theory] + [InlineData(new[] { "QueryCommandRunner", "--limit" }, "search", "--limit", "pass a positive integer", "--limit 20")] + [InlineData(new[] { "QueryCommandRunner", "--top" }, "search", "--limit", "pass a positive integer", "--limit 20")] + [InlineData(new[] { "QueryCommandRunner", "--db" }, "search", "--db", "pass a path to a CodeIndex SQLite database", ".cdidx/codeindex.db")] + [InlineData(new[] { "QueryCommandRunner", "--lang" }, "search", "--lang", "pass a language identifier", "--lang csharp")] + [InlineData(new[] { "QueryCommandRunner", "--path" }, "search", "--path", "pass a glob-style path pattern", "--path src/**")] + [InlineData(new[] { "QueryCommandRunner", "--exclude-path" }, "search", "--exclude-path", "pass a glob-style path pattern to exclude", "--exclude-path tests/**")] + [InlineData(new[] { "QueryCommandRunner", "--snippet-lines" }, "search", "--snippet-lines", "pass an integer between 1 and 20", "--snippet-lines 8")] + [InlineData(new[] { "QueryCommandRunner", "--snippet-focus" }, "search", "--snippet-focus", "pass one of `leftmost`, `quality`, or `proximity`", "--snippet-focus quality")] + [InlineData(new[] { "QueryCommandRunner", "--max-line-width" }, "search", "--max-line-width", "pass a non-negative integer", "--max-line-width 512")] + public void RunSearch_MissingOptionValueAppendsPerFlagHint_Issue1507( + string[] args, + string command, + string optionName, + string expectedHintFragment, + string expectedExampleFragment) + { + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch(args, _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains($"Error: {optionName} requires a value.", stderr); + Assert.Contains($"Hint: {expectedHintFragment}", stderr); + Assert.Contains(expectedExampleFragment, stderr); + Assert.Contains($"Usage: {ConsoleUi.GetUsageLine(command)}", stderr); + } + + [Fact] + public void RunExcerpt_MissingStartValueShowsPerFlagHint_Issue1507() + { + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunExcerpt(["src/CodeIndex/Program.cs", "--start"], _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("Error: --start requires a value.", stderr); + Assert.Contains("Hint: pass a 1-based line number", stderr); + Assert.Contains("--start 10", stderr); + } + + // Issue #1507: when a separated `--db --foo` shape rejects the next token as a recognized + // option, both the inline-form hint AND the per-flag hint must surface so users know why + // the parser stopped *and* what value to pass. + // Issue #1507: `--db --foo` のような separated dashed literal が拒否されたときは、 + // inline-form ヒント (`--db=` 形式) と、フラグ別の値ヒントを両方表示する。 + [Fact] + public void RunSearch_DbDoubleDashLiteralKeepsBothHints_Issue1507() + { + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["QueryCommandRunner", "--db", "--mystery"], _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("Error: --db requires a value.", stderr); + Assert.Contains("Hint: if the literal value starts with `--`, pass it as `--db=`.", stderr); + Assert.Contains("Hint: pass a path to a CodeIndex SQLite database", stderr); + } + + // Issue #1507: `find` validates its options through ValidateFindArgs (separate path from + // ParseArgs), so the per-flag hint table must apply there too. Otherwise users running + // `cdidx find foo --path` would still see the bare "requires a value" message. + // Issue #1507: `find` は ValidateFindArgs 経由で値検証する独自経路を持つので、 + // この経路でもフラグ別ヒントを表示する。 + [Fact] + public void RunFind_MissingPathValueShowsPerFlagHint_Issue1507() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_issue1507_find_missing_path"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( + ["foo", "--db", dbPath, "--path"], _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("Error: --path requires a value.", stderr); + Assert.Contains("Hint: pass a glob-style path pattern", stderr); + Assert.Contains("--path src/**", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + // Pins `find --path=` with a value that starts with `--`. ParseArgs supports + // this shape via inline `=`, but ValidateFindArgs previously saw only the bare token + // and `PrepareFindArgs` briefly tried to normalize the inline form by splitting it into + // two tokens — that split destroyed inline `--`-prefixed values. Locks the contract + // that `find` honors the CLI hint (`pass it as --path=`) just like the other + // query commands. + // `find --path=` で value が `--` で始まる合法な inline 値を壊さないよう固定する + // 回帰テスト。`PrepareFindArgs` 側で inline を分解すると `--path=--literal.txt` が + // `--path`/`--literal.txt` に割れ、`ParseArgs` が値を option と誤認して失敗していた。 + [Fact] + public void RunFind_PathFilterAcceptsRecognizedOptionTokenViaInlineValue() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_find_path_inline_recognized_option"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/--json-dir/Demo.cs", + "csharp", + "class Demo { void Alpha() {} }\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( + ["Alpha", $"--db={dbPath}", "--path=--json-dir", "--count", "--json"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + using var document = ParseJsonOutput(stdout); + Assert.Equal(1, document.RootElement.GetProperty("count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + // `--paht` should surface as `--path` so MCP/CLI users do not have to read full help + // text to recover from a single-letter swap (#1582). + // `--paht` のような 1 文字入れ替えミスから `--path` を提案できることを確認する (#1582)。 + [Fact] + public void RunSearch_UnsupportedFlagTypo_SuggestsClosestFlag() + { + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["foo", "--paht", "src/**"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("Error: --paht is not supported for search.", stderr); + Assert.Contains("Did you mean: --path?", stderr); + } + + // Inline `--foo=bar` form must surface the same suggestion as the separated form. + // ParseArgs only splits `=value` for known value-taking options, so for `--paht=...` + // the suggester previously saw the full `--paht=src/**` token and produced no match; + // the round-2 fix strips the `=value` portion before searching for a similar flag. + // インライン `--foo=bar` 形式も separated 形式と同じ提案を出すこと。ParseArgs は + // 既知の value-taking option でしか `=value` を分解しないため、`--paht=...` は + // まるごと matcher に渡され従来は提案が出なかった。round-2 修正で `=value` を + // 除去してから候補を探すようにした。 + [Fact] + public void RunSearch_UnsupportedFlagTypoInInlineValueForm_SuggestsClosestFlag_Issue1582() + { + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["foo", "--paht=src/**"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("is not supported for search", stderr); + Assert.Contains("Did you mean: --path?", stderr); + } + + [Fact] + public void RunSearch_UnknownFlagAfterQuery_ReturnsUsageError() + { + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["foo", "--dapth", "3"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Equal(string.Empty, stdout); + Assert.Contains("--dapth is not supported for search", stderr); + Assert.Contains("Did you mean: --path?", stderr); + } + + // `find` previously emitted only the raw `Error: unsupported option for find: --paht` + // line — round-2 fix routes the unknown token through the same suggester so users see + // `Did you mean: --path?`. Covers both the separated and inline `=value` forms. + // 従来 find は `Error: unsupported option for find: --paht` だけを出していたが、 + // round-2 修正で同じ suggester を経由するようにし `Did you mean: --path?` を出す。 + // separated 形式と inline `=value` 形式の両方を確認する。 + [Fact] + public void RunFind_UnsupportedFlagTypo_SuggestsClosestFlag_Issue1582() + { + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( + ["guard", "--paht", "src/Auth.cs"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("unsupported option for find: --paht", stderr); + Assert.Contains("Did you mean: --path?", stderr); + } + + [Fact] + public void RunFind_UnsupportedFlagTypoInInlineValueForm_SuggestsClosestFlag_Issue1582() + { + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( + ["guard", "--paht=src/Auth.cs"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("unsupported option for find: --paht=src/Auth.cs", stderr); + Assert.Contains("Did you mean: --path?", stderr); + } + + // Regression lock for #184 follow-up: `--query` accepts dashed literals including + // recognized flags (e.g. `--json`) as query text, because FTS-style queries can + // legitimately contain dash-prefixed tokens. The recognized-option guard must NOT + // short-circuit `--query`, and the downstream IsRejectedSeparatedStringValue check + // must also skip `--query` so the flag-shaped token flows through as a literal. + // #184 のフォローアップ回帰ロック: `--query` は `--json` のような既知フラグを含む dashed + // literal をクエリ本文として受け入れる(FTS 風クエリには dash 付きトークンが現れ得る)。 + // recognized-option guard で `--query` を早期に短絡してはならず、後段の + // IsRejectedSeparatedStringValue も `--query` を素通りさせて flag 形状のトークンをリテラルと + // して扱う契約を維持する。 + [Fact] + public void RunFind_QueryAcceptsDashedLiteralValue_Issue184() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_issue184_query_dashed_literal"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/Issue184.cs", + "csharp", + "namespace Issue184; public class T { public void M() { } }"); + + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( + ["--query", "--json", "--path", "src/**", "--db", dbPath, "--json"], _jsonOptions)); + + // Accepting `--json` as query text is the success contract; the query may or may not + // match, but parsing must NOT fail with a "requires a value" error for --query. + // `--json` をクエリテキストとして受け入れるのが成功契約。ヒットの有無は問わないが、 + // --query が "requires a value" で失敗してはならない。 + Assert.NotEqual(CommandExitCodes.UsageError, exitCode); + Assert.DoesNotContain("--query requires a value", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + // Regression lock for #184 follow-up: `--focus-column` requires a positive integer, + // while `--max-line-width` accepts non-negative integers so `0` can disable truncation. + // Zero, negative, and non-numeric values must fail closed with UsageError and the + // corresponding validation message. Earlier tests only covered the missing-value case + // (which now short-circuits before TryParsePositiveInt), leaving these option-specific + // numeric contracts uncovered. + // #184 のフォローアップ回帰ロック: `--focus-column` は正の整数を要求し、 + // `--max-line-width` は切り詰め解除のため 0 を許容する非負整数。0・負数・非数値は + // UsageError と対応する validation message で fail-close する。以前のテストは値欠如 + // (今は TryParsePositiveInt 前に短絡する)しかカバーしていなかったため、 + // これらのオプション固有の数値契約を明示的にロックする。 + [Theory] + [InlineData("0")] + [InlineData("abc")] + public void RunExcerpt_RejectsInvalidFocusColumnValue(string invalidValue) + { + var projectRoot = TestProjectHelper.CreateTempProject($"cdidx_excerpt_invalid_focus_column_{invalidValue}"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "README.md", "markdown", "sample"); + + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunExcerpt( + ["README.md", "--db", dbPath, "--start", "1", "--focus-column", invalidValue, "--json"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("--focus-column requires an integer between 1 and 100000", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + // `search --lang csarp` previously emitted "No results found." with no language hint + // — RunSearch never called WriteLangHint. Round-2 wires WriteLangHint into the zero- + // result branch and lets it fall back to ReferenceExtractor.GetSupportedLanguages() + // when the typo'd value matches no indexed language (#1582). + // 従来 `search --lang csarp` は "No results found." だけ表示し、RunSearch から + // WriteLangHint を呼んでいなかった。round-2 で zero-result 分岐に WriteLangHint を + // 配線し、index 済み言語にマッチしない場合は ReferenceExtractor.GetSupportedLanguages() + // にフォールバックして提案を出すようにした (#1582)。 + [Fact] + public void RunSearch_LangTypo_SuggestsClosestLanguage_Issue1582() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_lang_typo"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/App.cs", + "csharp", + "class App { }\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["nothing_matches_xyzzy", "--db", dbPath, "--lang", "csarp"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stdout); + Assert.Contains("No results found.", stderr); + Assert.Contains("Did you mean: --lang csharp?", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + // `--lang java` against a repo with no Java files used to print a confusing + // "Did you mean: --lang java?" because the fallback ReferenceExtractor match returned + // the exact input. Regression coverage for the round-3 fix that suppresses + // self-suggestions in WriteLangHint (#1582). + // Java を含まないリポジトリで `--lang java` を指定した際、フォールバックの + // ReferenceExtractor が入力と同じ値を返すため "Did you mean: --lang java?" という + // 紛らわしいメッセージが出ていた。round-3 で WriteLangHint が自己提案を抑止する + // ようにしたことの回帰ロック (#1582)。 + [Fact] + public void RunSearch_LangNotIndexedButSupported_DoesNotSelfSuggest_Issue1582() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_lang_no_self_suggest"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/App.cs", + "csharp", + "class App { }\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["nothing_matches_xyzzy", "--db", dbPath, "--lang", "java"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stdout); + Assert.Contains("No results found.", stderr); + Assert.Contains("'java' not found in index", stderr); + Assert.DoesNotContain("Did you mean: --lang java?", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunExcerpt_JsonClampsLongSingleLineContentAroundFocus() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_excerpt_long_line"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + var longLine = new string('a', 320) + "TARGET" + new string('b', 320); + TestProjectHelper.InsertIndexedFile(dbPath, "dist/data.txt", "text", longLine); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunExcerpt( + ["dist/data.txt", "--db", dbPath, "--start", "1", "--end", "1", "--json", "--max-line-width", "96", "--focus-column", (longLine.IndexOf("TARGET", StringComparison.Ordinal) + 1).ToString(), "--focus-length", "6"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.True(json.GetProperty("content_truncated").GetBoolean()); + Assert.DoesNotContain(longLine, json.GetProperty("content").GetString()); + Assert.Contains("TARGET", json.GetProperty("content").GetString()); + Assert.True(json.GetProperty("content").GetString()!.Length <= 96); + var semanticTokens = json.GetProperty("semantic_tokens").EnumerateArray().ToArray(); + Assert.Contains(semanticTokens, token => + token.GetProperty("type").GetString() == "variable" && + token.GetProperty("start_line").GetInt32() == 1 && + token.GetProperty("start_column").GetInt32() > 0); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunExcerpt_Json_AcceptsAbsolutePathWithExplicitDbOutsideProjectRoot() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_excerpt_absolute_path"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Sample.cs", "csharp", "namespace Demo;\npublic class Svc { }\n"); + var absolutePath = Path.Combine(projectRoot, "src", "Sample.cs"); + + var (relativeExitCode, relativeStdout, relativeStderr) = CaptureConsole(() => QueryCommandRunner.RunExcerpt( + ["src/Sample.cs", "--db", dbPath, "--start", "1", "--end", "2", "--json"], + _jsonOptions)); + var (absoluteExitCode, absoluteStdout, absoluteStderr) = CaptureConsole(() => QueryCommandRunner.RunExcerpt( + [absolutePath, "--db", dbPath, "--start", "1", "--end", "2", "--json"], + _jsonOptions)); + + using var relativeDocument = ParseJsonOutput(relativeStdout); + using var absoluteDocument = ParseJsonOutput(absoluteStdout); + + Assert.Equal(CommandExitCodes.Success, relativeExitCode); + Assert.Equal(CommandExitCodes.Success, absoluteExitCode); + Assert.Equal(string.Empty, relativeStderr); + Assert.Equal(string.Empty, absoluteStderr); + Assert.Equal("src/Sample.cs", relativeDocument.RootElement.GetProperty("path").GetString()); + Assert.Equal(relativeDocument.RootElement.GetProperty("path").GetString(), absoluteDocument.RootElement.GetProperty("path").GetString()); + Assert.Equal(relativeDocument.RootElement.GetProperty("content").GetString(), absoluteDocument.RootElement.GetProperty("content").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunExcerpt_JsonClampsLongSingleLineContentWithoutFocus() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_excerpt_long_line_no_focus"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + var longLine = new string('a', 320) + "TARGET" + new string('b', 320); + TestProjectHelper.InsertIndexedFile(dbPath, "dist/data.txt", "text", longLine); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunExcerpt( + ["dist/data.txt", "--db", dbPath, "--start", "1", "--end", "1", "--json", "--max-line-width", "96"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.True(json.GetProperty("content_truncated").GetBoolean()); + Assert.DoesNotContain(longLine, json.GetProperty("content").GetString()); + Assert.True(json.GetProperty("content").GetString()!.Length <= 96); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunExcerpt_JsonLeavesLongSingleLineContentUnclampedWhenMaxLineWidthIsZero() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_excerpt_long_line_no_truncate"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + var longLine = new string('a', 320) + "TARGET" + new string('b', 320); + TestProjectHelper.InsertIndexedFile(dbPath, "dist/data.txt", "text", longLine); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunExcerpt( + ["dist/data.txt", "--db", dbPath, "--start", "1", "--end", "1", "--json", "--max-line-width", "0"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.False(json.GetProperty("content_truncated").GetBoolean()); + Assert.Equal(longLine, json.GetProperty("content").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_JsonLeavesLongSingleLineSnippetUnclampedWhenMaxLineWidthIsZero() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_long_line_no_truncate"); + try + { + var longLine = new string('a', 320) + " TARGET " + new string('b', 320); + var sourcePath = Path.Combine(projectRoot, "notes.md"); + File.WriteAllText(sourcePath, longLine); + + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["TARGET", "--db", dbPath, "--json", "--max-line-width", "0"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + var rows = ParseJsonLines(stdout).Select(document => document.RootElement).ToList(); + Assert.Single(rows); + Assert.Equal("TARGET", rows[0].GetProperty("query").GetString()); + Assert.Contains(longLine, stdout); + Assert.DoesNotContain("...(+", stdout); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunExcerpt_FocusLineWithoutFocusColumnReturnsUsageError() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_excerpt_focus_dep"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "dist/data.txt", "text", new string('a', 320) + "TARGET" + new string('b', 320)); + + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunExcerpt( + ["dist/data.txt", "--db", dbPath, "--start", "1", "--end", "1", "--json", "--max-line-width", "96", "--focus-line", "1"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("--focus-line and --focus-length require --focus-column", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunExcerpt_FocusLineOutsideReturnedRangeReturnsUsageError() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_excerpt_focus_range"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "README.md", "markdown", "line one\nline two\nline three"); + + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunExcerpt( + ["README.md", "--db", dbPath, "--start", "2", "--end", "2", "--focus-line", "999", "--focus-column", "1", "--json"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("--focus-line (999) must be within the returned excerpt range", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunExcerpt_FocusColumnOutsideFocusedLineReturnsUsageError() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_excerpt_focus_column_range"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "dist/data.txt", "text", new string('a', 320) + "TARGET" + new string('b', 320)); + + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunExcerpt( + ["dist/data.txt", "--db", dbPath, "--start", "1", "--end", "1", "--focus-column", "9999", "--max-line-width", "40", "--json"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("--focus-column (9999) must be within the focused line length", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunFind_JsonClampsLongSingleLineSnippet() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_find_long_line"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + var longLine = new string('a', 320) + "target" + new string('b', 320); + TestProjectHelper.InsertIndexedFile(dbPath, "dist/search.txt", "text", longLine); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( + ["target", "--db", dbPath, "--path", "dist/search.txt", "--json", "--max-line-width", "96"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.True(json.GetProperty("snippet_truncated").GetBoolean()); + Assert.Contains("target", json.GetProperty("snippet").GetString()); + Assert.True(json.GetProperty("snippet").GetString()!.Length <= 96); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunFind_JsonTreatsZeroMaxLineWidthAsUnclamped() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_find_long_line_zero_width"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + var longLine = new string('a', 320) + "target" + new string('b', 320); + TestProjectHelper.InsertIndexedFile(dbPath, "dist/search.txt", "text", longLine); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( + ["target", "--db", dbPath, "--path", "dist/search.txt", "--json", "--max-line-width", "0"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.False(json.GetProperty("snippet_truncated").GetBoolean()); + Assert.Contains("target", json.GetProperty("snippet").GetString()); + Assert.True(json.GetProperty("snippet").GetString()!.Length > 512); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Theory] + [InlineData("--exact", "--exact-substring")] + [InlineData("--exact", "--exact-name")] + [InlineData("--exact-substring", "--exact-name")] + public void RunSearch_RejectsCombinedExactFlags(string first, string second) + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_combined_exact"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["needle", "--db", dbPath, first, second], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("pass only one of --exact, --exact-substring, --exact-name", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_RejectsAllThreeExactFlagsTogether() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_triple_exact"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["needle", "--db", dbPath, "--exact", "--exact-substring", "--exact-name"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("pass only one of --exact, --exact-substring, --exact-name", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_WithJsonOutputsCompactSnippetMetadata() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_search"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/app.cs", + "csharp", + "line 1\nline 2\nline 3\nTarget();\nline 5\nline 6"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["Target", "--db", dbPath, "--json", "--snippet-lines", "3"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("src/app.cs", json.GetProperty("path").GetString()); + Assert.Equal(1, json.GetProperty("chunk_start_line").GetInt32()); + Assert.Equal(6, json.GetProperty("chunk_end_line").GetInt32()); + Assert.Equal(3, json.GetProperty("snippet_start_line").GetInt32()); + Assert.Equal(5, json.GetProperty("snippet_end_line").GetInt32()); + Assert.Contains("Target();", json.GetProperty("snippet").GetString()); + Assert.Equal(4, json.GetProperty("match_lines")[0].GetInt32()); + Assert.Equal(1, json.GetProperty("highlights").GetArrayLength()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_ExactSubstringAliasMatchesBackwardCompatibleExact() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_search_exact_alias"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/app.cs", + "csharp", + "void Run() { }\nvoid RunAsync() { Run(); }\nvoid run() { }\n"); + + var exact = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["Run();", "--db", dbPath, "--json", "--exact"], + _jsonOptions)); + var alias = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["Run();", "--db", dbPath, "--json", "--exact-substring"], + _jsonOptions)); + + Assert.Equal(exact.Result, alias.Result); + Assert.Equal(exact.Stdout, alias.Stdout); + Assert.Equal(exact.Stderr, alias.Stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_ExcludeTestsSkipsPythonConftestFiles() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_search_conftest_exclude"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + var queryToken = "python_conftest_fixture_8bb7c4"; + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/conftest.py", + "python", + $"fixture_token = \"{queryToken}\"\n"); + + var withoutExclude = CaptureConsole(() => QueryCommandRunner.RunSearch( + [queryToken, "--db", dbPath, "--count"], + _jsonOptions)); + var withExclude = CaptureConsole(() => QueryCommandRunner.RunSearch( + [queryToken, "--db", dbPath, "--exclude-tests", "--count"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, withoutExclude.Result); + Assert.Equal("1", withoutExclude.Stdout.Trim()); + Assert.Equal(string.Empty, withoutExclude.Stderr); + + Assert.Equal(CommandExitCodes.Success, withExclude.Result); + Assert.Equal("0", withExclude.Stdout.Trim()); + Assert.Equal(string.Empty, withExclude.Stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_WithTypeScriptLangAliasesFiltersTypeScriptFiles() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_search_typescript_lang_alias"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/app.ts", + "typescript", + """ + export function hit() { + return "TypeScript"; + } + """); + + foreach (var langAlias in new[] { "ts", "tsx", "cts", "mts" }) + { + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["TypeScript", "--db", dbPath, "--lang", langAlias, "--count"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal("1", stdout.Trim()); + Assert.Equal(string.Empty, stderr); + } + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_WithJavaLangAliasFiltersJavaFiles() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_search_java_lang_alias"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/App.java", + "java", + """ + public class App { + String hit() { + return "Java"; + } + } + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["Java", "--db", dbPath, "--lang", "jav", "--count"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal("1", stdout.Trim()); + Assert.Equal(string.Empty, stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_ExactSubstringTreatsCSharpVerbatimQualifiedNamesAsCanonical() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_search_exact_csharp_verbatim"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/app.cs", + "csharp", + """ + namespace Demo; + + using @Foo.@Bar; + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["Foo.Bar", "--db", dbPath, "--path", "src/app.cs", "--json", "--exact-substring", "--count"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(1, json.GetProperty("count").GetInt32()); + Assert.Equal(1, json.GetProperty("files").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_ExactSubstringTreatsJavaUnicodeEscapesAsCanonical() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_search_exact_java_unicode"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/App.java", + "java", + """ + public class \u0046oo + { + void match() {} + } + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["Foo", "--db", dbPath, "--path", "src/App.java", "--json", "--exact-substring", "--count"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(1, json.GetProperty("count").GetInt32()); + Assert.Equal(1, json.GetProperty("files").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunFind_ExactTreatsJavaUnicodeEscapesAsCanonical() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_find_exact_java_unicode"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/App.java", + "java", + """ + public class \u0046oo + { + void match() {} + } + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( + ["Foo", "--db", dbPath, "--path", "src/App.java", "--json", "--exact", "--count"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(1, json.GetProperty("count").GetInt32()); + Assert.Equal(1, json.GetProperty("files").GetInt32()); + Assert.Equal(1, json.GetProperty("file_count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_ExactSubstringTreatsKotlinBacktickedIdentifiersAsCanonical() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_search_exact_kotlin_backticks"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/App.kt", + "kotlin", + """ + fun `when`() {} + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["when", "--db", dbPath, "--path", "src/App.kt", "--json", "--exact-substring", "--count"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(1, json.GetProperty("count").GetInt32()); + Assert.Equal(1, json.GetProperty("files").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunFind_ExactTreatsCSharpVerbatimQualifiedNamesAsCanonical() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_find_exact_csharp_verbatim"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/app.cs", + "csharp", + """ + namespace Demo; + + using @Foo.@Bar; + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( + ["Foo.Bar", "--db", dbPath, "--path", "src/app.cs", "--json", "--exact", "--count"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(1, json.GetProperty("count").GetInt32()); + Assert.Equal(1, json.GetProperty("files").GetInt32()); + Assert.Equal(1, json.GetProperty("file_count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_ExactSubstringTreatsCSharpGlobalQualifiedNamesAsCanonical() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_search_exact_csharp_global"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/global.cs", + "csharp", + """ + namespace Demo; + + public class GlobalQualified + { + public void Match() + { + var value = global::Foo.Bar; + } + } + """); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/plain.cs", + "csharp", + """ + namespace Demo; + + public class PlainQualified + { + public void Match() + { + var value = Foo.Bar; + } + } + """); + + var (globalExitCode, globalStdout, globalStderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["Foo.Bar", "--db", dbPath, "--path", "src/global.cs", "--json", "--exact-substring", "--count"], + _jsonOptions)); + using var globalDocument = ParseJsonOutput(globalStdout); + + var (qualifiedExitCode, qualifiedStdout, qualifiedStderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["global::Foo.Bar", "--db", dbPath, "--path", "src/plain.cs", "--json", "--exact-substring", "--count"], + _jsonOptions)); + using var qualifiedDocument = ParseJsonOutput(qualifiedStdout); + + Assert.Equal(CommandExitCodes.Success, globalExitCode); + Assert.Equal(string.Empty, globalStderr); + Assert.Equal(1, globalDocument.RootElement.GetProperty("count").GetInt32()); + Assert.Equal(1, globalDocument.RootElement.GetProperty("files").GetInt32()); + + Assert.Equal(CommandExitCodes.Success, qualifiedExitCode); + Assert.Equal(string.Empty, qualifiedStderr); + Assert.Equal(1, qualifiedDocument.RootElement.GetProperty("count").GetInt32()); + Assert.Equal(1, qualifiedDocument.RootElement.GetProperty("files").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_ExactSubstringTreatsCSharpUnicodeEscapedIdentifiersAsCanonical() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_search_exact_csharp_unicode_escape"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/escaped.cs", + "csharp", + "namespace Demo;\n\n" + + "public class EscapedIdentifiers\n" + + "{\n" + + " public void Match()\n" + + " {\n" + + " var first = \\u0047lobalName;\n" + + " var second = \\U00000047lobalName;\n" + + " var keyword = @\\u0063lass.Member;\n" + + " }\n" + + "}\n"); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/plain.cs", + "csharp", + """ + namespace Demo; + + public class PlainIdentifiers + { + public void Match() + { + var first = GlobalName; + } + } + """); + + var (plainQueryExitCode, plainQueryStdout, plainQueryStderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["GlobalName", "--db", dbPath, "--path", "src/escaped.cs", "--json", "--exact-substring", "--count"], + _jsonOptions)); + using var plainQueryDocument = ParseJsonOutput(plainQueryStdout); + + var (escapedQueryExitCode, escapedQueryStdout, escapedQueryStderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["\\U00000047lobalName", "--db", dbPath, "--path", "src/plain.cs", "--json", "--exact-substring", "--count"], + _jsonOptions)); + using var escapedQueryDocument = ParseJsonOutput(escapedQueryStdout); + + var (verbatimExitCode, verbatimStdout, verbatimStderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["class.Member", "--db", dbPath, "--path", "src/escaped.cs", "--json", "--exact-substring", "--count"], + _jsonOptions)); + using var verbatimDocument = ParseJsonOutput(verbatimStdout); + + Assert.Equal(CommandExitCodes.Success, plainQueryExitCode); + Assert.Equal(string.Empty, plainQueryStderr); + Assert.Equal(1, plainQueryDocument.RootElement.GetProperty("count").GetInt32()); + Assert.Equal(1, plainQueryDocument.RootElement.GetProperty("files").GetInt32()); + + Assert.Equal(CommandExitCodes.Success, escapedQueryExitCode); + Assert.Equal(string.Empty, escapedQueryStderr); + Assert.Equal(1, escapedQueryDocument.RootElement.GetProperty("count").GetInt32()); + Assert.Equal(1, escapedQueryDocument.RootElement.GetProperty("files").GetInt32()); + + Assert.Equal(CommandExitCodes.Success, verbatimExitCode); + Assert.Equal(string.Empty, verbatimStderr); + Assert.Equal(1, verbatimDocument.RootElement.GetProperty("count").GetInt32()); + Assert.Equal(1, verbatimDocument.RootElement.GetProperty("files").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_LiteralSafeSearchKeepsCSharpUnicodeEscapeQueriesRawForFts() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_search_csharp_unicode_escape_fts"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/escaped.cs", + "csharp", + "namespace Demo;\n\n" + + "public class EscapedIdentifiers\n" + + "{\n" + + " public void Match()\n" + + " {\n" + + " var first = \\u0047lobalName;\n" + + " }\n" + + "}\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["\\u0047lobalName", "--db", dbPath, "--path", "src/escaped.cs", "--lang", "csharp", "--json", "--count"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(1, json.GetProperty("count").GetInt32()); + Assert.Equal(1, json.GetProperty("files").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_TreatsCSharpVerbatimQualifiedNamesAsCanonicalInLiteralSafeSearch() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_search_csharp_literal_safe_canonical"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/app.cs", + "csharp", + """ + namespace Demo; + + public class CanonicalQualified + { + public void Match() + { + var first = Foo.Bar; + var second = Foo.Bar; + } + } + """); + + var (globalExitCode, globalStdout, globalStderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["global::Foo.Bar", "--db", dbPath, "--path", "src/app.cs", "--lang", "csharp", "--json", "--count"], + _jsonOptions)); + using var globalDocument = ParseJsonOutput(globalStdout); + + var (verbatimExitCode, verbatimStdout, verbatimStderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["@Foo.@Bar", "--db", dbPath, "--path", "src/app.cs", "--lang", "csharp", "--json", "--count"], + _jsonOptions)); + using var verbatimDocument = ParseJsonOutput(verbatimStdout); + + Assert.Equal(CommandExitCodes.Success, globalExitCode); + Assert.Equal(string.Empty, globalStderr); + Assert.Equal(1, globalDocument.RootElement.GetProperty("count").GetInt32()); + Assert.Equal(1, globalDocument.RootElement.GetProperty("files").GetInt32()); + + Assert.Equal(CommandExitCodes.Success, verbatimExitCode); + Assert.Equal(string.Empty, verbatimStderr); + Assert.Equal(1, verbatimDocument.RootElement.GetProperty("count").GetInt32()); + Assert.Equal(1, verbatimDocument.RootElement.GetProperty("files").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_ExactSubstringKeepsNormalizationScopedToCSharp() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_search_exact_csharp_scope"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/app.cs", + "csharp", + """ + namespace Demo; + + using @Foo.@Bar; + """); + TestProjectHelper.InsertIndexedFile( + dbPath, + "scripts/run.bat", + "batch", + "@Foo.@Bar\r\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["Foo.Bar", "--db", dbPath, "--json", "--exact-substring", "--count"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(1, json.GetProperty("count").GetInt32()); + Assert.Equal(1, json.GetProperty("files").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_ExactSubstringTreatsTsqlQualifiedNamesAsCanonical() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_search_exact_tsql_canonical"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/target.sql", + "sql", + """ + CREATE PROCEDURE [sales] . [usp_Target] + AS + SELECT 1; + GO + """); + TestProjectHelper.InsertIndexedFile( + dbPath, + "scripts/ignored.bat", + "batch", + """ + [sales] . [usp_Target] + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["sales.usp_Target", "--db", dbPath, "--json", "--exact-substring", "--count"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(1, json.GetProperty("count").GetInt32()); + Assert.Equal(1, json.GetProperty("files").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_ExactSubstringHumanSnippetUsesCaseSensitiveFocusLine() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_search_exact_human_snippet"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/app.cs", + "csharp", + "void run() { }\nvoid Run() { }\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["Run()", "--db", dbPath, "--exact-substring", "--snippet-lines", "1"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Contains("src/app.cs:", stdout); + Assert.Contains(" void Run() { }", stdout); + Assert.DoesNotContain(" void run() { }", stdout); + Assert.Contains("(1 results in 1 files)", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_RejectsExactNameAlias() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_search_wrong_exact_alias"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["Run", "--db", dbPath, "--exact-name"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("--exact-substring", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_MissingQueryUsageMentionsExactSubstringAlias() + { + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch([], _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("--exact|--exact-substring", stderr); + } + + [Fact] + public void RunSearch_ZeroResultJson_EmitsStructuredPayloadWithFreshnessHint() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_search_zero_json"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/app.cs", + "csharp", + "class App { void Target() {} }\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["MissingTarget", "--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Equal("MissingTarget", json.GetProperty("query").GetString()); + Assert.Equal(0, json.GetProperty("results").GetArrayLength()); + Assert.Equal(1, json.GetProperty("indexed_file_count").GetInt32()); + Assert.True(json.TryGetProperty("indexed_at", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_ZeroResultJson_EmptyIndexEmitsNullIndexedAt() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_search_zero_json_empty"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["MissingTarget", "--db", dbPath, "--json"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Equal("MissingTarget", json.GetProperty("query").GetString()); + Assert.Equal(0, json.GetProperty("results").GetArrayLength()); + Assert.Equal(0, json.GetProperty("indexed_file_count").GetInt32()); + Assert.Equal(JsonValueKind.Null, json.GetProperty("indexed_at").ValueKind); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_ZeroResultJson_CountOnlyEmitsFreshnessHint() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_search_zero_json_count"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/app.cs", + "csharp", + "class App { void Target() {} }\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["MissingTarget", "--db", dbPath, "--json", "--count"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Equal("MissingTarget", json.GetProperty("query").GetString()); + Assert.Equal(0, json.GetProperty("files").GetInt32()); + Assert.Equal(1, json.GetProperty("indexed_file_count").GetInt32()); + Assert.True(json.TryGetProperty("indexed_at", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_ZeroResultJson_CountOnlyEmptyIndexEmitsNullIndexedAt() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_search_zero_json_count_empty"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["MissingTarget", "--db", dbPath, "--json", "--count"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(0, json.GetProperty("count").GetInt32()); + Assert.Equal("MissingTarget", json.GetProperty("query").GetString()); + Assert.Equal(0, json.GetProperty("files").GetInt32()); + Assert.Equal(0, json.GetProperty("indexed_file_count").GetInt32()); + Assert.Equal(JsonValueKind.Null, json.GetProperty("indexed_at").ValueKind); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_GroupByName_IsRejectedOutsideHotspots() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_group_by_name_reject"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", "public class App { public void Run() { } }"); + + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["Run", "--db", dbPath, "--group-by-name"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("--group-by-name is only supported by 'hotspots'", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_GroupBy_IsRejectedOutsideHotspots() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_group_by_reject"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", "public class App { public void Run() { } }"); + + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["Run", "--db", dbPath, "--group-by", "file"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("--group-by is only supported by 'hotspots'", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunFind_RequiresPathScope() + { + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( + ["guard"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("requires at least one --path", stderr); + } + + [Fact] + public void RunFind_PathGlobsMatchExpectedFiles() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_find_path_glob"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/app.py", + "python", + """ + def hello(): + return "hello" + """); + TestProjectHelper.InsertIndexedFile( + dbPath, + "tests/app.py", + "python", + """ + def hello(): + return "hello" + """); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/app.txt", + "text", + "greetings\n"); + + var (suffixExitCode, suffixStdout, suffixStderr) = CaptureConsole(() => QueryCommandRunner.RunFind( + ["hello", "--db", dbPath, "--path", "*.py"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, suffixExitCode); + Assert.Contains("4 matches in 2 files", suffixStderr); + Assert.Contains("src/app.py", suffixStdout); + Assert.Contains("tests/app.py", suffixStdout); + Assert.DoesNotContain("src/app.txt", suffixStdout); + + var (prefixedExitCode, prefixedStdout, prefixedStderr) = CaptureConsole(() => QueryCommandRunner.RunFind( + ["hello", "--db", dbPath, "--path", "src/*.py"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, prefixedExitCode); + Assert.Contains("2 matches in 1 file", prefixedStderr); + Assert.Contains("src/app.py", prefixedStdout); + Assert.DoesNotContain("tests/app.py", prefixedStdout); + Assert.DoesNotContain("src/app.txt", prefixedStdout); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunFind_RejectsUnsupportedFlags() + { + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( + ["guard", "--path", "src/Auth.cs", "--since", "2099-01-01"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("unsupported option for find: --since", stderr); + } + + [Fact] + public void RunFind_RejectsInvalidNumericOptions() + { + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( + ["FindUsage", "--path", "src/CodeIndex/Cli/QueryCommandRunner.cs", "--before", "-1"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("--before requires an integer between 0 and 1000", stderr); + } + + [Fact] + public void RunFind_RejectsInvalidLimit() + { + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( + ["FindUsage", "--path", "src/CodeIndex/Cli/QueryCommandRunner.cs", "--limit", "nope"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("--limit requires an integer between 1 and 10000", stderr); + } + + [Theory] + [InlineData("--limit", "10001", 10_000)] + [InlineData("--before", "1001", 1_000)] + [InlineData("--after", "1001", 1_000)] + public void RunFind_RejectsNumericFlagAboveUpperBound_Issue1503(string flag, string value, int expectedMax) + { + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( + ["FindUsage", "--path", "src/CodeIndex/Cli/QueryCommandRunner.cs", flag, value], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains($"must be less than or equal to {expectedMax}", stderr); + Assert.Contains($"got '{value}'", stderr); + } + + [Fact] + public void RunFind_InvalidSinceFailsClosedInsteadOfRunning() + { + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( + ["guard", "--path", "src/Auth.cs", "--since", "not-a-date"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("unsupported option for find: --since", stderr); + } + + [Fact] + public void RunFind_AllowsDashedLiteralViaQueryFlag() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_find_query_flag"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "README.md", + "markdown", + "--json appears here\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( + ["--query", "--json", "--db", dbPath, "--path", "README.md", "--count"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("1", stdout.Trim()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunFind_AllowsDashedLiteralViaDoubleDash() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_find_double_dash"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "README.md", + "markdown", + "--path appears here\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( + ["--db", dbPath, "--path", "README.md", "--count", "--", "--path"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("1", stdout.Trim()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_QueryOptionAcceptsOptionLookingLiteral_Issue923() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_issue923_search_query_option"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "README.md", + "markdown", + "--path appears here\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["--query", "--path", "--path", "README.md", "--db", dbPath, "--count"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal("1", stdout.Trim()); + Assert.Equal(string.Empty, stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_DoubleDashEscapesSingleOptionLookingQueryToken_Issue923() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_issue923_search_dashdash"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "README.md", + "markdown", + "--path appears here\n--json appears elsewhere\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["--", "--path", "--path", "README.md", "--db", dbPath, "--count"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal("1", stdout.Trim()); + Assert.Equal(string.Empty, stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_EndOfOptionsAcceptsOptionLookingLiteral() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_issue799_search_positional_literal"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "README.md", + "markdown", + "--open-reports appears here\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["--", "--open-reports", "--path", "README.md", "--db", dbPath, "--count"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal("1", stdout.Trim()); + Assert.Equal(string.Empty, stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunExcerpt_RejectsQueryOption() + { + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunExcerpt( + ["src/CodeIndex/Cli/QueryCommandRunner.cs", "--start", "626", "--query", "src/CodeIndex/Cli/ConsoleUi.cs"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("--query is not supported by this command", stderr); + } + + [Fact] + public void RunFind_ZeroResultHintDistinguishesPathMatchesFromQueryMiss_Issue1406() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_find_zero_hint"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "README.md", + "markdown", + "hello world\n"); + + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( + ["qqq__no_such_token__zzz", "--db", dbPath, "--path", "README.md"], + _jsonOptions)); + var normalizedStderr = stderr.ToLowerInvariant(); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Contains("No matches found.", stderr); + Assert.Contains("--path matched 1 file, but the query did not match their contents", stderr); + Assert.Contains("try a broader query or check the query syntax", normalizedStderr); + Assert.DoesNotContain("broadening --path or adding another --path value", normalizedStderr); + Assert.DoesNotContain("try removing --lang, --path", normalizedStderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunFind_ZeroResultHintStillSuggestsBroadeningUnmatchedPath_Issue1406() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_find_zero_path_hint"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "README.md", + "markdown", + "hello world\n"); + + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( + ["hello", "--db", dbPath, "--path", "src/**/*.cs"], + _jsonOptions)); + var normalizedStderr = stderr.ToLowerInvariant(); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Contains("broadening --path or adding another --path value", normalizedStderr); + Assert.DoesNotContain("query did not match", normalizedStderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunFind_WithJsonOutputsLineColumnAndSnippet() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_find"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/Auth.cs", + "csharp", + "class Auth\n{\n void Guard() {}\n void Next() {}\n}\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( + ["guard", "--db", dbPath, "--path", "src/Auth.cs", "--json", "--before", "1", "--after", "1"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal("src/Auth.cs", json.GetProperty("path").GetString()); + Assert.Equal(3, json.GetProperty("line").GetInt32()); + Assert.Equal(10, json.GetProperty("column").GetInt32()); + Assert.Equal(2, json.GetProperty("start_line").GetInt32()); + Assert.Equal(4, json.GetProperty("end_line").GetInt32()); + Assert.Contains("void Guard()", json.GetProperty("snippet").GetString()); + Assert.Contains("void Next()", json.GetProperty("snippet").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunFind_SnippetLinesControlsMatchContext() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_find_snippet_lines"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/Auth.cs", + "csharp", + "line one\nline two\nvoid Guard() {}\nline four\nline five\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( + ["Guard", "--db", dbPath, "--path", "src/Auth.cs", "--snippet-lines", "5"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Contains("line one", stdout); + Assert.Contains("line five", stdout); + Assert.Contains("1 matches in 1 file", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunFind_FocusLineAndColumnRestrictMatch() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_find_focus"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/Auth.cs", + "csharp", + "target here\nno match\nother target\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( + ["target", "--db", dbPath, "--path", "src/Auth.cs", "--focus-line", "3", "--focus-column", "8"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Contains("src/Auth.cs:3:7", stdout); + Assert.DoesNotContain("src/Auth.cs:1:1", stdout); + Assert.Contains("1 matches in 1 file", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunFind_RegexMatchesAnchors() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_find_regex"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Auth.cs", "csharp", "alpha\nGuard()\nnot Guard()\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( + ["^Guard", "--regex", "--db", dbPath, "--path", "src/Auth.cs"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Contains("src/Auth.cs:2:1", stdout); + Assert.DoesNotContain("src/Auth.cs:3:5", stdout); + Assert.Contains("1 matches in 1 file", stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunFind_CountOnlyRegexAndFocusUseSameMatchingSemantics() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_find_count_regex_focus"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Auth.cs", "csharp", "Guard()\nnot Guard()\nGuardAgain()\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( + ["^Guard", "--regex", "--db", dbPath, "--path", "src/Auth.cs", "--focus-line", "3", "--focus-column", "5", "--json", "--count"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(1, json.GetProperty("count").GetInt32()); + Assert.Equal(1, json.GetProperty("files").GetInt32()); + Assert.Equal(1, json.GetProperty("file_count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunFind_CountOnlyJsonUsesFilesAsCanonicalCountAndKeepsDeprecatedAlias_Issue1423() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_find_count"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/Auth.cs", + "csharp", + "guard one\nline two\nguard three\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( + ["guard", "--db", dbPath, "--path", "src/Auth.cs", "--json", "--count"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(2, json.GetProperty("count").GetInt32()); + Assert.Equal(1, json.GetProperty("files").GetInt32()); + Assert.Equal(1, json.GetProperty("file_count").GetInt32()); + Assert.Equal(json.GetProperty("files").GetInt32(), json.GetProperty("file_count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunFind_ExactTreatsCSharpGlobalQualifiedNamesAsCanonical() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_find_exact_csharp_global"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/global.cs", + "csharp", + """ + namespace Demo; + + public class GlobalQualified + { + public void Match() + { + var value = global::Foo.Bar; + } + } + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( + ["Foo.Bar", "--db", dbPath, "--path", "src/global.cs", "--json", "--exact", "--count"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(1, json.GetProperty("count").GetInt32()); + Assert.Equal(1, json.GetProperty("files").GetInt32()); + Assert.Equal(1, json.GetProperty("file_count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunFind_ExactTreatsKotlinBacktickedIdentifiersAsCanonical() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_find_exact_kotlin_backticks"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/App.kt", + "kotlin", + """ + fun `when`() { + println("ok") + } + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( + ["when", "--db", dbPath, "--path", "src/App.kt", "--json", "--exact", "--count"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(1, json.GetProperty("count").GetInt32()); + Assert.Equal(1, json.GetProperty("files").GetInt32()); + Assert.Equal(1, json.GetProperty("file_count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunFind_CountOnlyJsonCountsEverySameLineOccurrence() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_find_multi_count"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/Sample.cs", + "csharp", + "alpha alpha alpha\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( + ["alpha", "--db", dbPath, "--path", "src/Sample.cs", "--json", "--count"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(3, json.GetProperty("count").GetInt32()); + Assert.Equal(1, json.GetProperty("files").GetInt32()); + Assert.Equal(1, json.GetProperty("file_count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunFind_CountOnlyJsonCountsOverlappingOccurrences() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_find_overlap_count"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/Sample.cs", + "csharp", + "// banana\n"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( + ["ana", "--db", dbPath, "--path", "src/Sample.cs", "--json", "--count"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(2, json.GetProperty("count").GetInt32()); + Assert.Equal(1, json.GetProperty("files").GetInt32()); + Assert.Equal(1, json.GetProperty("file_count").GetInt32()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunExcerpt_RequiresStartLine() + { + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunExcerpt( + ["src/app.cs"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("Error: excerpt requires --start ", stderr); + } + + [Fact] + public void RunExcerpt_RejectsStartGreaterThanEnd() + { + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunExcerpt( + ["src/app.cs", "--start", "5", "--end", "3"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("--start (5) must be less than or equal to --end (3)", stderr); + } + + [Fact] + public void RunExcerpt_AcceptsMcpStyleStartAndEndLineAliases() + { + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunExcerpt( + ["src/app.cs", "--start-line", "5", "--end-line", "3"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("--start (5) must be less than or equal to --end (3)", stderr); + Assert.DoesNotContain("unsupported option", stderr, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void RunFind_WhitespaceQueryReturnsDistinctUsageError() + { + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( + [" ", "--path", "src/**/*.cs"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("Error: find query cannot be empty or whitespace-only", stderr); + Assert.DoesNotContain("find requires a query argument", stderr); + } + + [Fact] + public void RunFind_MissingQueryStillReportsRequiresArgument() + { + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( + ["--path", "src/**/*.cs"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("Error: find requires a query argument", stderr); + Assert.DoesNotContain("query cannot be empty or whitespace-only", stderr); + } + + [Fact] + public void RunSearch_ZeroResultsHonorsStaleAfterEnvironment() + { + var prior = Environment.GetEnvironmentVariable(QueryCommandRunner.StaleAfterEnvironmentVariable); + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_search_stale_after_env"); + try + { + Environment.SetEnvironmentVariable(QueryCommandRunner.StaleAfterEnvironmentVariable, "1m"); + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", "class App {}\n"); + using (var db = new DbContext(dbPath)) + { + using var cmd = db.Connection.CreateCommand(); + cmd.CommandText = "UPDATE files SET indexed_at = @indexedAt"; + cmd.Parameters.AddWithValue("@indexedAt", DateTime.UtcNow.AddMinutes(-5).ToString("O")); + cmd.ExecuteNonQuery(); + } + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["MissingSymbol", "--db", dbPath], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stdout); + Assert.Contains("threshold: 1m", stderr); + } + finally + { + Environment.SetEnvironmentVariable(QueryCommandRunner.StaleAfterEnvironmentVariable, prior); + TestProjectHelper.DeleteDirectory(projectRoot); + } + } +} diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerStatusReadinessTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerStatusReadinessTests.cs new file mode 100644 index 0000000000..2ccfc74605 --- /dev/null +++ b/tests/CodeIndex.Tests/QueryCommandRunnerStatusReadinessTests.cs @@ -0,0 +1,52 @@ +using System.Text.Json; +using CodeIndex.Cli; +using CodeIndex.Database; +using CodeIndex.Models; +using Microsoft.Data.Sqlite; + +namespace CodeIndex.Tests; + +public partial class QueryCommandRunnerTests +{ + [Fact] + public void RunStatus_Json_ReportsHotspotFamilyReadinessDegradationRebuild_2959() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_status_hotspots_family_degradation_2959"); + try + { + var dbPath = CreateHotspotFamilyFixtureDb(projectRoot, markHotspotFamilyReady: false); + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--db", dbPath, "--json"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + var hotspotDegradation = json.GetProperty("readiness_degradations") + .EnumerateArray() + .Single(item => item.GetProperty("field").GetString() == "hotspot_family_ready"); + Assert.Equal("hotspot_family_ready=false", hotspotDegradation.GetProperty("root_cause").GetString()); + Assert.Contains("--rebuild", hotspotDegradation.GetProperty("recommended_action").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunStatus_Explain_HotspotFamilyReadyRecommendsRebuild() + { + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( + ["--explain", "hotspot_family_ready"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Contains("Hotspot family contract (hotspot_family_ready)", stdout); + Assert.Contains("Remediation:", stdout); + Assert.Contains("cdidx index --rebuild", stdout); + Assert.Contains("every indexed row", stdout); + } +} diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerSymbolTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerSymbolTests.cs new file mode 100644 index 0000000000..75ed5faa50 --- /dev/null +++ b/tests/CodeIndex.Tests/QueryCommandRunnerSymbolTests.cs @@ -0,0 +1,5139 @@ +using System.Text.Json; +using CodeIndex.Cli; +using CodeIndex.Database; +using CodeIndex.Models; +using Microsoft.Data.Sqlite; + +namespace CodeIndex.Tests; + +public partial class QueryCommandRunnerTests +{ + [Fact] + public void RunSymbols_ExactNameFindsPythonDottedImportPrefixes() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_symbols_python_dotted_import_prefix"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/app.py", + "python", + """ + import package.submodule as alias + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSymbols( + ["package", "--db", dbPath, "--lang", "python", "--exact-name", "--count"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal("1", stdout.Trim()); + Assert.Equal(string.Empty, stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSymbols_ExactNameFindsPythonFromImportQualifiedNames() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_symbols_python_from_import_qualified"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/app.py", + "python", + """ + from package import submodule as alias + from .helpers import build + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSymbols( + ["package.submodule", "--db", dbPath, "--lang", "python", "--exact-name", "--count"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal("1", stdout.Trim()); + Assert.Equal(string.Empty, stderr); + + var (relativeExitCode, relativeStdout, relativeStderr) = CaptureConsole(() => QueryCommandRunner.RunSymbols( + ["helpers.build", "--db", dbPath, "--lang", "python", "--exact-name", "--count"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, relativeExitCode); + Assert.Equal("1", relativeStdout.Trim()); + Assert.Equal(string.Empty, relativeStderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSymbols_ExactNameFindsPythonQualifiedInitAllExports() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_python_qualified_init_all_exports"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "package/subpkg/__init__.py", + "python", + """ + __all__ = [ + "submodule", + ] + """); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSymbols( + ["package.subpkg.submodule", "--db", dbPath, "--lang", "python", "--exact-name", "--count"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal("1", stdout.Trim()); + Assert.Equal(string.Empty, stderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSymbols_ExactNameFindsWrappedXamlTypeBearingAttributes() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_xaml_wrapped_type_bearing"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "GenericPage.xaml"), + """ + + + + + """); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( + [projectRoot, "--json", "--quiet"], + _jsonOptions)); + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSymbols( + ["local:App", "--db", dbPath, "--json", "--exact-name", "--lang", "xml"], + _jsonOptions)); + + var rows = ParseJsonLines(stdout); + + Assert.Equal(CommandExitCodes.Success, indexExitCode); + Assert.Equal(string.Empty, indexStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Single(rows); + Assert.Equal("local:App", rows[0].RootElement.GetProperty("name").GetString()); + Assert.Equal("class", rows[0].RootElement.GetProperty("kind").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSymbols_ExactNameFindsPythonInitModuleAliases() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_python_init_module_aliases"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "package/subpkg/__init__.py", + "python", + """ + import submodule as module_alias + import package.submodule as external_alias + from . import helper as alias + """); + + var (moduleExitCode, moduleStdout, moduleStderr) = CaptureConsole(() => QueryCommandRunner.RunSymbols( + ["package.subpkg.module_alias", "--db", dbPath, "--lang", "python", "--exact-name", "--count"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, moduleExitCode); + Assert.Equal("1", moduleStdout.Trim()); + Assert.Equal(string.Empty, moduleStderr); + + var (moduleNameExitCode, moduleNameStdout, moduleNameStderr) = CaptureConsole(() => QueryCommandRunner.RunSymbols( + ["package.subpkg.submodule", "--db", dbPath, "--lang", "python", "--exact-name", "--count"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, moduleNameExitCode); + Assert.Equal("1", moduleNameStdout.Trim()); + Assert.Equal(string.Empty, moduleNameStderr); + + var (externalExitCode, externalStdout, externalStderr) = CaptureConsole(() => QueryCommandRunner.RunSymbols( + ["package.submodule", "--db", dbPath, "--lang", "python", "--exact-name", "--count"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, externalExitCode); + Assert.Equal("1", externalStdout.Trim()); + Assert.Equal(string.Empty, externalStderr); + + var (noisyExitCode, noisyStdout, noisyStderr) = CaptureConsole(() => QueryCommandRunner.RunSymbols( + ["package.subpkg.package.submodule", "--db", dbPath, "--lang", "python", "--exact-name", "--count"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, noisyExitCode); + Assert.Equal("0", noisyStdout.Trim()); + Assert.Equal(string.Empty, noisyStderr); + + var (aliasExitCode, aliasStdout, aliasStderr) = CaptureConsole(() => QueryCommandRunner.RunSymbols( + ["package.subpkg.alias", "--db", dbPath, "--lang", "python", "--exact-name", "--count"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, aliasExitCode); + Assert.Equal("1", aliasStdout.Trim()); + Assert.Equal(string.Empty, aliasStderr); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + // Issue #1507: command-specific value-taking flags should also surface the per-flag hint, so + // commands that wire through TryReadStringOptionValue / TryReadRawOptionValue stay consistent + // with the search-side coverage. + // Issue #1507: コマンド固有の値取りフラグも同じテーブル経由でヒントを表示する。 + [Fact] + public void RunSymbols_MissingNameValueShowsPerFlagHint_Issue1507() + { + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunSymbols(["--name"], _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("Error: --name requires a value.", stderr); + Assert.Contains("Hint: pass a literal symbol name", stderr); + Assert.Contains("--name UserService", stderr); + } + + [Fact] + public void RunDefinition_MissingKindValueShowsPerFlagHint_Issue1507() + { + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunDefinition(["QueryCommandRunner", "--kind"], _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("Error: --kind requires a value.", stderr); + Assert.Contains("Hint: pass a kind identifier", stderr); + Assert.Contains("--kind function", stderr); + } + + [Fact] + public void BuildSymbolQueryList_TreatsPipeAsLiteralNameCharacter() + { + // `|` is a legitimate character in operator symbols (C# `operator |`, etc.); it must not + // be treated as OR syntax so those names stay searchable. + // `|` は `operator |` など演算子名に出現する有効な文字。OR 構文として分割してはならない。 + var options = QueryCommandRunner.ParseArgs(["|"], jsonDefault: false); + var (queries, hadInput) = QueryCommandRunner.BuildSymbolQueryList(options); + Assert.True(hadInput); + Assert.NotNull(queries); + Assert.Equal(new[] { "|" }, queries!); + + var compound = QueryCommandRunner.ParseArgs(["operator|"], jsonDefault: false); + var (compoundQueries, _) = QueryCommandRunner.BuildSymbolQueryList(compound); + Assert.Equal(new[] { "operator|" }, compoundQueries!); + } + + [Fact] + public void BuildSymbolQueryList_EmptyNameNowFailsAtParseTime() + { + // Empty inline/separated string values are now rejected during argument parsing before + // symbol-query normalization runs, so they cannot broaden into an all-symbols dump. + // 空文字の値は symbol-query 正規化まで進む前に引数解析で拒否される。 + var rejected = QueryCommandRunner.ParseArgs(["--name", ""], jsonDefault: false); + Assert.NotNull(rejected.ParseError); + Assert.Contains("--name requires a value", rejected.ParseError); + } + + [Fact] + public void RunSymbols_EmptyAfterNormalizationFailsClosed() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_symbols_empty_norm"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + var (exit, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunSymbols(["--name", "", "--db", dbPath], _jsonOptions)); + Assert.Equal(1, exit); + Assert.Contains("--name requires a value", stderr); + + var (definitionExit, definitionStdout, definitionStderr) = CaptureConsole(() => QueryCommandRunner.RunDefinition( + ["@", "--db", dbPath, "--count", "--json"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, definitionExit); + Assert.Contains("bare verbatim prefixes", definitionStderr); + Assert.Equal(string.Empty, definitionStdout); + + var (referencesExit, referencesStdout, referencesStderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( + ["@", "--db", dbPath, "--count", "--json"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, referencesExit); + Assert.Contains("bare verbatim prefixes", referencesStderr); + Assert.Equal(string.Empty, referencesStdout); + + var (callersExit, callersStdout, callersStderr) = CaptureConsole(() => QueryCommandRunner.RunCallers( + ["@", "--db", dbPath, "--count", "--json"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, callersExit); + Assert.Contains("bare verbatim prefixes", callersStderr); + Assert.Equal(string.Empty, callersStdout); + + var (calleesExit, calleesStdout, calleesStderr) = CaptureConsole(() => QueryCommandRunner.RunCallees( + ["@", "--db", dbPath, "--count", "--json"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, calleesExit); + Assert.Contains("bare verbatim prefixes", calleesStderr); + Assert.Equal(string.Empty, calleesStdout); + + var (impactExit, impactStdout, impactStderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( + ["@", "--db", dbPath, "--count", "--json"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, impactExit); + Assert.Contains("bare verbatim prefixes", impactStderr); + Assert.Equal(string.Empty, impactStdout); + + var (inspectExit, inspectStdout, inspectStderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( + ["@", "--db", dbPath, "--json"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, inspectExit); + Assert.Contains("bare verbatim prefixes", inspectStderr); + Assert.Equal(string.Empty, inspectStdout); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSymbols_ExactNameFindsXamlTargetType() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_xaml_target_type"); + try + { + Directory.CreateDirectory(Path.Combine(projectRoot, "src")); + File.WriteAllText( + Path.Combine(projectRoot, "src", "MainPage.xaml"), + """ + + + - - """); + [Theory] + [InlineData("search-extra", "unexpected extra positional 1 argument for search")] + [InlineData("excerpt-extra", "unexpected extra positional 1 argument for excerpt")] + [InlineData("map-extra", "map does not accept positional arguments")] + [InlineData("outline-extra", "outline does not accept positional arguments")] + [InlineData("status-extra", "status does not accept positional arguments")] + [InlineData("validate-extra", "validate does not accept positional arguments")] + [InlineData("languages-extra", "languages does not accept positional arguments")] + public void QueryEntrypoints_UnexpectedPositionalsReturnUsageError(string scenario, string expectedError) + { + var (exitCode, _, stderr) = CaptureConsole(() => RunCommandWithUnexpectedPositionals(scenario)); - var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); - var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( - [projectRoot, "--json", "--quiet"], - _jsonOptions)); - var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSymbols( - ["local:App", "--db", dbPath, "--json", "--exact-name", "--lang", "xml"], - _jsonOptions)); + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains(expectedError, stderr); + Assert.DoesNotContain("database not found", stderr); + } - var rows = ParseJsonLines(stdout); - Assert.Equal(CommandExitCodes.Success, indexExitCode); - Assert.Equal(string.Empty, indexStderr); - Assert.Equal(CommandExitCodes.Success, exitCode); - Assert.Equal(string.Empty, stderr); - Assert.Single(rows); - Assert.Equal("local:App", rows[0].RootElement.GetProperty("name").GetString()); - Assert.Equal("class", rows[0].RootElement.GetProperty("kind").GetString()); - } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); - } - } - [Fact] - public void RunSymbols_ExactNameFindsPythonInitModuleAliases() + + + + [Theory] + [InlineData("--db", "/tmp/does-not-matter.db")] + [InlineData("--db=")] + [InlineData("--mystery")] + public void RunLanguages_UnsupportedOptionsReturnUsageError(string flag, string? value = null) { - var projectRoot = TestProjectHelper.CreateTempProject("cdidx_python_init_module_aliases"); - try - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); - TestProjectHelper.InsertIndexedFile( - dbPath, - "package/subpkg/__init__.py", - "python", - """ - import submodule as module_alias - import package.submodule as external_alias - from . import helper as alias - """); + var args = value == null + ? new[] { flag } + : new[] { flag, value }; - var (moduleExitCode, moduleStdout, moduleStderr) = CaptureConsole(() => QueryCommandRunner.RunSymbols( - ["package.subpkg.module_alias", "--db", dbPath, "--lang", "python", "--exact-name", "--count"], - _jsonOptions)); + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunLanguages(args, _jsonOptions)); - Assert.Equal(CommandExitCodes.Success, moduleExitCode); - Assert.Equal("1", moduleStdout.Trim()); - Assert.Equal(string.Empty, moduleStderr); + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains($"Error: {flag} is not supported for languages.", stderr); + Assert.Contains($"Usage: {ConsoleUi.GetUsageLine("languages")}", stderr); + Assert.DoesNotContain("requires a value", stderr); + Assert.DoesNotContain("Warning: unknown option", stderr); + } - var (moduleNameExitCode, moduleNameStdout, moduleNameStderr) = CaptureConsole(() => QueryCommandRunner.RunSymbols( - ["package.subpkg.submodule", "--db", dbPath, "--lang", "python", "--exact-name", "--count"], - _jsonOptions)); + [Fact] + public void RunLanguages_JsonListsModernNodeModuleExtensions() + { + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunLanguages(["--json"], _jsonOptions)); - Assert.Equal(CommandExitCodes.Success, moduleNameExitCode); - Assert.Equal("1", moduleNameStdout.Trim()); - Assert.Equal(string.Empty, moduleNameStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); - var (externalExitCode, externalStdout, externalStderr) = CaptureConsole(() => QueryCommandRunner.RunSymbols( - ["package.submodule", "--db", dbPath, "--lang", "python", "--exact-name", "--count"], - _jsonOptions)); + using var document = ParseJsonOutput(stdout); + var languages = document.RootElement.GetProperty("languages"); + var javascript = languages.EnumerateArray().First(lang => lang.GetProperty("lang").GetString() == "javascript"); + var typescript = languages.EnumerateArray().First(lang => lang.GetProperty("lang").GetString() == "typescript"); + var objc = languages.EnumerateArray().First(lang => lang.GetProperty("lang").GetString() == "objc"); - Assert.Equal(CommandExitCodes.Success, externalExitCode); - Assert.Equal("1", externalStdout.Trim()); - Assert.Equal(string.Empty, externalStderr); + Assert.Contains(".cjs", javascript.GetProperty("extensions").EnumerateArray().Select(ext => ext.GetString())); + Assert.Contains(".mjs", javascript.GetProperty("extensions").EnumerateArray().Select(ext => ext.GetString())); + Assert.Contains("js", javascript.GetProperty("aliases").EnumerateArray().Select(alias => alias.GetString())); + Assert.Contains("jsx", javascript.GetProperty("aliases").EnumerateArray().Select(alias => alias.GetString())); + Assert.Contains(".cts", typescript.GetProperty("extensions").EnumerateArray().Select(ext => ext.GetString())); + Assert.Contains(".mts", typescript.GetProperty("extensions").EnumerateArray().Select(ext => ext.GetString())); + Assert.Contains(".m", objc.GetProperty("extensions").EnumerateArray().Select(ext => ext.GetString())); + Assert.Contains(".mm", objc.GetProperty("extensions").EnumerateArray().Select(ext => ext.GetString())); + } - var (noisyExitCode, noisyStdout, noisyStderr) = CaptureConsole(() => QueryCommandRunner.RunSymbols( - ["package.subpkg.package.submodule", "--db", dbPath, "--lang", "python", "--exact-name", "--count"], - _jsonOptions)); + [Fact] + public void RunLanguages_JsonListsHtmlWithSymbolExtractionAndAllExtensions() + { + // Pin the #215 surface: `cdidx languages --json` must report html with + // symbol_extraction=true and list all four extensions (.html, .htm, .xhtml, .shtml) + // so AI tools can discover HTML symbol support without indexing first. + // #215 の表面契約を pin: `cdidx languages --json` は html を symbol_extraction=true で + // 返し、`.html` / `.htm` / `.xhtml` / `.shtml` の 4 拡張子を列挙する必要がある。 + // AI ツールがインデックス前でも HTML のシンボル対応を検出できるようにするため。 + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunLanguages(["--json"], _jsonOptions)); - Assert.Equal(CommandExitCodes.Success, noisyExitCode); - Assert.Equal("0", noisyStdout.Trim()); - Assert.Equal(string.Empty, noisyStderr); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); - var (aliasExitCode, aliasStdout, aliasStderr) = CaptureConsole(() => QueryCommandRunner.RunSymbols( - ["package.subpkg.alias", "--db", dbPath, "--lang", "python", "--exact-name", "--count"], - _jsonOptions)); + using var document = ParseJsonOutput(stdout); + var languages = document.RootElement.GetProperty("languages"); + var html = languages.EnumerateArray().First(lang => lang.GetProperty("lang").GetString() == "html"); - Assert.Equal(CommandExitCodes.Success, aliasExitCode); - Assert.Equal("1", aliasStdout.Trim()); - Assert.Equal(string.Empty, aliasStderr); - } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); - } + Assert.True(html.GetProperty("symbol_extraction").GetBoolean()); + var extensions = html.GetProperty("extensions").EnumerateArray().Select(ext => ext.GetString()).ToList(); + Assert.Contains(".html", extensions); + Assert.Contains(".htm", extensions); + Assert.Contains(".xhtml", extensions); + Assert.Contains(".shtml", extensions); } [Fact] - public void RunPublishedTrimmedCli_SerializesQueryJsonAndErrorJson() + public void RunLanguages_JsonListsAssemblyWithSymbolExtractionGraphAndAliases() { - var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_trimmed_publish"); - var publishDir = Path.Combine(Path.GetTempPath(), $"cdidx_query_trimmed_publish_{Guid.NewGuid():N}"); - try - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); - var queryToken = "TrimmedPublishNeedle_1a2b3c"; - TestProjectHelper.InsertIndexedFile(dbPath, "src/app.cs", "csharp", $"class App {{ void Run() {{ var {queryToken} = 1; }} }}\n"); - - var publishedDll = PublishTrimmedCli(publishDir); + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunLanguages(["--json"], _jsonOptions)); - var (searchExitCode, searchStdOut, searchStdErr) = RunPublishedCli(publishedDll, publishDir, "search", queryToken, "--db", dbPath, "--count", "--json"); - Assert.Equal(CommandExitCodes.Success, searchExitCode); - Assert.Equal(string.Empty, searchStdErr); - using (var searchJson = JsonDocument.Parse(searchStdOut)) - { - Assert.True(searchJson.RootElement.GetProperty("count").GetInt32() >= 1); - Assert.Equal(1, searchJson.RootElement.GetProperty("files").GetInt32()); - } + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); - var (findExitCode, findStdOut, findStdErr) = RunPublishedCli(publishedDll, publishDir, "find", queryToken, "--db", dbPath, "--path", "src/**", "--count", "--json"); - Assert.Equal(CommandExitCodes.Success, findExitCode); - Assert.Equal(string.Empty, findStdErr); - using (var findJson = JsonDocument.Parse(findStdOut)) - { - Assert.True(findJson.RootElement.GetProperty("count").GetInt32() >= 1); - Assert.Equal(1, findJson.RootElement.GetProperty("files").GetInt32()); - Assert.Equal(1, findJson.RootElement.GetProperty("file_count").GetInt32()); - } + using var document = ParseJsonOutput(stdout); + var languages = document.RootElement.GetProperty("languages"); + var assembly = languages.EnumerateArray().First(lang => lang.GetProperty("lang").GetString() == "assembly"); - var (symbolsExitCode, symbolsStdOut, symbolsStdErr) = RunPublishedCli(publishedDll, publishDir, "symbols", "@", "--db", dbPath, "--lang", "csharp", "--exact", "--count", "--json"); - Assert.Equal(CommandExitCodes.Success, symbolsExitCode); - Assert.Equal(string.Empty, symbolsStdErr); - using (var symbolsJson = JsonDocument.Parse(symbolsStdOut)) - { - Assert.Equal(0, symbolsJson.RootElement.GetProperty("count").GetInt32()); - Assert.Equal(0, symbolsJson.RootElement.GetProperty("files").GetInt32()); - } + Assert.True(assembly.GetProperty("symbol_extraction").GetBoolean()); + Assert.True(assembly.GetProperty("graph_queries").GetBoolean()); - var (validateExitCode, validateStdOut, validateStdErr) = RunPublishedCli(publishedDll, publishDir, "validate", "--db", dbPath, "--json"); - Assert.Equal(CommandExitCodes.Success, validateExitCode); - Assert.Equal(string.Empty, validateStdErr); - using (var validateJson = JsonDocument.Parse(validateStdOut)) - { - Assert.Equal(0, validateJson.RootElement.GetProperty("count").GetInt32()); - Assert.True(validateJson.RootElement.GetProperty("issues").ValueKind is JsonValueKind.Array); - } + var extensions = assembly.GetProperty("extensions").EnumerateArray().Select(ext => ext.GetString()).ToList(); + Assert.Contains(".s", extensions); + Assert.Contains(".S", extensions); + Assert.Contains(".asm", extensions); + Assert.Contains(".nasm", extensions); - var (outlineExitCode, outlineStdOut, outlineStdErr) = RunPublishedCli(publishedDll, publishDir, "outline", "src/missing.cs", "--db", dbPath, "--json"); - Assert.Equal(CommandExitCodes.NotFound, outlineExitCode); - Assert.Equal(string.Empty, outlineStdErr); - using (var outlineJson = JsonDocument.Parse(outlineStdOut)) - { - Assert.Equal("src/missing.cs", outlineJson.RootElement.GetProperty("path").GetString()); - Assert.Equal("file not found in index", outlineJson.RootElement.GetProperty("error").GetString()); - } - } - finally - { - SqliteConnection.ClearAllPools(); - TestProjectHelper.DeleteDirectory(projectRoot); - TestProjectHelper.DeleteDirectory(publishDir); - } + var aliases = assembly.GetProperty("aliases").EnumerateArray().Select(alias => alias.GetString()).ToList(); + Assert.Contains("asm", aliases); + Assert.Contains("assembler", aliases); + Assert.Contains("gas", aliases); + Assert.Contains("gnuasm", aliases); + Assert.Contains("gnu assembler", aliases); } - [SkipOnMacOsArm64Theory] - [InlineData("cshtml")] - [InlineData("razor")] - public void RunPublishedTrimmedCli_SearchSupportsCSharpRazorAliases(string lang) + [Fact] + public void RunLanguages_JsonListsCSharpRazorAliases() { - var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_trimmed_lang_alias_publish"); - var publishDir = Path.Combine(Path.GetTempPath(), $"cdidx_query_trimmed_lang_alias_publish_{Guid.NewGuid():N}"); - try - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); - var queryToken = $"TrimmedPublishLangAliasNeedle_{Guid.NewGuid():N}"; - TestProjectHelper.InsertIndexedFile( - dbPath, - "web/Views/Home/Index.cshtml", - "csharp", - $$""" - @{ - var marker = "{{queryToken}}"; - } - """); - - var publishedDll = PublishTrimmedCli(publishDir); + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunLanguages(["--json"], _jsonOptions)); - var (exitCode, stdout, stderr) = RunPublishedCli(publishedDll, publishDir, "search", queryToken, "--db", dbPath, "--lang", lang, "--count", "--json"); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); - Assert.Equal(CommandExitCodes.Success, exitCode); - Assert.Equal(string.Empty, stderr); + using var document = ParseJsonOutput(stdout); + var languages = document.RootElement.GetProperty("languages"); + var csharp = languages.EnumerateArray().First(lang => lang.GetProperty("lang").GetString() == "csharp"); + var aliases = csharp.GetProperty("aliases").EnumerateArray().Select(alias => alias.GetString()).ToList(); - using var document = JsonDocument.Parse(stdout); - Assert.Equal(1, document.RootElement.GetProperty("count").GetInt32()); - Assert.Equal(1, document.RootElement.GetProperty("files").GetInt32()); - } - finally - { - SqliteConnection.ClearAllPools(); - TestProjectHelper.DeleteDirectory(projectRoot); - TestProjectHelper.DeleteDirectory(publishDir); - } + Assert.Contains("cshtml", aliases); + Assert.Contains("razor", aliases); } - [Theory] - [InlineData("definition", "--focus-column", "10")] - [InlineData("definition", "--max-line-width", "10")] - [InlineData("search", "--focus-column", "10")] - [InlineData("symbols", "--max-line-width", "10")] - public void QueryCommands_RejectPreviewOptionsWhenUnsupported(string command, string option, string value) + [Fact] + public void RunLanguages_Json_SearchOnlyBucketsAdvertiseZeroSymbolAndGraphSupport() { - var projectRoot = TestProjectHelper.CreateTempProject($"cdidx_preview_reject_{command}"); - try - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); - var args = new List(); - switch (command) - { - case "definition": - case "search": - case "symbols": - args.AddRange(["QueryCommandRunner", "--db", dbPath, option, value, "--count"]); - break; - } - - var (exitCode, _, stderr) = command switch - { - "definition" => CaptureConsole(() => QueryCommandRunner.RunDefinition([.. args], _jsonOptions)), - "search" => CaptureConsole(() => QueryCommandRunner.RunSearch([.. args], _jsonOptions)), - "symbols" => CaptureConsole(() => QueryCommandRunner.RunSymbols([.. args], _jsonOptions)), - _ => throw new InvalidOperationException($"Unexpected command: {command}") - }; - - Assert.Equal(CommandExitCodes.UsageError, exitCode); - Assert.Contains($"{option} is not supported for {command}", stderr); - } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); - } - } - - [Fact] - public void RunSearch_ZeroResultsHumanOutputIncludesQueryFilterContext() - { - var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_zero_context_human"); - try - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); - TestProjectHelper.InsertIndexedFile( - dbPath, - "src/App.cs", - "csharp", - "public sealed class App { }"); + // Search-only languages that intentionally live outside the Python / CSS extractors + // (Cython .pyx/.pxd, Sass .sass, Stylus .styl, and the newly added extension-only + // languages) must advertise + // symbol_extraction=false / graph_queries=false so AI clients can tell the difference + // between "indexed with symbols" and "indexed for search only". + // 意図的に Python / CSS 抽出器の対象外になっている search-only 言語 + // (Cython の .pyx/.pxd、Sass の .sass、Stylus の .styl、そして新規追加の + // 拡張子ベース言語)は、 + // symbol_extraction=false / graph_queries=false で広告しなければならない。 + // こうしないと、AI クライアントが「シンボル付きインデックス」と「検索のみインデックス」を区別できない。 + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunLanguages(["--json"], _jsonOptions)); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); - var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( - ["missing-token", "--db", dbPath, "--path", "src/**", "--lang", "csharp", "--limit", "7"], - _jsonOptions)); + using var document = ParseJsonOutput(stdout); + var languages = document.RootElement.GetProperty("languages").EnumerateArray() + .ToDictionary(entry => entry.GetProperty("lang").GetString()!, entry => entry); - Assert.Equal(CommandExitCodes.Success, exitCode); - Assert.Equal(string.Empty, stdout); - Assert.Contains("No results found. (query: \"missing-token\", path: src/**, lang: csharp, limit: 7)", stderr); - } - finally + foreach (var searchOnly in new[] { "cython", "sass", "stylus", "msbuild" }) { - TestProjectHelper.DeleteDirectory(projectRoot); + Assert.True(languages.ContainsKey(searchOnly), $"expected '{searchOnly}' to be listed"); + var entry = languages[searchOnly]; + Assert.False(entry.GetProperty("symbol_extraction").GetBoolean(), + $"{searchOnly} must advertise symbol_extraction=false"); + Assert.False(entry.GetProperty("graph_queries").GetBoolean(), + $"{searchOnly} must advertise graph_queries=false"); } - } - - [Fact] - public void RunSearch_ZeroResultsJsonIncludesStructuredQueryContext() - { - var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_zero_context_json"); - try - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); - TestProjectHelper.InsertIndexedFile( - dbPath, - "src/App.cs", - "csharp", - "public sealed class App { }"); - - var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( - ["missing-token", "--db", dbPath, "--path", "src/**", "--lang", "csharp", "--limit", "7", "--json"], - _jsonOptions)); - Assert.Equal(CommandExitCodes.Success, exitCode); - Assert.Equal(string.Empty, stderr); - using var document = ParseJsonOutput(stdout); - var root = document.RootElement; - var queryContext = root.GetProperty("query_context"); - - Assert.Equal(0, root.GetProperty("count").GetInt32()); - Assert.Equal("missing-token", root.GetProperty("query").GetString()); - Assert.Equal("missing-token", queryContext.GetProperty("text").GetString()); - Assert.Equal("src/**", queryContext.GetProperty("path")[0].GetString()); - Assert.Equal("csharp", queryContext.GetProperty("lang").GetString()); - Assert.Equal(7, queryContext.GetProperty("limit").GetInt32()); - } - finally + foreach (var searchOnly in new[] { "crystal", "clojure", "d", "erlang", "julia", "nim", "ocaml", "solidity", "tcl" }) { - TestProjectHelper.DeleteDirectory(projectRoot); + Assert.True(languages.ContainsKey(searchOnly), $"expected '{searchOnly}' to be listed"); + var entry = languages[searchOnly]; + Assert.False(entry.GetProperty("symbol_extraction").GetBoolean(), + $"{searchOnly} must advertise symbol_extraction=false"); + Assert.False(entry.GetProperty("graph_queries").GetBoolean(), + $"{searchOnly} must advertise graph_queries=false"); } - } - - [Fact] - public void RunSearch_AllowsPathValueThatLooksLikePreviewOption() - { - var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_preview_like_path_value"); - try - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); - var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( - ["foo", "--db", dbPath, "--path=--max-line-width", "--count"], - _jsonOptions)); + var yamlAliases = languages["yaml"].GetProperty("aliases").EnumerateArray() + .Select(alias => alias.GetString()).ToList(); + Assert.Contains("yml", yamlAliases); - Assert.Equal(CommandExitCodes.Success, exitCode); - Assert.Equal("0", stdout.Trim()); - Assert.DoesNotContain("is not supported", stderr); - } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); - } - } + Assert.True(languages.ContainsKey("perl"), "expected 'perl' to be listed"); + Assert.True(languages["perl"].GetProperty("symbol_extraction").GetBoolean(), + "perl must advertise symbol_extraction=true"); + Assert.True(languages["perl"].GetProperty("graph_queries").GetBoolean(), + "perl must advertise graph_queries=true"); - [Fact] - public void RunReferences_AllowsExcludePathValueThatLooksLikePreviewOption() - { - var projectRoot = TestProjectHelper.CreateTempProject("cdidx_references_preview_like_exclude_path_value"); - try - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + // Cython owns .pyx / .pxd exclusively; python keeps .py / .pyi / .pyw and Bazel filenames. + // Cython は .pyx / .pxd を専有し、python は .py / .pyi / .pyw と Bazel ファイル名を維持。 + var cythonExts = languages["cython"].GetProperty("extensions").EnumerateArray() + .Select(ext => ext.GetString()).ToList(); + Assert.Contains(".pyx", cythonExts); + Assert.Contains(".pxd", cythonExts); - var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( - ["target", "--db", dbPath, "--exclude-path=--focus-line", "--count"], - _jsonOptions)); + var pythonExts = languages["python"].GetProperty("extensions").EnumerateArray() + .Select(ext => ext.GetString()).ToList(); + Assert.DoesNotContain(".pyx", pythonExts); + Assert.DoesNotContain(".pxd", pythonExts); + Assert.Contains(".py", pythonExts); + Assert.Contains(".pyi", pythonExts); - Assert.Equal(CommandExitCodes.Success, exitCode); - Assert.Equal("0", stdout.Trim()); - Assert.DoesNotContain("is not supported", stderr); - } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); - } - } + var ocamlExts = languages["ocaml"].GetProperty("extensions").EnumerateArray() + .Select(ext => ext.GetString()).ToList(); + Assert.Contains(".ml", ocamlExts); + Assert.Contains(".mli", ocamlExts); - [Fact] - public void RunInspect_AllowsPathValueThatLooksLikePreviewOption() - { - var projectRoot = TestProjectHelper.CreateTempProject("cdidx_inspect_preview_like_path_value"); - try - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + var clojureExts = languages["clojure"].GetProperty("extensions").EnumerateArray() + .Select(ext => ext.GetString()).ToList(); + Assert.Contains(".clj", clojureExts); + Assert.Contains(".cljs", clojureExts); + Assert.Contains(".cljc", clojureExts); + Assert.Contains(".edn", clojureExts); - var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( - ["target", "--db", dbPath, "--path=--max-line-width", "--json"], - _jsonOptions)); + var perlExts = languages["perl"].GetProperty("extensions").EnumerateArray() + .Select(ext => ext.GetString()).ToList(); + Assert.Contains(".pl", perlExts); + Assert.Contains(".pm", perlExts); + Assert.Contains(".pod", perlExts); + Assert.Contains(".t", perlExts); - Assert.NotEqual(CommandExitCodes.UsageError, exitCode); - Assert.DoesNotContain("is not supported", stderr); - } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); - } + var msbuildExts = languages["msbuild"].GetProperty("extensions").EnumerateArray() + .Select(ext => ext.GetString()).ToList(); + Assert.Contains(".csproj", msbuildExts); + Assert.Contains(".fsproj", msbuildExts); + Assert.Contains(".vbproj", msbuildExts); + Assert.Contains(".props", msbuildExts); + Assert.Contains(".targets", msbuildExts); + Assert.DoesNotContain(".csproj", languages["xml"].GetProperty("extensions").EnumerateArray().Select(ext => ext.GetString())); } [Fact] - public void RunOutline_IndentsByContainerDepth() + public void RunLanguages_HumanOutput_WideExtensionListSpillsOntoContinuationLine() { - var projectRoot = TestProjectHelper.CreateTempProject("cdidx_outline_depth"); - try - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); - TestProjectHelper.InsertIndexedFile( - dbPath, - "src/deep.cs", - "csharp", - """ - namespace OuterNs - { - namespace InnerNs - { - public class OuterClass - { - public class NestedClass - { - public class DeeplyNested - { - public void Method() { } - } - } - } - } - } - """); + // The human-readable table must not let long extension lists (dockerfile / makefile / + // python / ruby / xml / msbuild) swallow the Symbols / Graph columns. Instead, spill onto a + // continuation line so the row is still readable. + // 人間向けテーブルは、長い拡張子リスト(dockerfile / makefile / python / ruby / xml / msbuild)が + // Symbols / Graph 列を食い潰さないようにし、継続行へ退避させて可読性を保つこと。 + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunLanguages([], _jsonOptions)); - var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunOutline( - ["src/deep.cs", "--db", dbPath], - _jsonOptions)); + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Contains("languages)", stderr); - Assert.Equal(CommandExitCodes.Success, exitCode); - Assert.Equal(string.Empty, stderr); + var lines = stdout.Split('\n').Select(line => line.TrimEnd('\r')).ToArray(); - var lines = stdout.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); - var outer = lines.Single(line => line.Contains("namespace OuterNs", StringComparison.Ordinal)); - var inner = lines.Single(line => line.Contains("namespace InnerNs", StringComparison.Ordinal)); - var outerClass = lines.Single(line => line.Contains("public class OuterClass", StringComparison.Ordinal)); - var nestedClass = lines.Single(line => line.Contains("public class NestedClass", StringComparison.Ordinal)); - var deeplyNested = lines.Single(line => line.Contains("public class DeeplyNested", StringComparison.Ordinal)); - var method = lines.Single(line => line.Contains("public void Method()", StringComparison.Ordinal)); - - var outerIndex = outer.IndexOf("namespace OuterNs", StringComparison.Ordinal); - Assert.Equal(4, inner.IndexOf("namespace InnerNs", StringComparison.Ordinal) - outerIndex); - Assert.Equal(4, outerClass.IndexOf("public class OuterClass", StringComparison.Ordinal) - inner.IndexOf("namespace InnerNs", StringComparison.Ordinal)); - Assert.Equal(4, nestedClass.IndexOf("public class NestedClass", StringComparison.Ordinal) - outerClass.IndexOf("public class OuterClass", StringComparison.Ordinal)); - Assert.Equal(4, deeplyNested.IndexOf("public class DeeplyNested", StringComparison.Ordinal) - nestedClass.IndexOf("public class NestedClass", StringComparison.Ordinal)); - Assert.Equal(4, method.IndexOf("public void Method()", StringComparison.Ordinal) - deeplyNested.IndexOf("public class DeeplyNested", StringComparison.Ordinal)); - } - finally + // Rows with long extension / alias lists must spill onto a continuation line so the + // Symbols / Graph columns stay readable. + // 拡張子や alias が長い行は継続行に退避し、Symbols / Graph 列の可読性を保つ。 + var wideLangs = new[] { "csharp", "dockerfile", "makefile", "python", "ruby", "msbuild" }; + foreach (var wide in wideLangs) { - TestProjectHelper.DeleteDirectory(projectRoot); + var headerIndex = Array.FindIndex(lines, line => line.StartsWith($"{wide} ", StringComparison.Ordinal)); + Assert.True(headerIndex >= 0, $"expected row for {wide}"); + var header = lines[headerIndex]; + // Header row carries only lang + sym + graph, never the extension list itself. + // ヘッダ行には言語名・シンボル・グラフのみが含まれ、拡張子文字列は含まれない。 + Assert.DoesNotContain("Dockerfile", header); + Assert.DoesNotContain("Makefile", header); + Assert.DoesNotContain("WORKSPACE", header); + Assert.DoesNotContain("Gemfile", header); + Assert.DoesNotContain(".csproj", header); + var continuation = lines[headerIndex + 1]; + Assert.StartsWith(" Extensions: ", continuation); } - } - - [Fact] - public void RunExcerpt_RejectsMissingFocusColumnValue() - { - var projectRoot = TestProjectHelper.CreateTempProject("cdidx_excerpt_missing_focus_column"); - try - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); - TestProjectHelper.InsertIndexedFile(dbPath, "README.md", "markdown", "sample"); - var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunExcerpt( - ["README.md", "--db", dbPath, "--start", "1", "--focus-column", "--json"], - _jsonOptions)); + var yamlIndex = Array.FindIndex(lines, line => line.StartsWith("yaml ", StringComparison.Ordinal)); + Assert.True(yamlIndex >= 0, "expected row for yaml"); + Assert.Contains("yml", lines[yamlIndex]); - Assert.Equal(CommandExitCodes.UsageError, exitCode); - // The recognized-option guard in TryReadRawOptionValue short-circuits `--focus-column --json` - // as a missing-value case before TryParsePositiveInt runs, so the error is "requires a value" - // rather than the older TryParsePositiveInt-level "requires a positive integer" message. - // `--focus-column --json` は TryReadRawOptionValue の既知オプション判定で TryParsePositiveInt - // 実行前に値欠如として短絡するため、旧メッセージではなく "requires a value" となる。 - Assert.Contains("--focus-column requires a value", stderr); - } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); - } + // Spot-check: the continuation line for dockerfile contains both the bare filename and the + // `` pseudo-entry added for Issue #189 follow-up. + // dockerfile 継続行に完全一致ファイル名と `` 擬似エントリが両方入る。 + var dockerIndex = Array.FindIndex(lines, line => line.StartsWith("dockerfile ", StringComparison.Ordinal)); + var dockerContinuation = lines[dockerIndex + 1]; + Assert.Contains("Dockerfile ", dockerContinuation); + Assert.Contains("Dockerfile.", dockerContinuation); + Assert.Contains("Containerfile", dockerContinuation); } - [Fact] - public void RunReferences_RejectsMissingMaxLineWidthValue() + [Theory] + [InlineData("search")] + [InlineData("definition")] + [InlineData("symbols")] + [InlineData("files")] + public void QueryEntrypoints_InvalidSinceReturnUsageError(string command) { - var projectRoot = TestProjectHelper.CreateTempProject("cdidx_references_missing_max_line_width"); - try - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + var (exitCode, _, stderr) = CaptureConsole(() => RunCommandWithInvalidSince(command)); - var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( - ["target", "--db", dbPath, "--max-line-width", "--json"], - _jsonOptions)); - - Assert.Equal(CommandExitCodes.UsageError, exitCode); - // Missing-value guard short-circuits before TryParsePositiveInt; see - // RunExcerpt_RejectsMissingFocusColumnValue for the matching contract note. - // TryParsePositiveInt より前で値欠如として短絡する。契約の詳細は上記テスト参照。 - Assert.Contains("--max-line-width requires a value", stderr); - } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); - } - } - - [Fact] - public void RunSearch_RawFtsSyntaxErrorsAreReportedAsUsageErrors() - { - var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_raw_fts_syntax_error"); - try - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); - TestProjectHelper.InsertIndexedFile(dbPath, "src/demo.cs", "csharp", "class Demo {}\n"); - - var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( - ["title:foo", "--db", dbPath, "--fts", "--count"], - _jsonOptions)); - - Assert.Equal(CommandExitCodes.UsageError, exitCode); - Assert.Contains("Error [E006_FTS_QUERY_SYNTAX]: FTS5 query syntax:", stderr); - Assert.Contains("raw FTS5 syntax", stderr); - } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); - } - } - - [Fact] - public void RunOutline_UsesNestedSymbolDepthInHumanOutput() - { - var projectRoot = TestProjectHelper.CreateTempProject("cdidx_outline_depth"); - try - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); - TestProjectHelper.InsertIndexedFile( - dbPath, - "src/deep.cs", - "csharp", - """ - namespace OuterNs - { - namespace InnerNs - { - public class OuterClass - { - public class NestedClass - { - public class DeeplyNested - { - public void Method() { } - } - } - } - } - } - """); - - var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunOutline( - ["src/deep.cs", "--db", dbPath], - _jsonOptions)); - - Assert.Equal(CommandExitCodes.Success, exitCode); - Assert.Equal(string.Empty, stderr); - var outerIndex = stdout.IndexOf("public class OuterClass", StringComparison.Ordinal); - var nestedIndex = stdout.IndexOf("public class NestedClass", StringComparison.Ordinal); - var deepIndex = stdout.IndexOf("public class DeeplyNested", StringComparison.Ordinal); - - Assert.True(outerIndex >= 0); - Assert.True(nestedIndex > outerIndex); - Assert.True(deepIndex > nestedIndex); - } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); - } - } - - [Fact] - public void RunInspect_RejectsMissingMaxLineWidthValue() - { - var projectRoot = TestProjectHelper.CreateTempProject("cdidx_inspect_missing_max_line_width"); - try - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); - - var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( - ["target", "--db", dbPath, "--max-line-width", "--json"], - _jsonOptions)); - - Assert.Equal(CommandExitCodes.UsageError, exitCode); - // Missing-value guard short-circuits before TryParsePositiveInt; see - // RunExcerpt_RejectsMissingFocusColumnValue for the matching contract note. - // TryParsePositiveInt より前で値欠如として短絡する。契約の詳細は上記テスト参照。 - Assert.Contains("--max-line-width requires a value", stderr); - } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); - } - } - - [Fact] - public void RunReferences_UsageIncludesCount() - { - var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( - [], - _jsonOptions)); - - Assert.Equal(CommandExitCodes.UsageError, exitCode); - Assert.Contains("[--count]", stderr); - } - - [Fact] - public void ParseArgs_InvalidNumbersAndUnknownOptionsAccumulateParseErrors() - { - var (options, _, stderr) = CaptureConsole(() => QueryCommandRunner.ParseArgs( - [ - "RunSearch", - "--limit", "0", - "--start", "0", - "--end", "-1", - "--before", "-2", - "--after", "-3", - "--snippet-lines", "0", - "--mystery", - ], jsonDefault: false)); - - Assert.Equal(20, options.Limit); - Assert.Null(options.StartLine); - Assert.Null(options.EndLine); - Assert.Equal(0, options.ContextBefore); - Assert.Equal(0, options.ContextAfter); - Assert.Equal(SearchSnippetFormatter.DefaultSnippetLines, options.SnippetLines); - Assert.NotNull(options.ParseError); - Assert.Contains("Error: --limit requires an integer between 1 and 10000", options.ParseError); - Assert.Contains("Hint: retry with `--limit 1` or another value up to 10000.", options.ParseError); - Assert.Contains("Error: --start requires an integer between 1 and 10000000", options.ParseError); - Assert.Contains("Error: --end requires an integer between 1 and 10000000", options.ParseError); - Assert.Contains("Error: --before requires an integer between 0 and 1000", options.ParseError); - Assert.Contains("Hint: retry with `--before 0` or another value up to 1000.", options.ParseError); - Assert.Contains("Error: --after requires an integer between 0 and 1000", options.ParseError); - Assert.Contains("Error: --snippet-lines requires an integer between 1 and 20", options.ParseError); - Assert.Equal(string.Empty, stderr); - } - - [Theory] - [InlineData("--limit", "not-a-number", "between 1 and 10000")] - [InlineData("--snippet-lines", "0", "between 1 and 20")] - [InlineData("--max-line-width", "-1", "between 0 and 4096")] - public void ParseArgs_InvalidNumericOptionsIncludeBoundsContext_Issue2071(string flag, string value, string expectedRange) - { - var options = QueryCommandRunner.ParseArgs( - ["needle", flag, value], - jsonDefault: false, - allowNamedQuery: true); - - Assert.NotNull(options.ParseError); - Assert.Contains(flag, options.ParseError!); - Assert.Contains(expectedRange, options.ParseError!); - Assert.Contains($"got '{value}'", options.ParseError!); - } - - // Regression lock for #1503: numeric CLI flags must reject values above the documented - // per-flag upper bound with a clear message naming the cap, instead of silently allowing - // `int.MaxValue` or quietly clamping (e.g. `--snippet-lines 999999` previously folded down - // to 20). The cap surfaces typos as parse errors rather than huge allocations or hidden clamps. - // #1503 の回帰ロック: 数値 CLI フラグは documented な上限を超えた値を、上限を明示した - // エラーで拒否しなければならない。以前は `int.MaxValue` まで通したり silent に clamp - // していた(例: `--snippet-lines 999999` が黙って 20 に丸められていた)。上限を超えた - // 入力をパーズエラーとして見せることで、巨大確保や隠れた clamp ではなくユーザーのタイポ - // として顕在化させる。 - [Theory] - [InlineData("--limit", "10001", 10_000)] - [InlineData("--snippet-lines", "21", 20)] - [InlineData("--snippet-lines", "999999", 20)] - [InlineData("--start", "10000001", 10_000_000)] - [InlineData("--end", "10000001", 10_000_000)] - [InlineData("--focus-line", "10000001", 10_000_000)] - [InlineData("--focus-column", "100001", 100_000)] - [InlineData("--focus-length", "100001", 100_000)] - public void ParseArgs_PositiveIntOverUpperBoundIsRejected_Issue1503(string flag, string value, int expectedMax) - { - var options = QueryCommandRunner.ParseArgs( - ["needle", flag, value], - jsonDefault: false, - allowNamedQuery: true); - - Assert.NotNull(options.ParseError); - Assert.Contains($"{flag} must be less than or equal to {expectedMax}", options.ParseError!); - Assert.Contains($"got '{value}'", options.ParseError!); - } + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("Error: could not parse --since value 'nope' as a date/time.", stderr); + Assert.Contains("Hint: fix the invalid or missing option value", stderr); + Assert.Contains($"Usage: {ConsoleUi.GetUsageLine(command)}", stderr); + Assert.DoesNotContain("No ", stderr); + } [Theory] - [InlineData("--depth", "65", 64)] - [InlineData("--before", "1001", 1_000)] - [InlineData("--after", "1001", 1_000)] - [InlineData("--max-line-width", "4097", 4096)] - public void ParseArgs_NonNegativeIntOverUpperBoundIsRejected_Issue1503(string flag, string value, int expectedMax) + [InlineData("references")] + [InlineData("callers")] + [InlineData("callees")] + [InlineData("excerpt")] + [InlineData("map")] + [InlineData("inspect")] + [InlineData("outline")] + [InlineData("status")] + [InlineData("impact")] + [InlineData("deps")] + [InlineData("hotspots")] + [InlineData("unused")] + [InlineData("validate")] + public void QueryEntrypoints_UnsupportedSinceReturnUsageError(string command) { - var options = QueryCommandRunner.ParseArgs( - ["needle", flag, value], - jsonDefault: false, - allowNamedQuery: true); + var (exitCode, _, stderr) = CaptureConsole(() => RunCommandWithUnsupportedSince(command, "nope")); - Assert.NotNull(options.ParseError); - Assert.Contains($"{flag} must be less than or equal to {expectedMax}", options.ParseError!); - Assert.Contains($"got '{value}'", options.ParseError!); + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains($"Error: --since is not supported for {command}.", stderr); + Assert.Contains($"Usage: {ConsoleUi.GetUsageLine(command)}", stderr); + Assert.DoesNotContain("could not parse --since value", stderr); + Assert.DoesNotContain("database not found", stderr); } [Theory] - [InlineData("--limit", "10000")] - [InlineData("--snippet-lines", "20")] - [InlineData("--depth", "64")] - [InlineData("--before", "1000")] - [InlineData("--after", "1000")] - public void ParseArgs_AtUpperBoundIsAccepted_Issue1503(string flag, string value) - { - var options = QueryCommandRunner.ParseArgs( - ["needle", flag, value], - jsonDefault: false, - allowNamedQuery: true); - - Assert.Null(options.ParseError); - } - - [Fact] - public void ParseArgs_ExactMaxSnippetLinesIsNotRejected_Issue1503() + [InlineData("search", "--no-json")] + [InlineData("map", "--count")] + [InlineData("inspect", "--count")] + [InlineData("status", "--count")] + [InlineData("validate", "--exact")] + [InlineData("validate", "--count")] + [InlineData("validate", "--lang", "javascript")] + [InlineData("validate", "--exclude-path", "src/")] + [InlineData("validate", "--exclude-tests")] + [InlineData("validate", "--limit", "nope")] + [InlineData("validate", "--top", "nope")] + public void QueryEntrypoints_UnsupportedOptionsReturnUsageError(string command, string flag, string? value = null) { - var options = QueryCommandRunner.ParseArgs( - ["needle", "--snippet-lines", $"{SearchSnippetFormatter.MaxSnippetLines}"], - jsonDefault: false, - allowNamedQuery: true); - - Assert.Null(options.ParseError); - Assert.Equal(SearchSnippetFormatter.MaxSnippetLines, options.SnippetLines); - } + var args = value == null + ? new[] { flag } + : new[] { flag, value }; - [Fact] - public void ParseArgs_ParsesExactAliases() - { - var search = QueryCommandRunner.ParseArgs(["needle", "--exact-substring"], jsonDefault: false); - Assert.True(search.ExactSubstring); - Assert.False(search.ExactName); + var (exitCode, _, stderr) = CaptureConsole(() => RunCommandWithUnsupportedOption(command, args)); - var symbols = QueryCommandRunner.ParseArgs(["Run", "--exact-name"], jsonDefault: false); - Assert.True(symbols.ExactName); - Assert.False(symbols.ExactSubstring); + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains($"Error: {flag} is not supported for {command}.", stderr); + Assert.Contains($"Usage: {ConsoleUi.GetUsageLine(command)}", stderr); + if (flag is "--limit" or "--top") + Assert.DoesNotContain("requires a positive integer", stderr); + Assert.DoesNotContain("database not found", stderr); } - [Fact] - public void ParseArgs_NameFlagCollectsValuesAndRejectsMissingValue() - { - var ok = QueryCommandRunner.ParseArgs( - ["first", "--name", "Alpha", "--name", "Beta", "extraPositional"], - jsonDefault: false); - Assert.Null(ok.ParseError); - Assert.Equal("first", ok.Query); - Assert.Equal(new[] { "Alpha", "Beta", "extraPositional" }, ok.ExtraNames); - - // --name swallowing a following flag as data is a silent trust failure; must be rejected. - // --name が直後のフラグを値として飲み込むのは暗黙の誤動作。拒否する。 - var bad = QueryCommandRunner.ParseArgs( - ["--name", "--lang", "csharp"], - jsonDefault: false); - Assert.NotNull(bad.ParseError); - Assert.Contains("--name requires a value", bad.ParseError!); - - var badTail = QueryCommandRunner.ParseArgs(["--name"], jsonDefault: false); - Assert.NotNull(badTail.ParseError); - } - - [Theory] - [InlineData("search-limit-tail", "search", "Error: --limit requires a value.")] - [InlineData("search-top-tail", "search", "Error: --limit requires a value.")] - [InlineData("search-db-tail", "search", "Error: --db requires a value.")] - [InlineData("search-db-swallow", "search", "Error: --db requires a value.")] - [InlineData("search-db-unknown-double-dash", "search", "Error: --db requires a value.")] - [InlineData("search-db-recognized-double-dash", "search", "Error: --db requires a value.")] - [InlineData("search-lang-swallow", "search", "Error: --lang requires a value.")] - [InlineData("search-lang-unknown-double-dash", "search", "Error: --lang requires a value.")] - [InlineData("search-path-swallow", "search", "Error: --path requires a value.")] - [InlineData("search-exclude-path-swallow", "search", "Error: --exclude-path requires a value.")] - [InlineData("definition-kind-swallow", "definition", "Error: --kind requires a value.")] - public void QueryEntrypoints_MissingOrSwallowedOptionValuesReturnUsageError(string scenario, string command, string expectedError) - { - var (exitCode, _, stderr) = CaptureConsole(() => RunCommandWithMissingOrSwallowedValue(scenario)); - - Assert.Equal(CommandExitCodes.UsageError, exitCode); - Assert.Contains(expectedError, stderr); - Assert.Contains("Hint: fix the invalid or missing option value", stderr); - Assert.Contains($"Usage: {ConsoleUi.GetUsageLine(command)}", stderr); - Assert.DoesNotContain("database not found", stderr); - Assert.DoesNotContain("Warning: unknown option", stderr); - } - - [Theory] - [InlineData("search-db-inline-empty", "search", "Error: --db requires a value.")] - [InlineData("search-lang-inline-empty", "search", "Error: --lang requires a value.")] - [InlineData("search-path-inline-empty", "search", "Error: --path requires a value.")] - [InlineData("search-exclude-path-inline-empty", "search", "Error: --exclude-path requires a value.")] - public void QueryEntrypoints_EmptyInlineStringOptionValuesReturnUsageError(string scenario, string command, string expectedError) - { - var (exitCode, _, stderr) = CaptureConsole(() => RunCommandWithEmptyInlineStringValue(scenario)); - - Assert.Equal(CommandExitCodes.UsageError, exitCode); - Assert.Contains(expectedError, stderr); - Assert.Contains("Hint: fix the invalid or missing option value", stderr); - Assert.Contains($"Usage: {ConsoleUi.GetUsageLine(command)}", stderr); - Assert.DoesNotContain("Unhandled exception", stderr); - } - - [Fact] - public void RunSearch_ExplicitMissingDbReturnsUsageErrorBeforeOpeningReader_Issue2073() - { - var projectRoot = TestProjectHelper.CreateTempProject("cdidx_issue2073_missing_db"); - try - { - var missingDb = Path.Combine(projectRoot, "missing-dir", "codeindex.db"); - - var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( - ["QueryCommandRunner", "--db", missingDb], - _jsonOptions)); - - Assert.Equal(CommandExitCodes.UsageError, exitCode); - Assert.Contains("Error [E001_DB_NOT_FOUND]: --db", stderr); - Assert.Contains("does not point to an existing database file", stderr); - Assert.Contains("Hint: create or refresh the index with `cdidx index `", stderr); - Assert.Contains($"Usage: {ConsoleUi.GetUsageLine("search")}", stderr); - } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); - } - } - - [Theory] - [InlineData("--path")] - [InlineData("--exclude-path")] - public void RunSearch_InvalidPathGlobReturnsUsageErrorBeforeQuery_Issue2073(string optionName) - { - var projectRoot = TestProjectHelper.CreateTempProject("cdidx_issue2073_invalid_glob"); - try - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); - - var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( - ["QueryCommandRunner", "--db", dbPath, optionName, "[*-z]"], - _jsonOptions)); - - Assert.Equal(CommandExitCodes.UsageError, exitCode); - Assert.Contains($"Error: {optionName} '[*-z]' is not a valid glob", stderr); - Assert.Contains("character classes are not supported", stderr); - Assert.Contains("Hint: fix the invalid or missing option value", stderr); - Assert.Contains($"Usage: {ConsoleUi.GetUsageLine("search")}", stderr); - } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); - } - } - - [Theory] - [InlineData("--path")] - [InlineData("--exclude-path")] - public void ParseArgs_PathFiltersRejectTooManyValues_Issue2911(string optionName) - { - var args = new List { "RunSearch" }; - for (var i = 0; i <= QueryCommandRunner.MaxQueryPathFilterCount; i++) - { - args.Add(optionName); - args.Add("src/**"); - } - - var options = QueryCommandRunner.ParseArgs( - args.ToArray(), - jsonDefault: false, - allowNamedQuery: true); - - Assert.Contains($"{optionName} accepts at most", options.ParseError); - } - - [Theory] - [InlineData("--path")] - [InlineData("--exclude-path")] - public void ParseArgs_PathFiltersRejectOverlongPattern_Issue2911(string optionName) - { - var tooLong = new string('a', QueryCommandRunner.MaxQueryPathFilterLength + 1); - - var options = QueryCommandRunner.ParseArgs( - ["RunSearch", optionName, tooLong], - jsonDefault: false, - allowNamedQuery: true); - - Assert.Contains($"{optionName} value is too long", options.ParseError); - } - - // Issue #1507: missing-value errors for CLI flags must append a per-flag `Hint:` line that - // shows the expected value type or range (e.g. positive integer, glob pattern, language id), - // so users do not need to consult `--help` for trivial mistakes. The hint is sourced from - // a single per-flag metadata table so every command surfaces consistent guidance. - // Issue #1507: 値欠如エラーには、フラグごとに期待する値の型/範囲を示す `Hint:` 行を - // 追記する(例: 正の整数、glob、言語識別子)。`--help` を見なくてもユーザーが復旧できる。 - // ヒントは単一のメタデータテーブルから供給され、コマンド間で一貫した案内を出す。 - [Theory] - [InlineData(new[] { "QueryCommandRunner", "--limit" }, "search", "--limit", "pass a positive integer", "--limit 20")] - [InlineData(new[] { "QueryCommandRunner", "--top" }, "search", "--limit", "pass a positive integer", "--limit 20")] - [InlineData(new[] { "QueryCommandRunner", "--db" }, "search", "--db", "pass a path to a CodeIndex SQLite database", ".cdidx/codeindex.db")] - [InlineData(new[] { "QueryCommandRunner", "--lang" }, "search", "--lang", "pass a language identifier", "--lang csharp")] - [InlineData(new[] { "QueryCommandRunner", "--path" }, "search", "--path", "pass a glob-style path pattern", "--path src/**")] - [InlineData(new[] { "QueryCommandRunner", "--exclude-path" }, "search", "--exclude-path", "pass a glob-style path pattern to exclude", "--exclude-path tests/**")] - [InlineData(new[] { "QueryCommandRunner", "--snippet-lines" }, "search", "--snippet-lines", "pass an integer between 1 and 20", "--snippet-lines 8")] - [InlineData(new[] { "QueryCommandRunner", "--snippet-focus" }, "search", "--snippet-focus", "pass one of `leftmost`, `quality`, or `proximity`", "--snippet-focus quality")] - [InlineData(new[] { "QueryCommandRunner", "--max-line-width" }, "search", "--max-line-width", "pass a non-negative integer", "--max-line-width 512")] - public void RunSearch_MissingOptionValueAppendsPerFlagHint_Issue1507( - string[] args, - string command, - string optionName, - string expectedHintFragment, - string expectedExampleFragment) - { - var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch(args, _jsonOptions)); - - Assert.Equal(CommandExitCodes.UsageError, exitCode); - Assert.Contains($"Error: {optionName} requires a value.", stderr); - Assert.Contains($"Hint: {expectedHintFragment}", stderr); - Assert.Contains(expectedExampleFragment, stderr); - Assert.Contains($"Usage: {ConsoleUi.GetUsageLine(command)}", stderr); - } - - // Issue #1507: command-specific value-taking flags should also surface the per-flag hint, so - // commands that wire through TryReadStringOptionValue / TryReadRawOptionValue stay consistent - // with the search-side coverage. - // Issue #1507: コマンド固有の値取りフラグも同じテーブル経由でヒントを表示する。 - [Fact] - public void RunSymbols_MissingNameValueShowsPerFlagHint_Issue1507() - { - var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunSymbols(["--name"], _jsonOptions)); - - Assert.Equal(CommandExitCodes.UsageError, exitCode); - Assert.Contains("Error: --name requires a value.", stderr); - Assert.Contains("Hint: pass a literal symbol name", stderr); - Assert.Contains("--name UserService", stderr); - } - - [Fact] - public void RunImpact_MissingDepthValueShowsPerFlagHint_Issue1507() - { - var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunImpact(["QueryCommandRunner", "--depth"], _jsonOptions)); - - Assert.Equal(CommandExitCodes.UsageError, exitCode); - Assert.Contains("Error: --depth requires a value.", stderr); - Assert.Contains("Hint: deprecated alias", stderr); - Assert.Contains("--max-hops 5", stderr); - } - - [Fact] - public void RunDefinition_MissingKindValueShowsPerFlagHint_Issue1507() - { - var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunDefinition(["QueryCommandRunner", "--kind"], _jsonOptions)); - - Assert.Equal(CommandExitCodes.UsageError, exitCode); - Assert.Contains("Error: --kind requires a value.", stderr); - Assert.Contains("Hint: pass a kind identifier", stderr); - Assert.Contains("--kind function", stderr); - } - - [Fact] - public void RunExcerpt_MissingStartValueShowsPerFlagHint_Issue1507() - { - var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunExcerpt(["src/CodeIndex/Program.cs", "--start"], _jsonOptions)); - - Assert.Equal(CommandExitCodes.UsageError, exitCode); - Assert.Contains("Error: --start requires a value.", stderr); - Assert.Contains("Hint: pass a 1-based line number", stderr); - Assert.Contains("--start 10", stderr); - } - - [Fact] - public void RunFiles_MissingSinceValueShowsPerFlagHint_Issue1507() - { - var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunFiles(["--since"], _jsonOptions)); - - Assert.Equal(CommandExitCodes.UsageError, exitCode); - Assert.Contains("Error: --since requires a value.", stderr); - Assert.Contains("Hint: pass an ISO 8601 datetime", stderr); - Assert.Contains("--since 2024-01-01", stderr); - } - - // Issue #1507: when a separated `--db --foo` shape rejects the next token as a recognized - // option, both the inline-form hint AND the per-flag hint must surface so users know why - // the parser stopped *and* what value to pass. - // Issue #1507: `--db --foo` のような separated dashed literal が拒否されたときは、 - // inline-form ヒント (`--db=` 形式) と、フラグ別の値ヒントを両方表示する。 - [Fact] - public void RunSearch_DbDoubleDashLiteralKeepsBothHints_Issue1507() - { - var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( - ["QueryCommandRunner", "--db", "--mystery"], _jsonOptions)); - - Assert.Equal(CommandExitCodes.UsageError, exitCode); - Assert.Contains("Error: --db requires a value.", stderr); - Assert.Contains("Hint: if the literal value starts with `--`, pass it as `--db=`.", stderr); - Assert.Contains("Hint: pass a path to a CodeIndex SQLite database", stderr); - } - - // Issue #1507: `find` validates its options through ValidateFindArgs (separate path from - // ParseArgs), so the per-flag hint table must apply there too. Otherwise users running - // `cdidx find foo --path` would still see the bare "requires a value" message. - // Issue #1507: `find` は ValidateFindArgs 経由で値検証する独自経路を持つので、 - // この経路でもフラグ別ヒントを表示する。 - [Fact] - public void RunFind_MissingPathValueShowsPerFlagHint_Issue1507() - { - var projectRoot = TestProjectHelper.CreateTempProject("cdidx_issue1507_find_missing_path"); - try - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); - var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( - ["foo", "--db", dbPath, "--path"], _jsonOptions)); - - Assert.Equal(CommandExitCodes.UsageError, exitCode); - Assert.Contains("Error: --path requires a value.", stderr); - Assert.Contains("Hint: pass a glob-style path pattern", stderr); - Assert.Contains("--path src/**", stderr); - } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); - } - } - - // Issue #1507: `WithDb` surfaces the missing --db error when ParseArgs accepted an empty - // DbPath (e.g. via the CLI default fallback being cleared). Even at this late site the - // per-flag hint must come from the same metadata table, not a hard-coded one-off string. - // Issue #1507: WithDb 経路の missing --db エラーも、同じメタデータテーブル由来のヒントを使う。 - [Fact] - public void WithDb_BlankDbPathSurfacesPerFlagHint_Issue1507() - { - var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( - ["QueryCommandRunner", "--db="], _jsonOptions)); - - Assert.Equal(CommandExitCodes.UsageError, exitCode); - Assert.Contains("Error: --db requires a value.", stderr); - Assert.Contains("Hint: pass a path to a CodeIndex SQLite database", stderr); - } - - [Fact] - public void WithDb_InvalidSqliteFileSurfacesSqliteCategory_Issue2072() - { - var projectRoot = TestProjectHelper.CreateTempProject("cdidx_issue2072_invalid_sqlite"); - try - { - var dbPath = Path.Combine(projectRoot, "not-a-codeindex.db"); - File.WriteAllText(dbPath, "this is not a sqlite database"); - var dbUri = new Uri(dbPath).AbsoluteUri + "?mode=ro&immutable=1;Pooling=False"; - - var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( - ["--db", dbUri], - _jsonOptions)); - - Assert.Equal(CommandExitCodes.DatabaseError, exitCode); - Assert.Contains($"Error [{CommandErrorCodes.DbError}]: SQLite database error", stderr); - Assert.Contains("Hint: check `--db`, verify the index was written by a compatible cdidx version", stderr); - Assert.DoesNotContain("database error:", stderr); - } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); - } - } - - [Fact] - public void WithDb_EmptySqliteFileRejectedBeforeQuery_Issue2037() - { - var projectRoot = TestProjectHelper.CreateTempProject("cdidx_issue2037_empty_sqlite"); - try - { - var dbPath = Path.Combine(projectRoot, "empty.db"); - using (var connection = new SqliteConnection(new SqliteConnectionStringBuilder { DataSource = dbPath }.ConnectionString)) - { - connection.Open(); - } - - var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( - ["--db", dbPath], - _jsonOptions)); - - Assert.Equal(CommandExitCodes.DatabaseError, exitCode); - Assert.Contains("does not appear to be a valid CodeIndex database", stderr); - Assert.Contains("missing required table `files`", stderr); - Assert.Contains("Hint: rebuild with `cdidx index --db `", stderr); - } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); - } - } - - [Fact] - public void WithDb_MalformedFileUriSurfacesDbPathParseError_Issue1990() - { - const string malformedUri = "file:///tmp/codeindex%ZZ.db?immutable=1"; - - var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( - ["--db", malformedUri], - _jsonOptions)); - - Assert.Equal(CommandExitCodes.DatabaseError, exitCode); - Assert.Contains($"Error [{CommandErrorCodes.DbError}]: invalid --db file URI:", stderr); - Assert.Contains("file:///absolute/path/to/codeindex.db?immutable=1", stderr); - Assert.Contains($"the --db value resolved to: {malformedUri}", stderr); - } - - [Fact] - public void WithDb_SqliteCantOpenSurfacesAccessOpenCategory_Issue2072() - { - var projectRoot = TestProjectHelper.CreateTempProject("cdidx_issue2072_cantopen"); - try - { - var missingParent = Path.Combine(projectRoot, "missing-parent"); - var dbUri = new Uri(Path.Combine(missingParent, "codeindex.db")).AbsoluteUri + "?mode=ro"; - - var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( - ["--db", dbUri], - _jsonOptions)); - - Assert.Equal(CommandExitCodes.DatabaseError, exitCode); - Assert.Contains($"Error [{CommandErrorCodes.DbNotFound}]: database not found at {Path.GetFullPath(Path.Combine(missingParent, "codeindex.db"))}", stderr); - Assert.Contains("Hint: the --db path resolved to:", stderr); - Assert.DoesNotContain("database access/open denied", stderr); - } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); - } - } - - [Theory] - [InlineData("search-extra", "unexpected extra positional 1 argument for search")] - [InlineData("excerpt-extra", "unexpected extra positional 1 argument for excerpt")] - [InlineData("map-extra", "map does not accept positional arguments")] - [InlineData("outline-extra", "outline does not accept positional arguments")] - [InlineData("status-extra", "status does not accept positional arguments")] - [InlineData("validate-extra", "validate does not accept positional arguments")] - [InlineData("languages-extra", "languages does not accept positional arguments")] - public void QueryEntrypoints_UnexpectedPositionalsReturnUsageError(string scenario, string expectedError) - { - var (exitCode, _, stderr) = CaptureConsole(() => RunCommandWithUnexpectedPositionals(scenario)); - - Assert.Equal(CommandExitCodes.UsageError, exitCode); - Assert.Contains(expectedError, stderr); - Assert.DoesNotContain("database not found", stderr); - } - - [Fact] - public void RunFiles_PathFilterAcceptsLeadingDashValue() - { - var projectRoot = TestProjectHelper.CreateTempProject("cdidx_files_path_leading_dash"); - try - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); - var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFiles( - ["--db", dbPath, "--path", "-foo", "--count"], - _jsonOptions)); - - Assert.Equal(CommandExitCodes.Success, exitCode); - Assert.Equal("0", stdout.Trim()); - Assert.Equal(string.Empty, stderr); - } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); - } - } - - [Fact] - public void RunFiles_ExcludePathFilterAcceptsLeadingDashValue() - { - var projectRoot = TestProjectHelper.CreateTempProject("cdidx_files_exclude_path_leading_dash"); - try - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); - var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFiles( - ["--db", dbPath, "--exclude-path", "-foo", "--count"], - _jsonOptions)); - - Assert.Equal(CommandExitCodes.Success, exitCode); - Assert.Equal("0", stdout.Trim()); - Assert.Equal(string.Empty, stderr); - } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); - } - } - - [Fact] - public void RunFiles_PathFilterAcceptsRecognizedOptionTokenViaInlineValue() - { - var projectRoot = TestProjectHelper.CreateTempProject("cdidx_files_path_inline_recognized_option"); - try - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); - TestProjectHelper.InsertIndexedFile( - dbPath, - "src/--json-dir/Demo.cs", - "csharp", - "class Demo {}\n"); - - var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFiles( - [$"--db={dbPath}", "--path=--json-dir", "--count", "--json"], - _jsonOptions)); - - Assert.Equal(CommandExitCodes.Success, exitCode); - Assert.Equal(string.Empty, stderr); - using var document = ParseJsonOutput(stdout); - Assert.Equal(1, document.RootElement.GetProperty("count").GetInt32()); - } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); - } - } - - [Fact] - public void RunFiles_ExcludePathFilterAcceptsRecognizedOptionTokenViaInlineValue() - { - var projectRoot = TestProjectHelper.CreateTempProject("cdidx_files_exclude_path_inline_recognized_option"); - try - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); - TestProjectHelper.InsertIndexedFile( - dbPath, - "src/--count-dir/Demo.cs", - "csharp", - "class Demo {}\n"); - - var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFiles( - [$"--db={dbPath}", "--exclude-path=--count-dir", "--count", "--json"], - _jsonOptions)); - - Assert.Equal(CommandExitCodes.Success, exitCode); - Assert.Equal(string.Empty, stderr); - using var document = ParseJsonOutput(stdout); - Assert.Equal(0, document.RootElement.GetProperty("count").GetInt32()); - } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); - } - } - - // Pins `find --path=` with a value that starts with `--`. ParseArgs supports - // this shape via inline `=`, but ValidateFindArgs previously saw only the bare token - // and `PrepareFindArgs` briefly tried to normalize the inline form by splitting it into - // two tokens — that split destroyed inline `--`-prefixed values. Locks the contract - // that `find` honors the CLI hint (`pass it as --path=`) just like the other - // query commands. - // `find --path=` で value が `--` で始まる合法な inline 値を壊さないよう固定する - // 回帰テスト。`PrepareFindArgs` 側で inline を分解すると `--path=--literal.txt` が - // `--path`/`--literal.txt` に割れ、`ParseArgs` が値を option と誤認して失敗していた。 - [Fact] - public void RunFind_PathFilterAcceptsRecognizedOptionTokenViaInlineValue() - { - var projectRoot = TestProjectHelper.CreateTempProject("cdidx_find_path_inline_recognized_option"); - try - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); - TestProjectHelper.InsertIndexedFile( - dbPath, - "src/--json-dir/Demo.cs", - "csharp", - "class Demo { void Alpha() {} }\n"); - - var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( - ["Alpha", $"--db={dbPath}", "--path=--json-dir", "--count", "--json"], - _jsonOptions)); - - Assert.Equal(CommandExitCodes.Success, exitCode); - Assert.Equal(string.Empty, stderr); - using var document = ParseJsonOutput(stdout); - Assert.Equal(1, document.RootElement.GetProperty("count").GetInt32()); - } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); - } - } - - [Theory] - [InlineData("--db", "/tmp/does-not-matter.db")] - [InlineData("--db=")] - [InlineData("--mystery")] - public void RunLanguages_UnsupportedOptionsReturnUsageError(string flag, string? value = null) - { - var args = value == null - ? new[] { flag } - : new[] { flag, value }; - - var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunLanguages(args, _jsonOptions)); - - Assert.Equal(CommandExitCodes.UsageError, exitCode); - Assert.Contains($"Error: {flag} is not supported for languages.", stderr); - Assert.Contains($"Usage: {ConsoleUi.GetUsageLine("languages")}", stderr); - Assert.DoesNotContain("requires a value", stderr); - Assert.DoesNotContain("Warning: unknown option", stderr); - } - - [Fact] - public void RunLanguages_JsonListsModernNodeModuleExtensions() - { - var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunLanguages(["--json"], _jsonOptions)); - - Assert.Equal(CommandExitCodes.Success, exitCode); - Assert.Equal(string.Empty, stderr); - - using var document = ParseJsonOutput(stdout); - var languages = document.RootElement.GetProperty("languages"); - var javascript = languages.EnumerateArray().First(lang => lang.GetProperty("lang").GetString() == "javascript"); - var typescript = languages.EnumerateArray().First(lang => lang.GetProperty("lang").GetString() == "typescript"); - var objc = languages.EnumerateArray().First(lang => lang.GetProperty("lang").GetString() == "objc"); - - Assert.Contains(".cjs", javascript.GetProperty("extensions").EnumerateArray().Select(ext => ext.GetString())); - Assert.Contains(".mjs", javascript.GetProperty("extensions").EnumerateArray().Select(ext => ext.GetString())); - Assert.Contains("js", javascript.GetProperty("aliases").EnumerateArray().Select(alias => alias.GetString())); - Assert.Contains("jsx", javascript.GetProperty("aliases").EnumerateArray().Select(alias => alias.GetString())); - Assert.Contains(".cts", typescript.GetProperty("extensions").EnumerateArray().Select(ext => ext.GetString())); - Assert.Contains(".mts", typescript.GetProperty("extensions").EnumerateArray().Select(ext => ext.GetString())); - Assert.Contains(".m", objc.GetProperty("extensions").EnumerateArray().Select(ext => ext.GetString())); - Assert.Contains(".mm", objc.GetProperty("extensions").EnumerateArray().Select(ext => ext.GetString())); - } - - [Fact] - public void RunLanguages_JsonListsHtmlWithSymbolExtractionAndAllExtensions() - { - // Pin the #215 surface: `cdidx languages --json` must report html with - // symbol_extraction=true and list all four extensions (.html, .htm, .xhtml, .shtml) - // so AI tools can discover HTML symbol support without indexing first. - // #215 の表面契約を pin: `cdidx languages --json` は html を symbol_extraction=true で - // 返し、`.html` / `.htm` / `.xhtml` / `.shtml` の 4 拡張子を列挙する必要がある。 - // AI ツールがインデックス前でも HTML のシンボル対応を検出できるようにするため。 - var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunLanguages(["--json"], _jsonOptions)); - - Assert.Equal(CommandExitCodes.Success, exitCode); - Assert.Equal(string.Empty, stderr); - - using var document = ParseJsonOutput(stdout); - var languages = document.RootElement.GetProperty("languages"); - var html = languages.EnumerateArray().First(lang => lang.GetProperty("lang").GetString() == "html"); - - Assert.True(html.GetProperty("symbol_extraction").GetBoolean()); - var extensions = html.GetProperty("extensions").EnumerateArray().Select(ext => ext.GetString()).ToList(); - Assert.Contains(".html", extensions); - Assert.Contains(".htm", extensions); - Assert.Contains(".xhtml", extensions); - Assert.Contains(".shtml", extensions); - } - - [Fact] - public void RunLanguages_JsonListsAssemblyWithSymbolExtractionGraphAndAliases() - { - var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunLanguages(["--json"], _jsonOptions)); - - Assert.Equal(CommandExitCodes.Success, exitCode); - Assert.Equal(string.Empty, stderr); - - using var document = ParseJsonOutput(stdout); - var languages = document.RootElement.GetProperty("languages"); - var assembly = languages.EnumerateArray().First(lang => lang.GetProperty("lang").GetString() == "assembly"); - - Assert.True(assembly.GetProperty("symbol_extraction").GetBoolean()); - Assert.True(assembly.GetProperty("graph_queries").GetBoolean()); - - var extensions = assembly.GetProperty("extensions").EnumerateArray().Select(ext => ext.GetString()).ToList(); - Assert.Contains(".s", extensions); - Assert.Contains(".S", extensions); - Assert.Contains(".asm", extensions); - Assert.Contains(".nasm", extensions); - - var aliases = assembly.GetProperty("aliases").EnumerateArray().Select(alias => alias.GetString()).ToList(); - Assert.Contains("asm", aliases); - Assert.Contains("assembler", aliases); - Assert.Contains("gas", aliases); - Assert.Contains("gnuasm", aliases); - Assert.Contains("gnu assembler", aliases); - } - - [Fact] - public void RunLanguages_JsonListsCSharpRazorAliases() - { - var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunLanguages(["--json"], _jsonOptions)); - - Assert.Equal(CommandExitCodes.Success, exitCode); - Assert.Equal(string.Empty, stderr); - - using var document = ParseJsonOutput(stdout); - var languages = document.RootElement.GetProperty("languages"); - var csharp = languages.EnumerateArray().First(lang => lang.GetProperty("lang").GetString() == "csharp"); - var aliases = csharp.GetProperty("aliases").EnumerateArray().Select(alias => alias.GetString()).ToList(); - - Assert.Contains("cshtml", aliases); - Assert.Contains("razor", aliases); - } - - [Fact] - public void RunLanguages_Json_SearchOnlyBucketsAdvertiseZeroSymbolAndGraphSupport() - { - // Search-only languages that intentionally live outside the Python / CSS extractors - // (Cython .pyx/.pxd, Sass .sass, Stylus .styl, and the newly added extension-only - // languages) must advertise - // symbol_extraction=false / graph_queries=false so AI clients can tell the difference - // between "indexed with symbols" and "indexed for search only". - // 意図的に Python / CSS 抽出器の対象外になっている search-only 言語 - // (Cython の .pyx/.pxd、Sass の .sass、Stylus の .styl、そして新規追加の - // 拡張子ベース言語)は、 - // symbol_extraction=false / graph_queries=false で広告しなければならない。 - // こうしないと、AI クライアントが「シンボル付きインデックス」と「検索のみインデックス」を区別できない。 - var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunLanguages(["--json"], _jsonOptions)); - Assert.Equal(CommandExitCodes.Success, exitCode); - Assert.Equal(string.Empty, stderr); - - using var document = ParseJsonOutput(stdout); - var languages = document.RootElement.GetProperty("languages").EnumerateArray() - .ToDictionary(entry => entry.GetProperty("lang").GetString()!, entry => entry); - - foreach (var searchOnly in new[] { "cython", "sass", "stylus", "msbuild" }) - { - Assert.True(languages.ContainsKey(searchOnly), $"expected '{searchOnly}' to be listed"); - var entry = languages[searchOnly]; - Assert.False(entry.GetProperty("symbol_extraction").GetBoolean(), - $"{searchOnly} must advertise symbol_extraction=false"); - Assert.False(entry.GetProperty("graph_queries").GetBoolean(), - $"{searchOnly} must advertise graph_queries=false"); - } - - foreach (var searchOnly in new[] { "crystal", "clojure", "d", "erlang", "julia", "nim", "ocaml", "solidity", "tcl" }) - { - Assert.True(languages.ContainsKey(searchOnly), $"expected '{searchOnly}' to be listed"); - var entry = languages[searchOnly]; - Assert.False(entry.GetProperty("symbol_extraction").GetBoolean(), - $"{searchOnly} must advertise symbol_extraction=false"); - Assert.False(entry.GetProperty("graph_queries").GetBoolean(), - $"{searchOnly} must advertise graph_queries=false"); - } - - var yamlAliases = languages["yaml"].GetProperty("aliases").EnumerateArray() - .Select(alias => alias.GetString()).ToList(); - Assert.Contains("yml", yamlAliases); - - Assert.True(languages.ContainsKey("perl"), "expected 'perl' to be listed"); - Assert.True(languages["perl"].GetProperty("symbol_extraction").GetBoolean(), - "perl must advertise symbol_extraction=true"); - Assert.True(languages["perl"].GetProperty("graph_queries").GetBoolean(), - "perl must advertise graph_queries=true"); - - // Cython owns .pyx / .pxd exclusively; python keeps .py / .pyi / .pyw and Bazel filenames. - // Cython は .pyx / .pxd を専有し、python は .py / .pyi / .pyw と Bazel ファイル名を維持。 - var cythonExts = languages["cython"].GetProperty("extensions").EnumerateArray() - .Select(ext => ext.GetString()).ToList(); - Assert.Contains(".pyx", cythonExts); - Assert.Contains(".pxd", cythonExts); - - var pythonExts = languages["python"].GetProperty("extensions").EnumerateArray() - .Select(ext => ext.GetString()).ToList(); - Assert.DoesNotContain(".pyx", pythonExts); - Assert.DoesNotContain(".pxd", pythonExts); - Assert.Contains(".py", pythonExts); - Assert.Contains(".pyi", pythonExts); - - var ocamlExts = languages["ocaml"].GetProperty("extensions").EnumerateArray() - .Select(ext => ext.GetString()).ToList(); - Assert.Contains(".ml", ocamlExts); - Assert.Contains(".mli", ocamlExts); - - var clojureExts = languages["clojure"].GetProperty("extensions").EnumerateArray() - .Select(ext => ext.GetString()).ToList(); - Assert.Contains(".clj", clojureExts); - Assert.Contains(".cljs", clojureExts); - Assert.Contains(".cljc", clojureExts); - Assert.Contains(".edn", clojureExts); - - var perlExts = languages["perl"].GetProperty("extensions").EnumerateArray() - .Select(ext => ext.GetString()).ToList(); - Assert.Contains(".pl", perlExts); - Assert.Contains(".pm", perlExts); - Assert.Contains(".pod", perlExts); - Assert.Contains(".t", perlExts); - - var msbuildExts = languages["msbuild"].GetProperty("extensions").EnumerateArray() - .Select(ext => ext.GetString()).ToList(); - Assert.Contains(".csproj", msbuildExts); - Assert.Contains(".fsproj", msbuildExts); - Assert.Contains(".vbproj", msbuildExts); - Assert.Contains(".props", msbuildExts); - Assert.Contains(".targets", msbuildExts); - Assert.DoesNotContain(".csproj", languages["xml"].GetProperty("extensions").EnumerateArray().Select(ext => ext.GetString())); - } - - [Fact] - public void RunLanguages_HumanOutput_WideExtensionListSpillsOntoContinuationLine() - { - // The human-readable table must not let long extension lists (dockerfile / makefile / - // python / ruby / xml / msbuild) swallow the Symbols / Graph columns. Instead, spill onto a - // continuation line so the row is still readable. - // 人間向けテーブルは、長い拡張子リスト(dockerfile / makefile / python / ruby / xml / msbuild)が - // Symbols / Graph 列を食い潰さないようにし、継続行へ退避させて可読性を保つこと。 - var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunLanguages([], _jsonOptions)); - - Assert.Equal(CommandExitCodes.Success, exitCode); - Assert.Contains("languages)", stderr); - - var lines = stdout.Split('\n').Select(line => line.TrimEnd('\r')).ToArray(); - - // Rows with long extension / alias lists must spill onto a continuation line so the - // Symbols / Graph columns stay readable. - // 拡張子や alias が長い行は継続行に退避し、Symbols / Graph 列の可読性を保つ。 - var wideLangs = new[] { "csharp", "dockerfile", "makefile", "python", "ruby", "msbuild" }; - foreach (var wide in wideLangs) - { - var headerIndex = Array.FindIndex(lines, line => line.StartsWith($"{wide} ", StringComparison.Ordinal)); - Assert.True(headerIndex >= 0, $"expected row for {wide}"); - var header = lines[headerIndex]; - // Header row carries only lang + sym + graph, never the extension list itself. - // ヘッダ行には言語名・シンボル・グラフのみが含まれ、拡張子文字列は含まれない。 - Assert.DoesNotContain("Dockerfile", header); - Assert.DoesNotContain("Makefile", header); - Assert.DoesNotContain("WORKSPACE", header); - Assert.DoesNotContain("Gemfile", header); - Assert.DoesNotContain(".csproj", header); - var continuation = lines[headerIndex + 1]; - Assert.StartsWith(" Extensions: ", continuation); - } - - var yamlIndex = Array.FindIndex(lines, line => line.StartsWith("yaml ", StringComparison.Ordinal)); - Assert.True(yamlIndex >= 0, "expected row for yaml"); - Assert.Contains("yml", lines[yamlIndex]); - - // Spot-check: the continuation line for dockerfile contains both the bare filename and the - // `` pseudo-entry added for Issue #189 follow-up. - // dockerfile 継続行に完全一致ファイル名と `` 擬似エントリが両方入る。 - var dockerIndex = Array.FindIndex(lines, line => line.StartsWith("dockerfile ", StringComparison.Ordinal)); - var dockerContinuation = lines[dockerIndex + 1]; - Assert.Contains("Dockerfile ", dockerContinuation); - Assert.Contains("Dockerfile.", dockerContinuation); - Assert.Contains("Containerfile", dockerContinuation); - } - - [Theory] - [InlineData("search")] - [InlineData("definition")] - [InlineData("symbols")] - [InlineData("files")] - public void QueryEntrypoints_InvalidSinceReturnUsageError(string command) - { - var (exitCode, _, stderr) = CaptureConsole(() => RunCommandWithInvalidSince(command)); - - Assert.Equal(CommandExitCodes.UsageError, exitCode); - Assert.Contains("Error: could not parse --since value 'nope' as a date/time.", stderr); - Assert.Contains("Hint: fix the invalid or missing option value", stderr); - Assert.Contains($"Usage: {ConsoleUi.GetUsageLine(command)}", stderr); - Assert.DoesNotContain("No ", stderr); - } - - [Theory] - [InlineData("references")] - [InlineData("callers")] - [InlineData("callees")] - [InlineData("excerpt")] - [InlineData("map")] - [InlineData("inspect")] - [InlineData("outline")] - [InlineData("status")] - [InlineData("impact")] - [InlineData("deps")] - [InlineData("hotspots")] - [InlineData("unused")] - [InlineData("validate")] - public void QueryEntrypoints_UnsupportedSinceReturnUsageError(string command) - { - var (exitCode, _, stderr) = CaptureConsole(() => RunCommandWithUnsupportedSince(command, "nope")); - - Assert.Equal(CommandExitCodes.UsageError, exitCode); - Assert.Contains($"Error: --since is not supported for {command}.", stderr); - Assert.Contains($"Usage: {ConsoleUi.GetUsageLine(command)}", stderr); - Assert.DoesNotContain("could not parse --since value", stderr); - Assert.DoesNotContain("database not found", stderr); - } - - [Theory] - [InlineData("search", "--no-json")] - [InlineData("map", "--count")] - [InlineData("inspect", "--count")] - [InlineData("status", "--count")] - [InlineData("validate", "--exact")] - [InlineData("validate", "--count")] - [InlineData("validate", "--lang", "javascript")] - [InlineData("validate", "--exclude-path", "src/")] - [InlineData("validate", "--exclude-tests")] - [InlineData("validate", "--limit", "nope")] - [InlineData("validate", "--top", "nope")] - public void QueryEntrypoints_UnsupportedOptionsReturnUsageError(string command, string flag, string? value = null) - { - var args = value == null - ? new[] { flag } - : new[] { flag, value }; - - var (exitCode, _, stderr) = CaptureConsole(() => RunCommandWithUnsupportedOption(command, args)); - - Assert.Equal(CommandExitCodes.UsageError, exitCode); - Assert.Contains($"Error: {flag} is not supported for {command}.", stderr); - Assert.Contains($"Usage: {ConsoleUi.GetUsageLine(command)}", stderr); - if (flag is "--limit" or "--top") - Assert.DoesNotContain("requires a positive integer", stderr); - Assert.DoesNotContain("database not found", stderr); - } - - // `--paht` should surface as `--path` so MCP/CLI users do not have to read full help - // text to recover from a single-letter swap (#1582). - // `--paht` のような 1 文字入れ替えミスから `--path` を提案できることを確認する (#1582)。 - [Fact] - public void RunSearch_UnsupportedFlagTypo_SuggestsClosestFlag() - { - var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( - ["foo", "--paht", "src/**"], - _jsonOptions)); - - Assert.Equal(CommandExitCodes.UsageError, exitCode); - Assert.Contains("Error: --paht is not supported for search.", stderr); - Assert.Contains("Did you mean: --path?", stderr); - } - - // Inline `--foo=bar` form must surface the same suggestion as the separated form. - // ParseArgs only splits `=value` for known value-taking options, so for `--paht=...` - // the suggester previously saw the full `--paht=src/**` token and produced no match; - // the round-2 fix strips the `=value` portion before searching for a similar flag. - // インライン `--foo=bar` 形式も separated 形式と同じ提案を出すこと。ParseArgs は - // 既知の value-taking option でしか `=value` を分解しないため、`--paht=...` は - // まるごと matcher に渡され従来は提案が出なかった。round-2 修正で `=value` を - // 除去してから候補を探すようにした。 - [Fact] - public void RunSearch_UnsupportedFlagTypoInInlineValueForm_SuggestsClosestFlag_Issue1582() - { - var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( - ["foo", "--paht=src/**"], - _jsonOptions)); - - Assert.Equal(CommandExitCodes.UsageError, exitCode); - Assert.Contains("is not supported for search", stderr); - Assert.Contains("Did you mean: --path?", stderr); - } - - [Fact] - public void RunSearch_UnknownFlagAfterQuery_ReturnsUsageError() - { - var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( - ["foo", "--dapth", "3"], - _jsonOptions)); - - Assert.Equal(CommandExitCodes.UsageError, exitCode); - Assert.Equal(string.Empty, stdout); - Assert.Contains("--dapth is not supported for search", stderr); - Assert.Contains("Did you mean: --path?", stderr); - } - - [Fact] - public void ParseArgs_EndOfOptionsAllowsDashPrefixedQueryLiteral() - { - var options = QueryCommandRunner.ParseArgs(["--", "--baar"], jsonDefault: false, allowNamedQuery: true); - - Assert.Null(options.ParseError); - Assert.Equal("--baar", options.Query); - } - - // `find` previously emitted only the raw `Error: unsupported option for find: --paht` - // line — round-2 fix routes the unknown token through the same suggester so users see - // `Did you mean: --path?`. Covers both the separated and inline `=value` forms. - // 従来 find は `Error: unsupported option for find: --paht` だけを出していたが、 - // round-2 修正で同じ suggester を経由するようにし `Did you mean: --path?` を出す。 - // separated 形式と inline `=value` 形式の両方を確認する。 - [Fact] - public void RunFind_UnsupportedFlagTypo_SuggestsClosestFlag_Issue1582() - { - var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( - ["guard", "--paht", "src/Auth.cs"], - _jsonOptions)); - - Assert.Equal(CommandExitCodes.UsageError, exitCode); - Assert.Contains("unsupported option for find: --paht", stderr); - Assert.Contains("Did you mean: --path?", stderr); - } - - [Fact] - public void RunFind_UnsupportedFlagTypoInInlineValueForm_SuggestsClosestFlag_Issue1582() - { - var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( - ["guard", "--paht=src/Auth.cs"], - _jsonOptions)); - - Assert.Equal(CommandExitCodes.UsageError, exitCode); - Assert.Contains("unsupported option for find: --paht=src/Auth.cs", stderr); - Assert.Contains("Did you mean: --path?", stderr); - } - - [Theory] - [InlineData("search-limit", "search", "--limit requires an integer between 1 and 10000")] - [InlineData("search-top", "search", "--limit requires an integer between 1 and 10000")] - [InlineData("search-snippet-lines", "search", "--snippet-lines requires an integer between 1 and 20")] - [InlineData("impact-depth", "impact", "--depth requires an integer between 0 and 64")] - [InlineData("excerpt-start", "excerpt", "--start requires an integer between 1 and 10000000")] - [InlineData("excerpt-end", "excerpt", "--end requires an integer between 1 and 10000000")] - [InlineData("excerpt-before", "excerpt", "--before requires an integer between 0 and 1000")] - [InlineData("excerpt-after", "excerpt", "--after requires an integer between 0 and 1000")] - public void QueryEntrypoints_InvalidNumericOptionsReturnUsageError(string scenario, string command, string expectedErrorFragment) - { - var (exitCode, _, stderr) = CaptureConsole(() => RunCommandWithInvalidNumeric(scenario)); - - Assert.Equal(CommandExitCodes.UsageError, exitCode); - Assert.Contains(expectedErrorFragment, stderr); - Assert.Contains("Hint: fix the invalid or missing option value", stderr); - Assert.Contains($"Usage: {ConsoleUi.GetUsageLine(command)}", stderr); - Assert.DoesNotContain("database not found", stderr); - } - - [Fact] - public void RunImpact_OutOfRangeDepthUpperBound_ReturnsUsageError_Issue1700() - { - var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( - ["Target", "--depth", "999999999"], - _jsonOptions)); - - Assert.Equal(CommandExitCodes.UsageError, exitCode); - Assert.Equal(string.Empty, stdout); - Assert.Contains("--depth must be less than or equal to 64", stderr); - Assert.Contains($"Usage: {ConsoleUi.GetUsageLine("impact")}", stderr); - Assert.DoesNotContain("database not found", stderr); - } - - // Regression lock for #161: valid integers that fail the positive / non-negative - // range check used to be swallowed silently, leaving the command to run with the - // default value and write real results to stdout with exit 0. Every case below - // MUST abort with UsageError and leak NO stdout output. - // Uses a real indexed DB + real file on disk so that if ParseArgs ever regresses to - // "swallow invalid value and continue with default", the stdout-empty invariant has - // real teeth — the command could actually produce results, and the test would catch - // them. Without a real fixture the invariant could be preserved by accident (missing - // DB / missing file making the command fail at a later stage). - // #161 の回帰ロック: 数値として parse できるが positive / non-negative 制約を満たさない値は、 - // かつて parseError を立てずにデフォルトへ差し替えられ、本物の結果を stdout に出して exit 0 にしていた。 - // 各ケースは UsageError で停止し、stdout に 1 バイトも漏らしてはならない。 - // 実在の index 済み DB と実在ファイルを用意することで、ParseArgs が「不正値を既定値に差し替えて続行」へ - // 退行した場合に本当に stdout へ結果が漏れる経路を作り、stdout 空のアサーションが偶然通ってしまう逃げ道を塞ぐ。 - [Theory] - [InlineData("symbols", "0", "--limit", "--limit requires an integer between 1 and 10000")] - [InlineData("symbols", "-5", "--limit", "--limit requires an integer between 1 and 10000")] - [InlineData("symbols", "0", "--top", "--limit requires an integer between 1 and 10000")] - [InlineData("symbols", "-5", "--top", "--limit requires an integer between 1 and 10000")] - [InlineData("search", "0", "--limit", "--limit requires an integer between 1 and 10000")] - [InlineData("search", "-5", "--limit", "--limit requires an integer between 1 and 10000")] - [InlineData("search", "0", "--top", "--limit requires an integer between 1 and 10000")] - [InlineData("search", "-5", "--top", "--limit requires an integer between 1 and 10000")] - [InlineData("search", "0", "--snippet-lines", "--snippet-lines requires an integer between 1 and 20")] - [InlineData("impact", "-1", "--max-hops", "--max-hops requires an integer between 0 and 64")] - [InlineData("impact", "-1", "--depth", "--depth requires an integer between 0 and 64")] - [InlineData("excerpt", "0", "--start", "--start requires an integer between 1 and 10000000")] - [InlineData("excerpt", "-5", "--start", "--start requires an integer between 1 and 10000000")] - [InlineData("excerpt", "0", "--end", "--end requires an integer between 1 and 10000000")] - [InlineData("excerpt", "-1", "--before", "--before requires an integer between 0 and 1000")] - [InlineData("excerpt", "-1", "--after", "--after requires an integer between 0 and 1000")] - public void QueryEntrypoints_OutOfRangeNumericOptionsFailClosed_Issue161(string command, string value, string option, string expectedErrorFragment) - { - var projectRoot = TestProjectHelper.CreateTempProject( - $"cdidx_issue161_{command}_{option.Trim('-')}_{value.Replace('-', 'n')}"); - try - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); - // Seed indexed content so `search "Issue161"`, `symbols "Issue161"`, and - // `impact "Issue161Callee"` would all return real matches if ParseArgs regressed. - // Issue161Caller.Run calls Issue161Target.Issue161Callee, giving `impact` a live edge. - // `search "Issue161"` / `symbols "Issue161"` が本物の結果を返せるよう、index 済み内容をシードする。 - // Issue161Caller.Run が Issue161Target.Issue161Callee を呼ぶことで `impact` のエッジも張る。 - TestProjectHelper.InsertIndexedFile( - dbPath, - "src/Issue161Target.cs", - "csharp", - """" - namespace Issue161; - public class Issue161Target - { - public void Issue161Callee() { } - } - public class Issue161Caller - { - public void Run() - { - var target = new Issue161Target(); - target.Issue161Callee(); - } - } - """"); - MarkGraphAndFoldReady(dbPath); - - // Real file on disk so `excerpt` would actually read and print content - // if any of its numeric options regressed to their defaults. - // `excerpt` が既定値退行時に本当にファイル内容を読んで出力できるよう、実在ファイルも用意する。 - var excerptDir = Path.Combine(projectRoot, "src"); - Directory.CreateDirectory(excerptDir); - var excerptFilePath = Path.Combine(excerptDir, "Issue161Excerpt.cs"); - File.WriteAllText( - excerptFilePath, - "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\n"); - - string[] args = command switch - { - "symbols" => ["Issue161", "--db", dbPath, "--json", option, value], - "search" => ["Issue161", "--db", dbPath, "--json", option, value], - "impact" => ["Issue161Callee", "--db", dbPath, "--json", option, value], - "excerpt" when option == "--start" => [excerptFilePath, "--json", option, value], - "excerpt" => [excerptFilePath, "--json", "--start", "1", option, value], - _ => throw new ArgumentOutOfRangeException(nameof(command), command, null), - }; - - var (exitCode, stdout, stderr) = CaptureConsole(() => command switch - { - "symbols" => QueryCommandRunner.RunSymbols(args, _jsonOptions), - "search" => QueryCommandRunner.RunSearch(args, _jsonOptions), - "impact" => QueryCommandRunner.RunImpact(args, _jsonOptions), - "excerpt" => QueryCommandRunner.RunExcerpt(args, _jsonOptions), - _ => throw new ArgumentOutOfRangeException(nameof(command), command, null), - }); - - Assert.Equal(CommandExitCodes.UsageError, exitCode); - // The #161 bug was exactly this: real results on stdout alongside the stderr Error. - // stdout must stay empty so callers that branch on exit code alone cannot consume - // silently-defaulted output as if it were valid data. - // #161 の本質はここ。exit code だけを見る呼び出し元がデフォルト差し替えの出力を - // 正当な結果として消費しないよう、stdout は空でなければならない。 - Assert.Equal(string.Empty, stdout); - Assert.Contains(expectedErrorFragment, stderr); - Assert.Contains($"got '{value}'", stderr); - Assert.Contains("Hint: fix the invalid or missing option value", stderr); - Assert.Contains($"Usage: {ConsoleUi.GetUsageLine(command)}", stderr); - } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); - } - } - - [Fact] - public void ParseArgs_ImpactMaxHopsCanonicalAndDepthDeprecatedAliasShareValue() - { - var maxHops = QueryCommandRunner.ParseArgs(["Run", "--max-hops", "2"], jsonDefault: false); - Assert.Equal(2, maxHops.ContextAfter); - Assert.True(maxHops.ContextAfterExplicit); - Assert.False(maxHops.ImpactDeprecatedDepthUsed); - - var depthAlias = QueryCommandRunner.ParseArgs(["Run", "--depth", "3"], jsonDefault: false); - Assert.Equal(3, depthAlias.ContextAfter); - Assert.True(depthAlias.ContextAfterExplicit); - Assert.True(depthAlias.ImpactDeprecatedDepthUsed); - } - - // Regression lock for #184: when a value-taking option is followed by the next recognized - // `--flag` token (e.g. `--limit --lang rust`), the parser must NOT consume the next flag as - // this option's value. Previously `--limit --lang rust` silently stole `--lang` as the - // `--limit` value, failed TryParsePositiveInt, and surfaced a confusing error — while - // `--lang` itself was dropped. Now the parser detects the recognized-option token and - // reports "requires a value" cleanly, matching the contract that a missing value fails - // closed instead of swallowing neighboring flags. - // #184 の回帰ロック: value-taking オプションの直後に別の `--flag` が来た場合 - // (例: `--limit --lang rust`)、パーサはその次のフラグを値として取り込んではならない。 - // 以前は `--limit --lang rust` が silent に `--lang` を `--limit` の値として奪っており、 - // TryParsePositiveInt が失敗して分かりにくいエラーが出る一方で `--lang` も落ちていた。 - // 今は recognized-option token を検知して "requires a value" でクリーンに失敗させ、値欠落は - // 黙って隣接フラグを飲み込まず fail-close する、という契約に合わせる。 - [Theory] - [InlineData(new[] { "search", "hello", "--limit", "--lang", "rust" }, "--limit requires a value.")] - [InlineData(new[] { "search", "hello", "--lang", "--limit", "5" }, "--lang requires a value.")] - [InlineData(new[] { "search", "hello", "--snippet-lines", "--limit", "5" }, "--snippet-lines requires a value.")] - [InlineData(new[] { "search", "hello", "--snippet-focus", "--limit", "5" }, "--snippet-focus requires a value.")] - [InlineData(new[] { "search", "hello", "--max-line-width", "--limit", "5" }, "--max-line-width requires a value.")] - [InlineData(new[] { "symbols", "hello", "--kind", "--lang", "rust" }, "--kind requires a value.")] - [InlineData(new[] { "impact", "hello", "--depth", "--lang", "rust" }, "--depth requires a value.")] - public void QueryEntrypoints_RecognizedOptionAsValueFailsClosed_Issue184(string[] commandAndArgs, string expectedFragment) - { - var command = commandAndArgs[0]; - var projectRoot = TestProjectHelper.CreateTempProject($"cdidx_issue184_{command}_{expectedFragment.GetHashCode():x}"); - try - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); - TestProjectHelper.InsertIndexedFile( - dbPath, - "src/Issue184.cs", - "csharp", - "namespace Issue184; public class T { public void M() { } }"); - MarkGraphAndFoldReady(dbPath); - - var args = commandAndArgs.Skip(1).Concat(new[] { "--db", dbPath, "--json" }).ToArray(); - var (exitCode, stdout, stderr) = CaptureConsole(() => command switch - { - "search" => QueryCommandRunner.RunSearch(args, _jsonOptions), - "symbols" => QueryCommandRunner.RunSymbols(args, _jsonOptions), - "impact" => QueryCommandRunner.RunImpact(args, _jsonOptions), - _ => throw new ArgumentOutOfRangeException(nameof(command), command, null), - }); - - Assert.Equal(CommandExitCodes.UsageError, exitCode); - Assert.Equal(string.Empty, stdout); - Assert.Contains(expectedFragment, stderr); - Assert.Contains("Hint: fix the invalid or missing option value", stderr); - } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); - } - } - - // Regression lock for #184: non-repeatable value-taking options specified more than once - // (e.g. `--db /A --db /B`, `--limit 5 --limit 10`) must emit a stderr warning so the - // override is visible. The last value wins (backward compatible), but users and AI callers - // can spot a copy/paste or scripted mistake instead of the previous silent take-last. - // `--top` is canonicalized to `--limit`, so `--limit 5 --top 10` also triggers. - // Repeatable options (`--path`, `--exclude-path`, `--name`) must NOT warn. - // #184 の回帰ロック: 非 repeatable な value-taking オプションが 2 回以上指定された場合 - // (例: `--db /A --db /B`、`--limit 5 --limit 10`)、上書きを stderr に警告する。最後の値が - // 採用されるのは後方互換だが、従来の silent な take-last ではスクリプトやコピペのミスに - // ユーザーや AI 呼び出し側が気付けない。`--top` は `--limit` と canonical 共有。repeatable - // な `--path` / `--exclude-path` / `--name` は警告しない。 - [Fact] - public void QueryEntrypoints_DuplicateSingleValueOptionsWarn_Issue184() - { - var projectRoot = TestProjectHelper.CreateTempProject("cdidx_issue184_dup_single_value"); - try - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); - TestProjectHelper.InsertIndexedFile( - dbPath, - "src/Issue184.cs", - "csharp", - "namespace Issue184; public class T { public void M() { } }"); - - // Warnings must fire even when the command itself returns zero results (NotFound); - // the duplicate flag warning is an argv-parsing concern, not a result-set concern. - // NotFound 等のゼロ件応答でも warn は出るべき。重複フラグは argv 解析段階の関心事で、 - // 検索結果の有無とは独立している。 - - // --limit appears twice with different values: rightmost CLI value wins, warning emitted. - var (exitLimit, _, stderrLimit) = CaptureConsole(() => QueryCommandRunner.RunSearch( - ["Issue184", "--db", dbPath, "--json", "--limit", "5", "--limit", "10"], _jsonOptions)); - Assert.NotEqual(CommandExitCodes.UsageError, exitLimit); - Assert.Contains("Warning: --limit specified more than once; the rightmost CLI value '10' takes precedence over earlier CLI values and any environment/config default.", stderrLimit); - - // --top is canonicalized to --limit, so `--limit 5 --top 10` also warns. - var (exitTop, _, stderrTop) = CaptureConsole(() => QueryCommandRunner.RunSearch( - ["Issue184", "--db", dbPath, "--json", "--limit", "5", "--top", "10"], _jsonOptions)); - Assert.NotEqual(CommandExitCodes.UsageError, exitTop); - Assert.Contains("Warning: --limit specified more than once; the rightmost CLI value '10' takes precedence over earlier CLI values and any environment/config default.", stderrTop); - - // Single --limit must NOT warn. - var (exitSingle, _, stderrSingle) = CaptureConsole(() => QueryCommandRunner.RunSearch( - ["Issue184", "--db", dbPath, "--json", "--limit", "10"], _jsonOptions)); - Assert.NotEqual(CommandExitCodes.UsageError, exitSingle); - Assert.DoesNotContain("specified more than once", stderrSingle); - - // Repeatable --path must NOT warn on repetition. - var (exitPath, _, stderrPath) = CaptureConsole(() => QueryCommandRunner.RunSearch( - ["Issue184", "--db", dbPath, "--json", "--path", "src/**", "--path", "tests/**"], _jsonOptions)); - Assert.NotEqual(CommandExitCodes.UsageError, exitPath); - Assert.DoesNotContain("specified more than once", stderrPath); - } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); - } - } - - // Regression lock for #184 follow-up: options that legitimately accept separated - // dash-prefixed literal values (`--db`, `--path`, `--exclude-path`) must preserve the - // pre-existing "requires a value" error WITH the inline-form hint when followed by a - // double-dash token. The recognized-option guard added for `--lang --limit 5` style - // cases must NOT short-circuit these dashed-literal options; instead their error must - // still guide the user to the `--db=` disambiguation form. `--query` keeps - // accepting dashed literals as query text (no hint needed). - // #184 のフォローアップ回帰ロック: separated dashed literal を正当に受け入れる - // `--db` / `--path` / `--exclude-path` は、double-dash 値が続いた場合 "requires a value" と - // 同時に inline-form ヒントを返す既存契約を維持する。`--lang --limit 5` 系のために追加した - // recognized-option guard でこれらを早期に短絡させてはならず、ヒントで `--db=` 形式を - // ユーザーに案内する。`--query` は引き続き dashed literal を query テキストとして受け入れる。 - [Theory] - [InlineData("--db")] - [InlineData("--path")] - [InlineData("--exclude-path")] - public void QueryEntrypoints_DashedLiteralOptionsKeepHint_Issue184(string optionName) - { - var projectRoot = TestProjectHelper.CreateTempProject($"cdidx_issue184_dashed_literal_{optionName.TrimStart('-')}"); - try - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); - TestProjectHelper.InsertIndexedFile( - dbPath, - "src/Issue184.cs", - "csharp", - "namespace Issue184; public class T { public void M() { } }"); - - string[] args = optionName == "--db" - ? ["Issue184", optionName, "--json"] - : ["Issue184", "--db", dbPath, optionName, "--json"]; - - var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch(args, _jsonOptions)); - - Assert.Equal(CommandExitCodes.UsageError, exitCode); - Assert.Equal(string.Empty, stdout); - Assert.Contains($"{optionName} requires a value", stderr); - Assert.Contains($"pass it as `{optionName}=`", stderr); - } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); - } - } - - // Regression lock for #184 follow-up: `--query` accepts dashed literals including - // recognized flags (e.g. `--json`) as query text, because FTS-style queries can - // legitimately contain dash-prefixed tokens. The recognized-option guard must NOT - // short-circuit `--query`, and the downstream IsRejectedSeparatedStringValue check - // must also skip `--query` so the flag-shaped token flows through as a literal. - // #184 のフォローアップ回帰ロック: `--query` は `--json` のような既知フラグを含む dashed - // literal をクエリ本文として受け入れる(FTS 風クエリには dash 付きトークンが現れ得る)。 - // recognized-option guard で `--query` を早期に短絡してはならず、後段の - // IsRejectedSeparatedStringValue も `--query` を素通りさせて flag 形状のトークンをリテラルと - // して扱う契約を維持する。 - [Fact] - public void RunFind_QueryAcceptsDashedLiteralValue_Issue184() - { - var projectRoot = TestProjectHelper.CreateTempProject("cdidx_issue184_query_dashed_literal"); - try - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); - TestProjectHelper.InsertIndexedFile( - dbPath, - "src/Issue184.cs", - "csharp", - "namespace Issue184; public class T { public void M() { } }"); - - var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( - ["--query", "--json", "--path", "src/**", "--db", dbPath, "--json"], _jsonOptions)); - - // Accepting `--json` as query text is the success contract; the query may or may not - // match, but parsing must NOT fail with a "requires a value" error for --query. - // `--json` をクエリテキストとして受け入れるのが成功契約。ヒットの有無は問わないが、 - // --query が "requires a value" で失敗してはならない。 - Assert.NotEqual(CommandExitCodes.UsageError, exitCode); - Assert.DoesNotContain("--query requires a value", stderr); - } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); - } - } - - // Regression lock for #184 follow-up: `--focus-column` requires a positive integer, - // while `--max-line-width` accepts non-negative integers so `0` can disable truncation. - // Zero, negative, and non-numeric values must fail closed with UsageError and the - // corresponding validation message. Earlier tests only covered the missing-value case - // (which now short-circuits before TryParsePositiveInt), leaving these option-specific - // numeric contracts uncovered. - // #184 のフォローアップ回帰ロック: `--focus-column` は正の整数を要求し、 - // `--max-line-width` は切り詰め解除のため 0 を許容する非負整数。0・負数・非数値は - // UsageError と対応する validation message で fail-close する。以前のテストは値欠如 - // (今は TryParsePositiveInt 前に短絡する)しかカバーしていなかったため、 - // これらのオプション固有の数値契約を明示的にロックする。 - [Theory] - [InlineData("0")] - [InlineData("abc")] - public void RunExcerpt_RejectsInvalidFocusColumnValue(string invalidValue) - { - var projectRoot = TestProjectHelper.CreateTempProject($"cdidx_excerpt_invalid_focus_column_{invalidValue}"); - try - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); - TestProjectHelper.InsertIndexedFile(dbPath, "README.md", "markdown", "sample"); - - var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunExcerpt( - ["README.md", "--db", dbPath, "--start", "1", "--focus-column", invalidValue, "--json"], - _jsonOptions)); - - Assert.Equal(CommandExitCodes.UsageError, exitCode); - Assert.Contains("--focus-column requires an integer between 1 and 100000", stderr); - } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); - } - } - - [Theory] - [InlineData("-1")] - [InlineData("abc")] - public void RunReferences_RejectsNegativeOrNonNumericMaxLineWidthValue(string invalidValue) - { - var projectRoot = TestProjectHelper.CreateTempProject($"cdidx_references_invalid_max_line_width_{invalidValue}"); - try - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); - - var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( - ["target", "--db", dbPath, "--max-line-width", invalidValue, "--json"], - _jsonOptions)); - - Assert.Equal(CommandExitCodes.UsageError, exitCode); - Assert.Contains("--max-line-width requires an integer between 0 and 4096", stderr); - } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); - } - } - - // Regression lock for #196 follow-up: CLI `--max-line-width` above the shared ceiling - // (LineWidthFormatter.MaxAllowedLineWidth = 4096) must fail closed with UsageError and - // empty stdout, matching the MCP `maxLineWidth` contract. Previously the CLI silently - // clamped the value via LineWidthFormatter.ClampMaxLineWidth and ran to success, while - // MCP rejected the same 4097. Diverging contracts break automation that wires CLI and - // MCP together on a fail-close assumption. - // #196 のフォローアップ回帰ロック: CLI の `--max-line-width` が共有上限 - // (LineWidthFormatter.MaxAllowedLineWidth = 4096) を超えた場合、UsageError と空 stdout で - // fail-close しなければならず、MCP の `maxLineWidth` 契約と一致させる。以前は - // LineWidthFormatter.ClampMaxLineWidth で黙って丸めて成功していたため、MCP は 4097 を拒否する - // 一方で CLI は通るという不整合があり、fail-close 前提の自動化を壊していた。 - [Theory] - [InlineData("search", "4097", false)] - [InlineData("search", "8192", false)] - [InlineData("references", "4097", false)] - [InlineData("inspect", "4097", false)] - [InlineData("find", "4097", false)] - [InlineData("excerpt", "4097", false)] - // Inline `--max-line-width=` form: catches the silent-clamp regression on - // the `=`-attached path too. `find` historically rejected every inline `=` form at - // ValidateFindArgs with `unsupported option for find: --max-line-width=4097`, so - // this row also pins the `PrepareFindArgs` inline-`=` normalization. - // インライン `--max-line-width=` 形式の回帰ロック。`find` は歴史的に - // ValidateFindArgs の段階で inline `=` を全拒否していたため、`PrepareFindArgs` の - // inline-`=` 正規化もここで固定する。 - [InlineData("search", "4097", true)] - [InlineData("references", "4097", true)] - [InlineData("inspect", "4097", true)] - [InlineData("find", "4097", true)] - [InlineData("excerpt", "4097", true)] - public void QueryEntrypoints_MaxLineWidthAboveCeilingFailClosed_Issue196(string command, string value, bool useInlineEquals) - { - var projectRoot = TestProjectHelper.CreateTempProject( - $"cdidx_issue196_maxlinewidth_{command}_{value}_{(useInlineEquals ? "inline" : "space")}"); - try - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); - TestProjectHelper.InsertIndexedFile( - dbPath, - "src/Issue196Target.cs", - "csharp", - """" - namespace Issue196; - public class Issue196Target - { - public void Issue196Callee() { } - } - public class Issue196Caller - { - public void Run() - { - var target = new Issue196Target(); - target.Issue196Callee(); - } - } - """"); - MarkGraphAndFoldReady(dbPath); - - var excerptDir = Path.Combine(projectRoot, "src"); - Directory.CreateDirectory(excerptDir); - var excerptFilePath = Path.Combine(excerptDir, "Issue196Excerpt.cs"); - File.WriteAllText( - excerptFilePath, - "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\n"); - - string[] maxLineWidthArgs = useInlineEquals - ? [$"--max-line-width={value}"] - : ["--max-line-width", value]; - string[] args = command switch - { - "search" => ["Issue196", "--db", dbPath, "--json", .. maxLineWidthArgs], - "references" => ["Issue196Callee", "--db", dbPath, "--json", .. maxLineWidthArgs], - "inspect" => ["Issue196Target", "--db", dbPath, "--json", .. maxLineWidthArgs], - "find" => ["Issue196", "--path", excerptFilePath, "--db", dbPath, "--json", .. maxLineWidthArgs], - "excerpt" => [excerptFilePath, "--db", dbPath, "--start", "1", "--end", "1", "--json", .. maxLineWidthArgs], - _ => throw new ArgumentOutOfRangeException(nameof(command), command, null), - }; - - var (exitCode, stdout, stderr) = CaptureConsole(() => command switch - { - "search" => QueryCommandRunner.RunSearch(args, _jsonOptions), - "references" => QueryCommandRunner.RunReferences(args, _jsonOptions), - "inspect" => QueryCommandRunner.RunInspect(args, _jsonOptions), - "find" => QueryCommandRunner.RunFind(args, _jsonOptions), - "excerpt" => QueryCommandRunner.RunExcerpt(args, _jsonOptions), - _ => throw new ArgumentOutOfRangeException(nameof(command), command, null), - }); - - Assert.Equal(CommandExitCodes.UsageError, exitCode); - Assert.Equal(string.Empty, stdout); - Assert.Contains($"--max-line-width must be less than or equal to {LineWidthFormatter.MaxAllowedLineWidth}", stderr); - Assert.Contains($"got '{value}'", stderr); - Assert.Contains($"Usage: {ConsoleUi.GetUsageLine(command)}", stderr); - } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); - } - } - - [Fact] - public void RunValidate_KindFilterNarrowsIssues() - { - var projectRoot = TestProjectHelper.CreateTempProject("cdidx_validate_kind_filter"); - try - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); - Directory.CreateDirectory(Path.Combine(projectRoot, "src")); - File.WriteAllBytes( - Path.Combine(projectRoot, "src", "bom.cs"), - [0xEF, 0xBB, 0xBF, .. System.Text.Encoding.UTF8.GetBytes("class Bom {}\n")]); - File.WriteAllText( - Path.Combine(projectRoot, "src", "mixed.cs"), - "class Mixed {}\r\nclass Other {}\n"); - - var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( - [projectRoot, "--db", dbPath, "--json", "--quiet"], - _jsonOptions)); - Assert.Equal(CommandExitCodes.Success, indexExitCode); - Assert.Equal(string.Empty, indexStderr); - - var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunValidate( - ["--db", dbPath, "--json", "--kind", "bom"], - _jsonOptions)); - - using var document = ParseJsonOutput(stdout); - var json = document.RootElement; - - Assert.Equal(CommandExitCodes.Success, exitCode); - Assert.Equal(string.Empty, stderr); - Assert.Equal(1, json.GetProperty("count").GetInt32()); - Assert.Equal("bom", json.GetProperty("issues")[0].GetProperty("kind").GetString()); - } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); - } - } - - [Fact] - public void RunValidate_ReplacementCharJson_IncludesOriginAndSeverity() - { - var projectRoot = TestProjectHelper.CreateTempProject("cdidx_validate_replacement_origin"); - try - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); - Directory.CreateDirectory(Path.Combine(projectRoot, "src")); - File.WriteAllText( - Path.Combine(projectRoot, "src", "literal.cs"), - "class Literal { const char Value = '\uFFFD'; }\n"); - - var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( - [projectRoot, "--db", dbPath, "--json", "--quiet"], - _jsonOptions)); - Assert.Equal(CommandExitCodes.Success, indexExitCode); - Assert.Equal(string.Empty, indexStderr); - - var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunValidate( - ["--db", dbPath, "--json", "--kind", "replacement_char"], - _jsonOptions)); - - using var document = ParseJsonOutput(stdout); - var json = document.RootElement; - var issue = json.GetProperty("issues")[0]; - - Assert.Equal(CommandExitCodes.Success, exitCode); - Assert.Equal(string.Empty, stderr); - Assert.Equal(1, json.GetProperty("count").GetInt32()); - Assert.Equal("replacement_char", issue.GetProperty("kind").GetString()); - Assert.Equal(FileIssue.OriginSourceLiteral, issue.GetProperty("origin").GetString()); - Assert.Equal(FileIssue.SeverityInfo, issue.GetProperty("severity").GetString()); - } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); - } - } - - // `validate --kind replacement_chra` previously filtered the file_issues table by an - // unknown kind, returned zero rows, and printed the same "No encoding issues found." - // message a genuinely-clean repo would print — silently masking the typo. Round-2 adds - // a known-kind allowlist + did-you-mean hint (#1582). - // 従来 `validate --kind replacement_chra` は file_issues を 0 行に絞り込み、本当に - // クリーンな状態と同じ "No encoding issues found." を出して typo を握り潰していた。 - // round-2 で許可された kind 一覧と did-you-mean を追加した (#1582)。 - [Fact] - public void RunValidate_KindTypo_SuggestsClosestKind_Issue1582() - { - var projectRoot = TestProjectHelper.CreateTempProject("cdidx_validate_kind_typo"); - try - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); - Directory.CreateDirectory(Path.Combine(projectRoot, "src")); - File.WriteAllText( - Path.Combine(projectRoot, "src", "clean.cs"), - "class Clean {}\n"); - - var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( - [projectRoot, "--db", dbPath, "--json", "--quiet"], - _jsonOptions)); - Assert.Equal(CommandExitCodes.Success, indexExitCode); - Assert.Equal(string.Empty, indexStderr); - - var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunValidate( - ["--db", dbPath, "--kind", "replacement_chra"], - _jsonOptions)); - - Assert.Equal(CommandExitCodes.Success, exitCode); - Assert.Contains("No encoding issues found.", stderr); - Assert.Contains("'replacement_chra' is not a known validate kind", stderr); - Assert.Contains("Did you mean: --kind replacement_char?", stderr); - } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); - } - } - - // `search --lang csarp` previously emitted "No results found." with no language hint - // — RunSearch never called WriteLangHint. Round-2 wires WriteLangHint into the zero- - // result branch and lets it fall back to ReferenceExtractor.GetSupportedLanguages() - // when the typo'd value matches no indexed language (#1582). - // 従来 `search --lang csarp` は "No results found." だけ表示し、RunSearch から - // WriteLangHint を呼んでいなかった。round-2 で zero-result 分岐に WriteLangHint を - // 配線し、index 済み言語にマッチしない場合は ReferenceExtractor.GetSupportedLanguages() - // にフォールバックして提案を出すようにした (#1582)。 - [Fact] - public void RunSearch_LangTypo_SuggestsClosestLanguage_Issue1582() - { - var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_lang_typo"); - try - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); - TestProjectHelper.InsertIndexedFile( - dbPath, - "src/App.cs", - "csharp", - "class App { }\n"); - - var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( - ["nothing_matches_xyzzy", "--db", dbPath, "--lang", "csarp"], - _jsonOptions)); - - Assert.Equal(CommandExitCodes.Success, exitCode); - Assert.Equal(string.Empty, stdout); - Assert.Contains("No results found.", stderr); - Assert.Contains("Did you mean: --lang csharp?", stderr); - } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); - } - } - - // `--lang java` against a repo with no Java files used to print a confusing - // "Did you mean: --lang java?" because the fallback ReferenceExtractor match returned - // the exact input. Regression coverage for the round-3 fix that suppresses - // self-suggestions in WriteLangHint (#1582). - // Java を含まないリポジトリで `--lang java` を指定した際、フォールバックの - // ReferenceExtractor が入力と同じ値を返すため "Did you mean: --lang java?" という - // 紛らわしいメッセージが出ていた。round-3 で WriteLangHint が自己提案を抑止する - // ようにしたことの回帰ロック (#1582)。 - [Fact] - public void RunSearch_LangNotIndexedButSupported_DoesNotSelfSuggest_Issue1582() - { - var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_lang_no_self_suggest"); - try - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); - TestProjectHelper.InsertIndexedFile( - dbPath, - "src/App.cs", - "csharp", - "class App { }\n"); - - var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( - ["nothing_matches_xyzzy", "--db", dbPath, "--lang", "java"], - _jsonOptions)); - - Assert.Equal(CommandExitCodes.Success, exitCode); - Assert.Equal(string.Empty, stdout); - Assert.Contains("No results found.", stderr); - Assert.Contains("'java' not found in index", stderr); - Assert.DoesNotContain("Did you mean: --lang java?", stderr); - } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); - } - } - - [Fact] - public void BuildSymbolQueryList_TreatsPipeAsLiteralNameCharacter() - { - // `|` is a legitimate character in operator symbols (C# `operator |`, etc.); it must not - // be treated as OR syntax so those names stay searchable. - // `|` は `operator |` など演算子名に出現する有効な文字。OR 構文として分割してはならない。 - var options = QueryCommandRunner.ParseArgs(["|"], jsonDefault: false); - var (queries, hadInput) = QueryCommandRunner.BuildSymbolQueryList(options); - Assert.True(hadInput); - Assert.NotNull(queries); - Assert.Equal(new[] { "|" }, queries!); - - var compound = QueryCommandRunner.ParseArgs(["operator|"], jsonDefault: false); - var (compoundQueries, _) = QueryCommandRunner.BuildSymbolQueryList(compound); - Assert.Equal(new[] { "operator|" }, compoundQueries!); - } - - [Fact] - public void BuildSymbolQueryList_EmptyNameNowFailsAtParseTime() - { - // Empty inline/separated string values are now rejected during argument parsing before - // symbol-query normalization runs, so they cannot broaden into an all-symbols dump. - // 空文字の値は symbol-query 正規化まで進む前に引数解析で拒否される。 - var rejected = QueryCommandRunner.ParseArgs(["--name", ""], jsonDefault: false); - Assert.NotNull(rejected.ParseError); - Assert.Contains("--name requires a value", rejected.ParseError); - } - - [Fact] - public void RunSymbols_EmptyAfterNormalizationFailsClosed() - { - var projectRoot = TestProjectHelper.CreateTempProject("cdidx_symbols_empty_norm"); - try - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); - var (exit, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunSymbols(["--name", "", "--db", dbPath], _jsonOptions)); - Assert.Equal(1, exit); - Assert.Contains("--name requires a value", stderr); - - var (definitionExit, definitionStdout, definitionStderr) = CaptureConsole(() => QueryCommandRunner.RunDefinition( - ["@", "--db", dbPath, "--count", "--json"], - _jsonOptions)); - - Assert.Equal(CommandExitCodes.UsageError, definitionExit); - Assert.Contains("bare verbatim prefixes", definitionStderr); - Assert.Equal(string.Empty, definitionStdout); - - var (referencesExit, referencesStdout, referencesStderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( - ["@", "--db", dbPath, "--count", "--json"], - _jsonOptions)); - - Assert.Equal(CommandExitCodes.UsageError, referencesExit); - Assert.Contains("bare verbatim prefixes", referencesStderr); - Assert.Equal(string.Empty, referencesStdout); - - var (callersExit, callersStdout, callersStderr) = CaptureConsole(() => QueryCommandRunner.RunCallers( - ["@", "--db", dbPath, "--count", "--json"], - _jsonOptions)); - - Assert.Equal(CommandExitCodes.UsageError, callersExit); - Assert.Contains("bare verbatim prefixes", callersStderr); - Assert.Equal(string.Empty, callersStdout); - - var (calleesExit, calleesStdout, calleesStderr) = CaptureConsole(() => QueryCommandRunner.RunCallees( - ["@", "--db", dbPath, "--count", "--json"], - _jsonOptions)); - - Assert.Equal(CommandExitCodes.UsageError, calleesExit); - Assert.Contains("bare verbatim prefixes", calleesStderr); - Assert.Equal(string.Empty, calleesStdout); - - var (impactExit, impactStdout, impactStderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( - ["@", "--db", dbPath, "--count", "--json"], - _jsonOptions)); - - Assert.Equal(CommandExitCodes.UsageError, impactExit); - Assert.Contains("bare verbatim prefixes", impactStderr); - Assert.Equal(string.Empty, impactStdout); - - var (inspectExit, inspectStdout, inspectStderr) = CaptureConsole(() => QueryCommandRunner.RunInspect( - ["@", "--db", dbPath, "--json"], - _jsonOptions)); - - Assert.Equal(CommandExitCodes.UsageError, inspectExit); - Assert.Contains("bare verbatim prefixes", inspectStderr); - Assert.Equal(string.Empty, inspectStdout); - } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); - } - } - - [Fact] - public void GraphCommands_BodyOptionAddsCappedBodyExcerpt_Issue1594() - { - var projectRoot = TestProjectHelper.CreateTempProject("cdidx_graph_body"); - try - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); - TestProjectHelper.InsertIndexedFile(dbPath, "src/Session.cs", "csharp", """ - class Session - { - int Run(int user) - { - var value = user; - return value; - } - - int Login(int user) - { - return Run(user); - } - } - """); - using (var db = new DbContext(dbPath)) - { - using var select = db.Connection.CreateCommand(); - select.CommandText = "SELECT id FROM files WHERE path = 'src/Session.cs'"; - var fileId = Convert.ToInt32(select.ExecuteScalar()); - var writer = new DbWriter(db.Connection); - writer.InsertReferences([ - new ReferenceRecord - { - FileId = fileId, - SymbolName = "Run", - ReferenceKind = "call", - Line = 11, - Column = 16, - Context = " return Run(user);", - ContainerKind = "function", - ContainerName = "Login", - } - ]); - writer.MarkGraphReady(); - } - - AssertBodyExcerpt( - QueryCommandRunner.RunReferences, - ["Run", "--db", dbPath, "--json", "--body", "--snippet-lines", "1"], - "int Login(int user)"); - AssertBodyExcerpt( - QueryCommandRunner.RunCallers, - ["Run", "--db", dbPath, "--json", "--body", "--snippet-lines", "2"], - "int Login(int user)"); - AssertBodyExcerpt( - QueryCommandRunner.RunCallees, - ["Login", "--db", dbPath, "--json", "--body", "--snippet-lines", "1"], - "int Run(int user)"); - - var (impactExitCode, impactStdout, impactStderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( - ["Run", "--db", dbPath, "--json", "--body", "--snippet-lines", "2"], - _jsonOptions)); - - Assert.Equal(CommandExitCodes.Success, impactExitCode); - Assert.Equal(string.Empty, impactStderr); - using var impactDocument = ParseJsonOutput(impactStdout); - var impactCaller = impactDocument.RootElement.GetProperty("callers")[0]; - Assert.Contains("int Login(int user)", impactCaller.GetProperty("body_content").GetString()); - Assert.Equal(2, CountLines(impactCaller.GetProperty("body_content").GetString()!)); - } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); - } - } - - private void AssertBodyExcerpt( - Func command, - string[] args, - string expectedContent) - { - var (exitCode, stdout, stderr) = CaptureConsole(() => command(args, _jsonOptions)); - - Assert.True(exitCode == CommandExitCodes.Success, $"exit={exitCode}\nstdout={stdout}\nstderr={stderr}"); - Assert.Equal(string.Empty, stderr); - using var document = ParseJsonOutput(stdout); - Assert.Contains(expectedContent, document.RootElement.GetProperty("body_content").GetString()); - } - - private static int CountLines(string text) => text.Split('\n').Length; - - [Fact] - public void RunSymbols_ExactNameFindsXamlTargetType() - { - var projectRoot = TestProjectHelper.CreateTempProject("cdidx_xaml_target_type"); - try - { - Directory.CreateDirectory(Path.Combine(projectRoot, "src")); - File.WriteAllText( - Path.Combine(projectRoot, "src", "MainPage.xaml"), - """ - - -