From 0bbf5048f2862753b030e73735acaf03017596b7 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Mon, 27 Jul 2026 20:43:32 +0900 Subject: [PATCH 1/3] Fix callee call-site locations (#4841) --- DEVELOPER_GUIDE.md | 4 + USER_GUIDE.md | 4 + changelog.d/unreleased/4841.fixed.md | 19 +++ src/CodeIndex/Cli/ProjectionFieldRegistry.cs | 9 +- src/CodeIndex/Cli/QueryCommandRunner.Graph.cs | 14 +- .../Cli/QueryCommandRunner.Locations.cs | 9 +- .../Database/DbReader.GraphQueries.cs | 26 +++- src/CodeIndex/Mcp/McpToolHandlers.Graph.cs | 2 +- src/CodeIndex/Mcp/McpToolHandlers.cs | 2 + src/CodeIndex/Models/QueryResults.cs | 6 + tests/CodeIndex.Tests/DbReaderTests.cs | 44 ++++++ .../McpServerToolsCallTests.cs | 66 +++++++++ .../ProjectionFieldRegistryIssue4836Tests.cs | 7 +- .../QueryCommandRunnerReferencesTests.cs | 138 ++++++++++++++++++ 14 files changed, 328 insertions(+), 22 deletions(-) create mode 100644 changelog.d/unreleased/4841.fixed.md diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 2983801a5..18f14806d 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -1109,6 +1109,8 @@ mixed-language evidence unresolved. `symbol_references.reference_kind` stores raw extractor labels. Default call-graph surfaces (`callers`, `callees`, inspect/analyze caller and callee bundles, and their JSON/MCP fields) expose the canonical public vocabulary `call`, `instantiate`, and `subscribe`. The primary `reference_kind`, `reference_kinds`, and `reference_kind_counts` keys use that same vocabulary. Use `--raw-kinds` on `callers` / `callees`, or `references --kind `, when debugging raw extractor output. +`DbReader.GetCallees` must preserve a representative physical call-site span while aggregating counts. It selects the smallest `(line, column_number)` among rows with a stored column, exposes that 1-based pair as `first_line` / nullable `first_column`, and exposes the called token width as `first_length`; `reference_count` remains the independent aggregate. When every contributing legacy row has `column_number IS NULL`, the reader retains the minimum line and a null column so CLI/MCP location adapters can degrade to line-only output without fabricating precision. + Reference extraction deduplicates only within the same indexed file and language context. When adding extractor paths, include the file id and language hint in shared `seen` keys so same line/column/name edges from polyglot workspaces do not collapse across Java, Rust, C#, SQL, or other language-specific normalization contexts. | Raw kind | Logical graph kind | Notes | @@ -4273,6 +4275,8 @@ authoritative な判定と一貫した推論を区別し、複数言語の evide `symbol_references.reference_kind` には extractor が出力した raw label を保存する。既定の call-graph 表示(`callers`、`callees`、inspect/analyze の caller / callee bundle、および JSON/MCP フィールド)は、公開 canonical 語彙 `call`、`instantiate`、`subscribe` を返す。primary `reference_kind`、`reference_kinds`、`reference_kind_counts` の key はすべて同じ語彙を使う。raw extractor 出力を調べる場合は、`callers` / `callees` の `--raw-kinds`、または `references --kind ` を使う。 +`DbReader.GetCallees` は count を集約しながら、代表となる物理 call-site span を保持しなければならない。列が保存された row のうち最小の `(line, column_number)` を選び、その 1-based 座標を `first_line` / nullable な `first_column`、呼び出し token 幅を `first_length` として公開し、`reference_count` は独立した集約値のままにする。寄与する legacy row がすべて `column_number IS NULL` の場合、reader は最小行と null 列を保持し、CLI/MCP の location adapter が精度を捏造せず line-only 出力へ劣化できるようにする。 + | Raw kind | Logical graph kind | 備考 | |---|---|---| | `call` | `call` | 直接実行される呼び出しエッジ。 | diff --git a/USER_GUIDE.md b/USER_GUIDE.md index caebefd90..776273536 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -1643,6 +1643,8 @@ By default, `callers` and `callees` return only executable call, construction, a `callers` and `callees` rank results by weighted structural importance by default: `instantiate` edges count as `3.0`, direct `call` edges as `1.0`, and event `subscribe` edges as `0.1`. This keeps factory or composition-root constructor use from being buried under noisy event subscriptions. Use `--rank-by count` to restore raw `reference_count` ordering, or `--rank-by kind` to group `instantiate`, `call`, then `subscribe` before count. JSON rows keep the raw `reference_count` and add `reference_kind_counts` plus `reference_weight_score` so consumers can re-rank without losing the source counts; MCP structured rows expose the same data as `referenceKindCounts` and `referenceWeightScore`. +Grouped `callees` rows preserve the earliest precise call site separately from the aggregate `reference_count`. CLI JSON exposes its 1-based `first_line`, nullable `first_column`, and token-sized `first_length`; MCP uses the corresponding `firstLine`, `firstColumn`, and `firstLength` fields. Compact and quickfix output use the same 1-based column, SARIF carries the same token-sized region, and LSP converts it to a zero-based, non-empty token range. If every contributing legacy reference lacks a column, the column remains `null`, quickfix reports column `0`, SARIF omits `endColumn`, and LSP returns a zero-width range at the start of the retained source line instead of inventing a precise token location. + ### Outline a single file ```bash @@ -4740,6 +4742,8 @@ cdidx callees AddToGitExclude --exclude-tests `callers` と `callees` は既定で構造的重要度の weighted 順に並びます。`instantiate` は `3.0`、直接 `call` は `1.0`、event `subscribe` は `0.1` として数えるため、factory や composition root の constructor 利用が大量の event subscription に埋もれにくくなります。従来どおり生の `reference_count` で並べたい場合は `--rank-by count`、reference kind を優先して `instantiate`、`call`、`subscribe` の順でまとめたい場合は `--rank-by kind` を使ってください。JSON の各行は生の `reference_count` を維持し、`reference_kind_counts` と `reference_weight_score` も追加で返します。MCP structured row では同じ情報を `referenceKindCounts` と `referenceWeightScore` として返すため、consumer 側で再ランキングできます。 +集約された `callees` 行は、集約値の `reference_count` とは別に、最初の精密な call site を保持します。CLI JSON は 1-based の `first_line`、nullable な `first_column`、token 幅の `first_length` を返し、MCP は対応する `firstLine`、`firstColumn`、`firstLength` を使います。compact / quickfix も同じ 1-based 列を使い、SARIF は同じ token 幅の region、LSP は 0-based の非空 token range に変換します。寄与する legacy reference の全てで列が欠けている場合は列を `null` のまま保ち、quickfix は列 `0`、SARIF は `endColumn` を省略し、LSP は保持した source line の先頭に zero-width range を返すため、精密な token 位置を捏造しません。 + ### 1ファイルのアウトラインを見る ```bash diff --git a/changelog.d/unreleased/4841.fixed.md b/changelog.d/unreleased/4841.fixed.md new file mode 100644 index 000000000..6a44490ed --- /dev/null +++ b/changelog.d/unreleased/4841.fixed.md @@ -0,0 +1,19 @@ +--- +category: fixed +issues: + - 4841 +affected: + - src/CodeIndex/Database/DbReader.GraphQueries.cs + - src/CodeIndex/Models/QueryResults.cs + - src/CodeIndex/Cli/QueryCommandRunner.Graph.cs + - src/CodeIndex/Cli/QueryCommandRunner.Locations.cs + - src/CodeIndex/Mcp/McpToolHandlers.Graph.cs +--- + +## English + +- **`callees` now preserves the first physical call-site span in every location output (#4841)** — following the graph-location fidelity work in #4417, aggregated callee rows now expose a deterministic 1-based column and token length separately from `reference_count`; quickfix, LSP, SARIF, JSON, compact, and MCP output point at the called token, while legacy rows without columns degrade explicitly to line-only locations. + +## 日本語 + +- **`callees` がすべての location 出力で最初の物理 call-site span を保持するようになりました (#4841)** — #4417 の graph location fidelity 対応に続き、集約された callee 行は `reference_count` とは別に決定的な 1-based 列と token 長を公開し、quickfix、LSP、SARIF、JSON、compact、MCP は呼び出し token を指します。列を持たない legacy row は明示的に line-only location へ劣化します。 diff --git a/src/CodeIndex/Cli/ProjectionFieldRegistry.cs b/src/CodeIndex/Cli/ProjectionFieldRegistry.cs index 3f8efa7b0..f4666f6c8 100644 --- a/src/CodeIndex/Cli/ProjectionFieldRegistry.cs +++ b/src/CodeIndex/Cli/ProjectionFieldRegistry.cs @@ -239,9 +239,7 @@ private static ProjectionCommandFieldSchema CreateCallGraphSchema(string command var resultFields = isCallerCommand ? GetJsonFieldNames() : GetJsonFieldNames(); - var compactFields = isCallerCommand - ? new[] { "file", "line", "column" } - : ["file", "line"]; + string[] compactFields = ["file", "line", "column"]; return Create( command, compactFields, @@ -253,9 +251,8 @@ private static ProjectionCommandFieldSchema CreateCallGraphSchema(string command "reference_extraction_limits", "reference_graph_complete", "reference_extraction_cap_hits") .Alias("file", "path") - .Alias("line", "first_line"); - if (isCallerCommand) - builder.Alias("column", "first_column"); + .Alias("line", "first_line") + .Alias("column", "first_column"); }); } diff --git a/src/CodeIndex/Cli/QueryCommandRunner.Graph.cs b/src/CodeIndex/Cli/QueryCommandRunner.Graph.cs index 7a9c215ca..6381963a7 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.Graph.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.Graph.cs @@ -369,7 +369,7 @@ public static int RunCallees(string[] cmdArgs, JsonSerializerOptions jsonOptions { if (TryWriteFormattedLocations( options, - results.Select(r => new FormattedLocation(r.Path, r.FirstLine, null, $"{r.CallerName ?? ""} -> {r.CalleeName}")), + results.Select(r => new FormattedLocation(r.Path, r.FirstLine, r.FirstColumn, $"{r.CallerName ?? ""} -> {r.CalleeName}")), jsonOptions)) return CommandExitCodes.Success; if (options.OutputFormat == OutputFormatLsp) @@ -379,12 +379,20 @@ public static int RunCallees(string[] cmdArgs, JsonSerializerOptions jsonOptions } if (options.OutputFormat == OutputFormatQf) { - WriteQuickfix(results.Select(r => (r.Path, r.FirstLine, 1, $"{r.CallerName ?? ""} -> {r.CalleeName}"))); + WriteQuickfix(results.Select(r => (r.Path, r.FirstLine, r.FirstColumn ?? 0, $"{r.CallerName ?? ""} -> {r.CalleeName}"))); return CommandExitCodes.Success; } if (options.OutputFormat == OutputFormatSarif) { - WriteSarif(results.Select(r => (r.Path, r.FirstLine, 1, $"{r.CallerName ?? ""} -> {r.CalleeName}", r.ReferenceKind)), jsonOptions); + WriteSarif( + results.Select(r => new SarifLocation( + r.Path, + r.FirstLine, + r.FirstColumn ?? 1, + r.FirstColumn.HasValue ? r.FirstColumn.Value + Math.Max(1, r.FirstLength) : null, + $"{r.CallerName ?? ""} -> {r.CalleeName}", + r.ReferenceKind)), + jsonOptions); return CommandExitCodes.Success; } foreach (var r in results) diff --git a/src/CodeIndex/Cli/QueryCommandRunner.Locations.cs b/src/CodeIndex/Cli/QueryCommandRunner.Locations.cs index 5e75539ee..566f09f25 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.Locations.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.Locations.cs @@ -92,7 +92,14 @@ private static LspLocation ToLspLocation(CallerResult result) => BuildLspLocation(result.Path, result.FirstLine, Math.Max(1, result.FirstColumn), result.FirstLine, Math.Max(1, result.FirstColumn) + Math.Max(1, result.CalleeName.Length)); private static LspLocation ToLspLocation(CalleeResult result) - => BuildLspLocation(result.Path, result.FirstLine, 1, result.FirstLine, 1); + => result.FirstColumn.HasValue + ? BuildLspLocation( + result.Path, + result.FirstLine, + Math.Max(1, result.FirstColumn.Value), + result.FirstLine, + Math.Max(1, result.FirstColumn.Value) + Math.Max(1, result.FirstLength)) + : BuildLspLocation(result.Path, result.FirstLine, 1, result.FirstLine, 1); private static void WriteLspLocations(IEnumerable locations, JsonSerializerOptions jsonOptions) { diff --git a/src/CodeIndex/Database/DbReader.GraphQueries.cs b/src/CodeIndex/Database/DbReader.GraphQueries.cs index 3d382ccaf..7236b9635 100644 --- a/src/CodeIndex/Database/DbReader.GraphQueries.cs +++ b/src/CodeIndex/Database/DbReader.GraphQueries.cs @@ -510,7 +510,11 @@ WITH logical_references AS ( {calleeGroupKindSql} AS count_reference_kind, COUNT(*) AS reference_count, {ReferenceWeightedScoreSql("r.reference_kind")} AS weighted_score, - r.line + r.line, + CASE + WHEN r.column_number IS NULL THEN NULL + ELSE (CAST(r.line AS INTEGER) * 4294967296 + r.column_number) + END AS location_key FROM symbol_references r JOIN files f ON r.file_id = f.id WHERE r.container_name IS NOT NULL @@ -568,7 +572,10 @@ WHERE r.container_name IS NOT NULL GROUP BY f.path, f.lang, r.container_kind, r.container_name, r.symbol_name, r.file_id, r.line, r.column_number, r.reference_kind ) SELECT path, lang, container_kind, container_name, symbol_name, - reference_kind, MIN(line) AS first_line, SUM(r.reference_count) AS reference_count, + reference_kind, + COALESCE((MIN(location_key) / 4294967296), MIN(line)) AS first_line, + (MIN(location_key) % 4294967296) AS first_column, + SUM(r.reference_count) AS reference_count, GROUP_CONCAT(DISTINCT reference_kind) AS reference_kinds, GROUP_CONCAT(r.count_reference_kind || ':' || r.reference_count) AS reference_kind_counts, SUM(r.weighted_score) AS weighted_score @@ -619,25 +626,28 @@ FROM logical_references r while (reader.TrackedRead()) { var primaryKind = reader.GetString(5); - var kindAggregate = TruncateReferenceKindAggregate(GetNullableString(reader, 8), out var kindsTruncated); - var countAggregate = TruncateReferenceKindAggregate(GetNullableString(reader, 9), out var countsTruncated); + var kindAggregate = TruncateReferenceKindAggregate(GetNullableString(reader, 9), out var kindsTruncated); + var countAggregate = TruncateReferenceKindAggregate(GetNullableString(reader, 10), out var countsTruncated); var kinds = ParseDistinctReferenceKinds(kindAggregate, primaryKind); - var counts = ParseReferenceKindCounts(countAggregate, primaryKind, reader.GetInt32(7)); + var counts = ParseReferenceKindCounts(countAggregate, primaryKind, reader.GetInt32(8)); + var calleeName = reader.GetString(4); results.Add(new CalleeResult { Path = reader.GetString(0), Lang = GetNullableString(reader, 1), CallerKind = GetNullableString(reader, 2), CallerName = GetNullableString(reader, 3), - CalleeName = reader.GetString(4), + CalleeName = calleeName, ReferenceKind = primaryKind, ReferenceKinds = kinds, HasMixedReferenceKinds = kinds.Count > 1, ReferenceKindCounts = counts, AggregateTruncated = kindsTruncated || countsTruncated, - ReferenceWeightScore = reader.GetDouble(10), + ReferenceWeightScore = reader.GetDouble(11), FirstLine = reader.GetInt32(6), - ReferenceCount = reader.GetInt32(7), + FirstColumn = GetNullableInt32(reader, 7), + FirstLength = calleeName.Length, + ReferenceCount = reader.GetInt32(8), }); } return results; diff --git a/src/CodeIndex/Mcp/McpToolHandlers.Graph.cs b/src/CodeIndex/Mcp/McpToolHandlers.Graph.cs index e4f78e4e5..c7d3ddbb8 100644 --- a/src/CodeIndex/Mcp/McpToolHandlers.Graph.cs +++ b/src/CodeIndex/Mcp/McpToolHandlers.Graph.cs @@ -342,7 +342,7 @@ private JsonNode ExecuteCallees(JsonNode? id, JsonNode? args) }; AddPaginatedResultEnvelope(payload, results.Count, total, truncated, offset); if (format == "compact") - ApplyCompactResults(payload, results, result => result.Path, result => result.FirstLine); + ApplyCompactResults(payload, results, result => result.Path, result => result.FirstLine, result => result.FirstColumn); payload["aggregate_truncated"] = results.Any(result => result.AggregateTruncated); if (exact) AddExactGraphSignal(payload, exactSignal); diff --git a/src/CodeIndex/Mcp/McpToolHandlers.cs b/src/CodeIndex/Mcp/McpToolHandlers.cs index ad46b6a89..0ee25c83c 100644 --- a/src/CodeIndex/Mcp/McpToolHandlers.cs +++ b/src/CodeIndex/Mcp/McpToolHandlers.cs @@ -679,6 +679,8 @@ private static JsonArray BuildCompactCalleeRows(IEnumerable result { ["file"] = result.Path, ["line"] = result.FirstLine, + ["column"] = result.FirstColumn, + ["length"] = result.FirstLength, ["caller_kind"] = result.CallerKind, ["caller"] = result.CallerName, ["callee"] = result.CalleeName, diff --git a/src/CodeIndex/Models/QueryResults.cs b/src/CodeIndex/Models/QueryResults.cs index a1948474d..a04e4d1dc 100644 --- a/src/CodeIndex/Models/QueryResults.cs +++ b/src/CodeIndex/Models/QueryResults.cs @@ -734,6 +734,12 @@ public class CalleeResult public bool AggregateTruncated { get; set; } public double ReferenceWeightScore { get; set; } public int FirstLine { get; set; } + // The representative call-site span is explicit so location consumers do not + // reconstruct a column from display text. Legacy rows can retain a null column. + // 代表 call-site の span を明示し、location consumer が表示文字列から列を + // 復元しないようにする。legacy row の列は null のまま保持できる。 + public int? FirstColumn { get; set; } + public int FirstLength { get; set; } public int ReferenceCount { get; set; } [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? BodyContent { get; set; } diff --git a/tests/CodeIndex.Tests/DbReaderTests.cs b/tests/CodeIndex.Tests/DbReaderTests.cs index dc2d94044..5adda6786 100644 --- a/tests/CodeIndex.Tests/DbReaderTests.cs +++ b/tests/CodeIndex.Tests/DbReaderTests.cs @@ -2075,6 +2075,50 @@ public void GetCallees_ReturnsReferencedSymbolsForCaller() Assert.Equal("call", callee.ReferenceKind); } + [Fact] + public void GetCallees_PreservesFirstPreciseCallSiteAndLegacyLineOnlyFallback_Issue4841() + { + const string targetName = "Issue4841Target"; + const string callerName = "Issue4841Caller"; + const string callLine = " void Issue4841Caller() { var text = \"日本語\";\tIssue4841Target(); Issue4841Target(); }"; + var source = string.Join('\n', + "class Issue4841Probe", + "{", + " void Issue4841Target() { }", + callLine, + "}", + ""); + var expectedColumn = callLine.IndexOf(targetName, StringComparison.Ordinal) + 1; + InsertIndexedFile("src/Issue4841Probe.cs", "csharp", source); + + var precise = Assert.Single(_reader.GetCallees(callerName, lang: "csharp", exact: true)); + + Assert.Equal(4, precise.FirstLine); + Assert.Equal(expectedColumn, precise.FirstColumn); + Assert.Equal(targetName.Length, precise.FirstLength); + Assert.Equal(2, precise.ReferenceCount); + + using (var command = _db.Connection.CreateCommand()) + { + command.CommandText = """ + UPDATE symbol_references + SET column_number = NULL + WHERE container_name = @caller + AND symbol_name = @target; + """; + command.Parameters.AddWithValue("@caller", callerName); + command.Parameters.AddWithValue("@target", targetName); + Assert.Equal(2, command.ExecuteNonQuery()); + } + + var legacy = Assert.Single(_reader.GetCallees(callerName, lang: "csharp", exact: true)); + + Assert.Equal(4, legacy.FirstLine); + Assert.Null(legacy.FirstColumn); + Assert.Equal(targetName.Length, legacy.FirstLength); + Assert.Equal(2, legacy.ReferenceCount); + } + [Fact] public void GetCallers_CppFriendReferenceRequiresExplicitKind() { diff --git a/tests/CodeIndex.Tests/McpServerToolsCallTests.cs b/tests/CodeIndex.Tests/McpServerToolsCallTests.cs index 7c6735ab7..3722d6039 100644 --- a/tests/CodeIndex.Tests/McpServerToolsCallTests.cs +++ b/tests/CodeIndex.Tests/McpServerToolsCallTests.cs @@ -1775,6 +1775,72 @@ private void OnChanged(object? sender, EventArgs e) { } Assert.Equal(new[] { "subscribe" }, kinds); } + [Fact] + public void ToolsCall_Callees_PreservesFirstCallSiteSpanInFullAndCompactRows_Issue4841() + { + const string targetName = "Issue4841McpTarget"; + const string callerName = "Issue4841McpCaller"; + const string callLine = " void Issue4841McpCaller() { var text = \"日本語\";\tIssue4841McpTarget(); Issue4841McpTarget(); }"; + var source = string.Join('\n', + "class Issue4841McpProbe", + "{", + " void Issue4841McpTarget() { }", + callLine, + "}", + ""); + var expectedColumn = callLine.IndexOf(targetName, StringComparison.Ordinal) + 1; + InsertIndexedFile("src/Issue4841McpProbe.cs", "csharp", source); + + var fullRequest = new JsonObject + { + ["jsonrpc"] = "2.0", + ["id"] = 4841, + ["method"] = "tools/call", + ["params"] = new JsonObject + { + ["name"] = "callees", + ["arguments"] = new JsonObject + { + ["query"] = callerName, + ["lang"] = "csharp", + ["exact"] = true, + }, + }, + }; + var fullResponse = _server.HandleMessage(fullRequest)!; + var structured = fullResponse["result"]!["structuredContent"]!; + var fullRow = structured["results"]![0]!; + + Assert.Equal(1, structured["count"]!.GetValue()); + Assert.Equal(4, fullRow["firstLine"]!.GetValue()); + Assert.Equal(expectedColumn, fullRow["firstColumn"]!.GetValue()); + Assert.Equal(targetName.Length, fullRow["firstLength"]!.GetValue()); + Assert.Equal(2, fullRow["referenceCount"]!.GetValue()); + + var compactRequest = new JsonObject + { + ["jsonrpc"] = "2.0", + ["id"] = 4842, + ["method"] = "tools/call", + ["params"] = new JsonObject + { + ["name"] = "callees", + ["arguments"] = new JsonObject + { + ["query"] = callerName, + ["lang"] = "csharp", + ["exact"] = true, + ["format"] = "compact", + }, + }, + }; + var compactResponse = _server.HandleMessage(compactRequest)!; + var compactRow = compactResponse["result"]!["structuredContent"]!["results"]![0]!; + + Assert.Equal(4, compactRow["line"]!.GetValue()); + Assert.Equal(expectedColumn, compactRow["column"]!.GetValue()); + } + [Fact] public void ToolsCall_References_ReturnsIndexedReference() { diff --git a/tests/CodeIndex.Tests/ProjectionFieldRegistryIssue4836Tests.cs b/tests/CodeIndex.Tests/ProjectionFieldRegistryIssue4836Tests.cs index e1f3aa2d5..5e2968cdb 100644 --- a/tests/CodeIndex.Tests/ProjectionFieldRegistryIssue4836Tests.cs +++ b/tests/CodeIndex.Tests/ProjectionFieldRegistryIssue4836Tests.cs @@ -190,6 +190,8 @@ public void EveryDiscoveredProjectionField_ValidatesFromTheSameRegistry_Issue483 [InlineData("callers", "aggregate_truncated")] [InlineData("callers", "first_column")] [InlineData("callees", "body_content_recovery")] + [InlineData("callees", "first_column")] + [InlineData("callees", "first_length")] [InlineData("impact", "path_details")] [InlineData("map", "language_count")] [InlineData("map", "module_count")] @@ -224,7 +226,6 @@ public void ExistingConditionalAndModeSpecificFields_RemainValid_Issue4836( [InlineData("symbols", "is_metadata_target")] [InlineData("symbols", "metadata_target_source")] [InlineData("symbols", "same_line_signature_occurrence_index")] - [InlineData("callees", "first_column")] [InlineData("callees", "has_self_reference")] [InlineData("callees", "has_mutual_recursion")] public void NonOutputFields_AreNotAdvertisedOrAccepted_Issue4836( @@ -241,10 +242,10 @@ public void NonOutputFields_AreNotAdvertisedOrAccepted_Issue4836( } [Fact] - public void CallGraphCompactDefaults_AreCommandSpecific_Issue4836() + public void CallGraphCompactDefaults_IncludeCallSiteColumns_Issue4836_Issue4841() { Assert.Contains("column", ProjectionFieldRegistry.GetCompactFields("callers")!); - Assert.DoesNotContain("column", ProjectionFieldRegistry.GetCompactFields("callees")!); + Assert.Contains("column", ProjectionFieldRegistry.GetCompactFields("callees")!); } [Fact] diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerReferencesTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerReferencesTests.cs index f399089b1..8bcb450e7 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerReferencesTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerReferencesTests.cs @@ -9,6 +9,144 @@ namespace CodeIndex.Tests; public partial class QueryCommandRunnerTests { + [Fact] + public void RunCallees_LocationFormatsPreserveFirstCallSiteSpanAndLegacyFallback_Issue4841() + { + const string targetName = "Issue4841CliTarget"; + const string callerName = "Issue4841CliCaller"; + const string callLine = " void Issue4841CliCaller() { var text = \"日本語\";\tIssue4841CliTarget(); Issue4841CliTarget(); }"; + var source = string.Join('\n', + "class Issue4841CliProbe", + "{", + " void Issue4841CliTarget() { }", + callLine, + "}", + ""); + var expectedColumn = callLine.IndexOf(targetName, StringComparison.Ordinal) + 1; + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_callees_locations_issue4841"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Issue4841CliProbe.cs", "csharp", source); + MarkGraphAndFoldReady(dbPath); + string[] baseArgs = [callerName, "--db", dbPath, "--lang", "csharp", "--exact"]; + + var (jsonExit, jsonStdout, jsonStderr) = CaptureConsole(() => QueryCommandRunner.RunCallees( + [.. baseArgs, "--json"], + _jsonOptions)); + var jsonRow = Assert.Single(ParseJsonLines(jsonStdout)).RootElement; + Assert.Equal(CommandExitCodes.Success, jsonExit); + Assert.Equal(string.Empty, jsonStderr); + Assert.Equal(4, jsonRow.GetProperty("first_line").GetInt32()); + Assert.Equal(expectedColumn, jsonRow.GetProperty("first_column").GetInt32()); + Assert.Equal(targetName.Length, jsonRow.GetProperty("first_length").GetInt32()); + Assert.Equal(2, jsonRow.GetProperty("reference_count").GetInt32()); + + var (compactExit, compactStdout, compactStderr) = CaptureConsole(() => QueryCommandRunner.RunCallees( + [.. baseArgs, "--format", "compact"], + _jsonOptions)); + using var compactDocument = ParseJsonOutput(compactStdout); + var compactRow = compactDocument.RootElement.GetProperty("results")[0]; + Assert.Equal(CommandExitCodes.Success, compactExit); + Assert.Equal(string.Empty, compactStderr); + Assert.Equal(expectedColumn, compactRow.GetProperty("column").GetInt32()); + + var (quickfixExit, quickfixStdout, quickfixStderr) = CaptureConsole(() => QueryCommandRunner.RunCallees( + [.. baseArgs, "--format", "qf"], + _jsonOptions)); + Assert.Equal(CommandExitCodes.Success, quickfixExit); + Assert.Equal(string.Empty, quickfixStderr); + Assert.Equal( + $"src/Issue4841CliProbe.cs:4:{expectedColumn}:{callerName} -> {targetName}", + quickfixStdout.Trim()); + + var (lspExit, lspStdout, lspStderr) = CaptureConsole(() => QueryCommandRunner.RunCallees( + [.. baseArgs, "--format", "lsp"], + _jsonOptions)); + using var lspDocument = ParseJsonOutput(lspStdout); + var preciseRange = lspDocument.RootElement[0].GetProperty("range"); + Assert.Equal(CommandExitCodes.Success, lspExit); + Assert.Equal(string.Empty, lspStderr); + Assert.Equal(3, preciseRange.GetProperty("start").GetProperty("line").GetInt32()); + Assert.Equal(expectedColumn - 1, preciseRange.GetProperty("start").GetProperty("character").GetInt32()); + Assert.Equal(expectedColumn - 1 + targetName.Length, preciseRange.GetProperty("end").GetProperty("character").GetInt32()); + + var (sarifExit, sarifStdout, sarifStderr) = CaptureConsole(() => QueryCommandRunner.RunCallees( + [.. baseArgs, "--format", "sarif"], + _jsonOptions)); + using var sarifDocument = ParseJsonOutput(sarifStdout); + var preciseRegion = sarifDocument.RootElement + .GetProperty("runs")[0] + .GetProperty("results")[0] + .GetProperty("locations")[0] + .GetProperty("physicalLocation") + .GetProperty("region"); + Assert.Equal(CommandExitCodes.Success, sarifExit); + Assert.Equal(string.Empty, sarifStderr); + Assert.Equal(4, preciseRegion.GetProperty("startLine").GetInt32()); + Assert.Equal(expectedColumn, preciseRegion.GetProperty("startColumn").GetInt32()); + Assert.Equal(expectedColumn + targetName.Length, preciseRegion.GetProperty("endColumn").GetInt32()); + + using (var db = new DbContext(DbOpenIntent.WriteIndex, dbPath)) + { + using var command = db.Connection.CreateCommand(); + command.CommandText = """ + UPDATE symbol_references + SET column_number = NULL + WHERE container_name = @caller + AND symbol_name = @target; + """; + command.Parameters.AddWithValue("@caller", callerName); + command.Parameters.AddWithValue("@target", targetName); + Assert.Equal(2, command.ExecuteNonQuery()); + } + + var (legacyJsonExit, legacyJsonStdout, legacyJsonStderr) = CaptureConsole(() => QueryCommandRunner.RunCallees( + [.. baseArgs, "--json"], + _jsonOptions)); + var legacyJsonRow = Assert.Single(ParseJsonLines(legacyJsonStdout)).RootElement; + Assert.Equal(CommandExitCodes.Success, legacyJsonExit); + Assert.Equal(string.Empty, legacyJsonStderr); + Assert.Equal(JsonValueKind.Null, legacyJsonRow.GetProperty("first_column").ValueKind); + + var (legacyQuickfixExit, legacyQuickfixStdout, legacyQuickfixStderr) = CaptureConsole(() => QueryCommandRunner.RunCallees( + [.. baseArgs, "--format", "qf"], + _jsonOptions)); + Assert.Equal(CommandExitCodes.Success, legacyQuickfixExit); + Assert.Equal(string.Empty, legacyQuickfixStderr); + Assert.Contains(":4:0:", legacyQuickfixStdout, StringComparison.Ordinal); + + var (legacyLspExit, legacyLspStdout, legacyLspStderr) = CaptureConsole(() => QueryCommandRunner.RunCallees( + [.. baseArgs, "--format", "lsp"], + _jsonOptions)); + using var legacyLspDocument = ParseJsonOutput(legacyLspStdout); + var legacyRange = legacyLspDocument.RootElement[0].GetProperty("range"); + Assert.Equal(CommandExitCodes.Success, legacyLspExit); + Assert.Equal(string.Empty, legacyLspStderr); + Assert.Equal(0, legacyRange.GetProperty("start").GetProperty("character").GetInt32()); + Assert.Equal(0, legacyRange.GetProperty("end").GetProperty("character").GetInt32()); + + var (legacySarifExit, legacySarifStdout, legacySarifStderr) = CaptureConsole(() => QueryCommandRunner.RunCallees( + [.. baseArgs, "--format", "sarif"], + _jsonOptions)); + using var legacySarifDocument = ParseJsonOutput(legacySarifStdout); + var legacyRegion = legacySarifDocument.RootElement + .GetProperty("runs")[0] + .GetProperty("results")[0] + .GetProperty("locations")[0] + .GetProperty("physicalLocation") + .GetProperty("region"); + Assert.Equal(CommandExitCodes.Success, legacySarifExit); + Assert.Equal(string.Empty, legacySarifStderr); + Assert.Equal(1, legacyRegion.GetProperty("startColumn").GetInt32()); + Assert.False(legacyRegion.TryGetProperty("endColumn", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + [Fact] public void RunReferences_ConfigLanguageSelectsRulesReferences_Issue4740() { From 1de7f455f041be9a7a9de7d81ecf0ac16ee10bba Mon Sep 17 00:00:00 2001 From: Widthdom Date: Mon, 27 Jul 2026 21:09:42 +0900 Subject: [PATCH 2/3] Persist physical reference span widths (#4841) --- DEVELOPER_GUIDE.md | 4 +- USER_GUIDE.md | 4 +- changelog.d/unreleased/4841.fixed.md | 8 +- src/CodeIndex/Cli/QueryCommandRunner.Graph.cs | 4 +- .../Cli/QueryCommandRunner.Locations.cs | 20 ++-- .../Database/DbContext.ReadMigrations.cs | 3 + .../DbContext.SchemaInitialization.cs | 14 ++- .../Database/DbReader.GraphQueries.cs | 46 +++++--- .../Database/DbWriter.ReferenceSql.cs | 5 +- src/CodeIndex/Database/DbWriter.References.cs | 1 + .../Languages/CSharpReferenceExtractor.cs | 3 +- .../Languages/JavaReferenceExtractor.cs | 3 +- .../Languages/KotlinReferenceExtractor.cs | 3 +- ...ferenceExtractor.CoreCallClassification.cs | 27 +++-- .../ReferenceExtractor.ReferenceRecords.cs | 7 +- .../Indexer/References/ReferenceExtractor.cs | 2 + src/CodeIndex/Models/QueryResults.cs | 2 +- src/CodeIndex/Models/ReferenceRecord.cs | 3 + tests/CodeIndex.Tests/DbReaderTests.cs | 34 +++++- .../McpServerToolsCallTests.cs | 29 +++++ .../QueryCommandRunnerReferencesTests.cs | 105 ++++++++++++++++++ 21 files changed, 273 insertions(+), 54 deletions(-) diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 18f14806d..cd1d52a6c 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -1109,7 +1109,7 @@ mixed-language evidence unresolved. `symbol_references.reference_kind` stores raw extractor labels. Default call-graph surfaces (`callers`, `callees`, inspect/analyze caller and callee bundles, and their JSON/MCP fields) expose the canonical public vocabulary `call`, `instantiate`, and `subscribe`. The primary `reference_kind`, `reference_kinds`, and `reference_kind_counts` keys use that same vocabulary. Use `--raw-kinds` on `callers` / `callees`, or `references --kind `, when debugging raw extractor output. -`DbReader.GetCallees` must preserve a representative physical call-site span while aggregating counts. It selects the smallest `(line, column_number)` among rows with a stored column, exposes that 1-based pair as `first_line` / nullable `first_column`, and exposes the called token width as `first_length`; `reference_count` remains the independent aggregate. When every contributing legacy row has `column_number IS NULL`, the reader retains the minimum line and a null column so CLI/MCP location adapters can degrade to line-only output without fabricating precision. +`ReferenceRecord.SpanLength` and `symbol_references.span_length` persist the physical matched-token width rather than deriving it from the resolved symbol name; this matters for constructor-chain tokens such as `base`, `super`, and `this`. `DbReader.GetCallees` preserves that span while aggregating counts: it selects the smallest `(line, column_number)` among rows with a stored column, exposes that 1-based pair as `first_line` / nullable `first_column`, and carries the same row's nullable width as `first_length`; `reference_count` remains the independent aggregate. When every contributing legacy row has `column_number IS NULL`, the reader retains the minimum line and a null column. A migrated row can also retain a column with a null span length. CLI/MCP location adapters degrade either case without fabricating precision. Reference extraction deduplicates only within the same indexed file and language context. When adding extractor paths, include the file id and language hint in shared `seen` keys so same line/column/name edges from polyglot workspaces do not collapse across Java, Rust, C#, SQL, or other language-specific normalization contexts. @@ -4275,7 +4275,7 @@ authoritative な判定と一貫した推論を区別し、複数言語の evide `symbol_references.reference_kind` には extractor が出力した raw label を保存する。既定の call-graph 表示(`callers`、`callees`、inspect/analyze の caller / callee bundle、および JSON/MCP フィールド)は、公開 canonical 語彙 `call`、`instantiate`、`subscribe` を返す。primary `reference_kind`、`reference_kinds`、`reference_kind_counts` の key はすべて同じ語彙を使う。raw extractor 出力を調べる場合は、`callers` / `callees` の `--raw-kinds`、または `references --kind ` を使う。 -`DbReader.GetCallees` は count を集約しながら、代表となる物理 call-site span を保持しなければならない。列が保存された row のうち最小の `(line, column_number)` を選び、その 1-based 座標を `first_line` / nullable な `first_column`、呼び出し token 幅を `first_length` として公開し、`reference_count` は独立した集約値のままにする。寄与する legacy row がすべて `column_number IS NULL` の場合、reader は最小行と null 列を保持し、CLI/MCP の location adapter が精度を捏造せず line-only 出力へ劣化できるようにする。 +`ReferenceRecord.SpanLength` と `symbol_references.span_length` は、解決後の symbol 名から導出せず、物理的に一致した token 幅を永続化する。これは `base`、`super`、`this` のような constructor-chain token で重要になる。`DbReader.GetCallees` は count を集約しながらその span を保持し、列が保存された row のうち最小の `(line, column_number)` を選び、その 1-based 座標を `first_line` / nullable な `first_column`、同じ row の nullable な幅を `first_length` として公開し、`reference_count` は独立した集約値のままにする。寄与する legacy row がすべて `column_number IS NULL` の場合は最小行と null 列を保持する。移行済み row では列があっても span 長が null の場合がある。CLI/MCP の location adapter はどちらの場合も精度を捏造せず劣化させる。 | Raw kind | Logical graph kind | 備考 | |---|---|---| diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 776273536..3c6c489f8 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -1643,7 +1643,7 @@ By default, `callers` and `callees` return only executable call, construction, a `callers` and `callees` rank results by weighted structural importance by default: `instantiate` edges count as `3.0`, direct `call` edges as `1.0`, and event `subscribe` edges as `0.1`. This keeps factory or composition-root constructor use from being buried under noisy event subscriptions. Use `--rank-by count` to restore raw `reference_count` ordering, or `--rank-by kind` to group `instantiate`, `call`, then `subscribe` before count. JSON rows keep the raw `reference_count` and add `reference_kind_counts` plus `reference_weight_score` so consumers can re-rank without losing the source counts; MCP structured rows expose the same data as `referenceKindCounts` and `referenceWeightScore`. -Grouped `callees` rows preserve the earliest precise call site separately from the aggregate `reference_count`. CLI JSON exposes its 1-based `first_line`, nullable `first_column`, and token-sized `first_length`; MCP uses the corresponding `firstLine`, `firstColumn`, and `firstLength` fields. Compact and quickfix output use the same 1-based column, SARIF carries the same token-sized region, and LSP converts it to a zero-based, non-empty token range. If every contributing legacy reference lacks a column, the column remains `null`, quickfix reports column `0`, SARIF omits `endColumn`, and LSP returns a zero-width range at the start of the retained source line instead of inventing a precise token location. +Grouped `callees` rows preserve the earliest precise call site separately from the aggregate `reference_count`. CLI JSON exposes its 1-based `first_line`, nullable `first_column`, and persisted physical-token `first_length`; MCP uses the corresponding `firstLine`, `firstColumn`, and `firstLength` fields. This keeps resolved constructor-chain calls on their actual `base`, `super`, or `this` token. Compact and quickfix output use the same 1-based column, SARIF carries the same token-sized region, and LSP converts it to a zero-based, non-empty token range. If every contributing legacy reference lacks a column, the column remains `null`, quickfix reports column `0`, SARIF omits `endColumn`, and LSP returns a zero-width range at the start of the retained source line. If a migrated row retains its column but has no persisted span length, SARIF still omits `endColumn` and LSP returns a zero-width range at that column instead of inventing a token width. ### Outline a single file @@ -4742,7 +4742,7 @@ cdidx callees AddToGitExclude --exclude-tests `callers` と `callees` は既定で構造的重要度の weighted 順に並びます。`instantiate` は `3.0`、直接 `call` は `1.0`、event `subscribe` は `0.1` として数えるため、factory や composition root の constructor 利用が大量の event subscription に埋もれにくくなります。従来どおり生の `reference_count` で並べたい場合は `--rank-by count`、reference kind を優先して `instantiate`、`call`、`subscribe` の順でまとめたい場合は `--rank-by kind` を使ってください。JSON の各行は生の `reference_count` を維持し、`reference_kind_counts` と `reference_weight_score` も追加で返します。MCP structured row では同じ情報を `referenceKindCounts` と `referenceWeightScore` として返すため、consumer 側で再ランキングできます。 -集約された `callees` 行は、集約値の `reference_count` とは別に、最初の精密な call site を保持します。CLI JSON は 1-based の `first_line`、nullable な `first_column`、token 幅の `first_length` を返し、MCP は対応する `firstLine`、`firstColumn`、`firstLength` を使います。compact / quickfix も同じ 1-based 列を使い、SARIF は同じ token 幅の region、LSP は 0-based の非空 token range に変換します。寄与する legacy reference の全てで列が欠けている場合は列を `null` のまま保ち、quickfix は列 `0`、SARIF は `endColumn` を省略し、LSP は保持した source line の先頭に zero-width range を返すため、精密な token 位置を捏造しません。 +集約された `callees` 行は、集約値の `reference_count` とは別に、最初の精密な call site を保持します。CLI JSON は 1-based の `first_line`、nullable な `first_column`、永続化された物理 token 幅の `first_length` を返し、MCP は対応する `firstLine`、`firstColumn`、`firstLength` を使います。そのため、解決後の constructor-chain call も実際の `base`、`super`、`this` token を指します。compact / quickfix も同じ 1-based 列を使い、SARIF は同じ token 幅の region、LSP は 0-based の非空 token range に変換します。寄与する legacy reference の全てで列が欠けている場合は列を `null` のまま保ち、quickfix は列 `0`、SARIF は `endColumn` を省略し、LSP は保持した source line の先頭に zero-width range を返します。移行済み row に列はあるものの永続化 span 長がない場合も、SARIF は `endColumn` を省略し、LSP はその列に zero-width range を返すため、token 幅を捏造しません。 ### 1ファイルのアウトラインを見る diff --git a/changelog.d/unreleased/4841.fixed.md b/changelog.d/unreleased/4841.fixed.md index 6a44490ed..348be5463 100644 --- a/changelog.d/unreleased/4841.fixed.md +++ b/changelog.d/unreleased/4841.fixed.md @@ -3,6 +3,10 @@ category: fixed issues: - 4841 affected: + - src/CodeIndex/Models/ReferenceRecord.cs + - src/CodeIndex/Indexer/References/ReferenceExtractor.ReferenceRecords.cs + - src/CodeIndex/Database/DbContext.SchemaInitialization.cs + - src/CodeIndex/Database/DbWriter.ReferenceSql.cs - src/CodeIndex/Database/DbReader.GraphQueries.cs - src/CodeIndex/Models/QueryResults.cs - src/CodeIndex/Cli/QueryCommandRunner.Graph.cs @@ -12,8 +16,8 @@ affected: ## English -- **`callees` now preserves the first physical call-site span in every location output (#4841)** — following the graph-location fidelity work in #4417, aggregated callee rows now expose a deterministic 1-based column and token length separately from `reference_count`; quickfix, LSP, SARIF, JSON, compact, and MCP output point at the called token, while legacy rows without columns degrade explicitly to line-only locations. +- **`callees` now preserves the first physical call-site span in every location output (#4841)** — following the graph-location fidelity work in #4417, reference rows persist the matched token width and aggregated callee rows expose its deterministic 1-based column and length separately from `reference_count`; quickfix, LSP, SARIF, JSON, compact, and MCP output point at the called token—including resolved `base` / `super` / `this` constructor chains—while legacy rows without complete spans degrade explicitly. ## 日本語 -- **`callees` がすべての location 出力で最初の物理 call-site span を保持するようになりました (#4841)** — #4417 の graph location fidelity 対応に続き、集約された callee 行は `reference_count` とは別に決定的な 1-based 列と token 長を公開し、quickfix、LSP、SARIF、JSON、compact、MCP は呼び出し token を指します。列を持たない legacy row は明示的に line-only location へ劣化します。 +- **`callees` がすべての location 出力で最初の物理 call-site span を保持するようになりました (#4841)** — #4417 の graph location fidelity 対応に続き、reference row は一致した token 幅を永続化し、集約された callee 行は `reference_count` とは別に決定的な 1-based 列と長さを公開します。quickfix、LSP、SARIF、JSON、compact、MCP は、解決済みの `base` / `super` / `this` constructor chain を含めて実際の呼び出し token を指し、完全な span を持たない legacy row は明示的に劣化します。 diff --git a/src/CodeIndex/Cli/QueryCommandRunner.Graph.cs b/src/CodeIndex/Cli/QueryCommandRunner.Graph.cs index 6381963a7..e23fbf2a6 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.Graph.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.Graph.cs @@ -389,7 +389,9 @@ public static int RunCallees(string[] cmdArgs, JsonSerializerOptions jsonOptions r.Path, r.FirstLine, r.FirstColumn ?? 1, - r.FirstColumn.HasValue ? r.FirstColumn.Value + Math.Max(1, r.FirstLength) : null, + r.FirstColumn.HasValue && r.FirstLength.HasValue + ? r.FirstColumn.Value + Math.Max(1, r.FirstLength.Value) + : null, $"{r.CallerName ?? ""} -> {r.CalleeName}", r.ReferenceKind)), jsonOptions); diff --git a/src/CodeIndex/Cli/QueryCommandRunner.Locations.cs b/src/CodeIndex/Cli/QueryCommandRunner.Locations.cs index 566f09f25..2f9d0ec1d 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.Locations.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.Locations.cs @@ -92,14 +92,18 @@ private static LspLocation ToLspLocation(CallerResult result) => BuildLspLocation(result.Path, result.FirstLine, Math.Max(1, result.FirstColumn), result.FirstLine, Math.Max(1, result.FirstColumn) + Math.Max(1, result.CalleeName.Length)); private static LspLocation ToLspLocation(CalleeResult result) - => result.FirstColumn.HasValue - ? BuildLspLocation( - result.Path, - result.FirstLine, - Math.Max(1, result.FirstColumn.Value), - result.FirstLine, - Math.Max(1, result.FirstColumn.Value) + Math.Max(1, result.FirstLength)) - : BuildLspLocation(result.Path, result.FirstLine, 1, result.FirstLine, 1); + { + var column = Math.Max(1, result.FirstColumn ?? 1); + var endColumn = result.FirstLength.HasValue + ? column + Math.Max(1, result.FirstLength.Value) + : column; + return BuildLspLocation( + result.Path, + result.FirstLine, + column, + result.FirstLine, + endColumn); + } private static void WriteLspLocations(IEnumerable locations, JsonSerializerOptions jsonOptions) { diff --git a/src/CodeIndex/Database/DbContext.ReadMigrations.cs b/src/CodeIndex/Database/DbContext.ReadMigrations.cs index 759d1e418..56a50f831 100644 --- a/src/CodeIndex/Database/DbContext.ReadMigrations.cs +++ b/src/CodeIndex/Database/DbContext.ReadMigrations.cs @@ -157,6 +157,7 @@ file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, reference_kind TEXT, line INTEGER, column_number INTEGER, + span_length INTEGER, context TEXT, reference_line_id INTEGER REFERENCES reference_lines(id) ON DELETE SET NULL, container_kind TEXT, @@ -166,6 +167,8 @@ is_mutual_recursion INTEGER NOT NULL DEFAULT 0 )")); yield return ("EnsureColumn symbol_references.reference_line_id", () => EnsureColumn("symbol_references", "reference_line_id", "INTEGER REFERENCES reference_lines(id) ON DELETE SET NULL")); + yield return ("EnsureColumn symbol_references.span_length", + () => EnsureColumn("symbol_references", "span_length", "INTEGER")); yield return ("EnsureColumn symbol_references.is_self_reference", () => EnsureColumn("symbol_references", "is_self_reference", "INTEGER NOT NULL DEFAULT 0")); yield return ("EnsureColumn symbol_references.is_mutual_recursion", diff --git a/src/CodeIndex/Database/DbContext.SchemaInitialization.cs b/src/CodeIndex/Database/DbContext.SchemaInitialization.cs index a8bc7d198..8323b0bb5 100644 --- a/src/CodeIndex/Database/DbContext.SchemaInitialization.cs +++ b/src/CodeIndex/Database/DbContext.SchemaInitialization.cs @@ -149,6 +149,7 @@ file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, reference_kind TEXT CHECK (reference_kind IN (" + referenceKindCheck + @")), line INTEGER, column_number INTEGER, + span_length INTEGER, context TEXT, reference_line_id INTEGER REFERENCES reference_lines(id) ON DELETE SET NULL, container_kind TEXT CHECK (container_kind IS NULL OR container_kind IN (" + symbolKindCheck + @")), @@ -220,6 +221,7 @@ private void MigrateCoreTableColumns() "symbol_references", "reference_line_id", rebuildsSymbolReferences ? "INTEGER" : "INTEGER REFERENCES reference_lines(id) ON DELETE SET NULL"); + EnsureColumn("symbol_references", "span_length", "INTEGER"); // #86: Unicode-aware folded name columns for `--exact` name matching across all // `--exact` command variants. Populated by the writer via NameFold.Fold; NULL on // legacy rows until a full reindex, in which case the reader falls back to the @@ -486,6 +488,7 @@ file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, reference_kind TEXT CHECK (reference_kind IN ({referenceKindCheck})), line INTEGER, column_number INTEGER, + span_length INTEGER, context TEXT, reference_line_id INTEGER REFERENCES reference_lines(id) ON DELETE SET NULL, container_kind TEXT CHECK (container_kind IS NULL OR container_kind IN ({symbolKindCheck})), @@ -502,7 +505,7 @@ container_kind TEXT CHECK (container_kind IS NULL OR container_kind IN ({symbol resolution_candidate_count INTEGER NOT NULL DEFAULT 0 ) """; - const string symbolReferencesColumns = "id, file_id, symbol_name, reference_kind, line, column_number, context, reference_line_id, container_kind, container_name, symbol_name_folded, container_name_folded, is_self_reference, is_mutual_recursion, source_symbol_id, target_symbol_id, target_symbol_key, target_qualifier, resolution_state, resolution_candidate_count"; + const string symbolReferencesColumns = "id, file_id, symbol_name, reference_kind, line, column_number, span_length, context, reference_line_id, container_kind, container_name, symbol_name_folded, container_name_folded, is_self_reference, is_mutual_recursion, source_symbol_id, target_symbol_id, target_symbol_key, target_qualifier, resolution_state, resolution_candidate_count"; const string oldReferenceLines = "_reference_lines_nullable_file_id"; const string oldSymbolReferences = "_symbol_references_nullable_file_id"; @@ -538,6 +541,7 @@ file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, reference_kind TEXT CHECK (reference_kind IN ({referenceKindCheck})), line INTEGER, column_number INTEGER, + span_length INTEGER, context TEXT, reference_line_id INTEGER REFERENCES reference_lines(id) ON DELETE SET NULL, container_kind TEXT CHECK (container_kind IS NULL OR container_kind IN ({symbolKindCheck})), @@ -554,7 +558,7 @@ container_kind TEXT CHECK (container_kind IS NULL OR container_kind IN ({symbol resolution_candidate_count INTEGER NOT NULL DEFAULT 0 ) """; - const string symbolReferencesColumns = "id, file_id, symbol_name, reference_kind, line, column_number, context, reference_line_id, container_kind, container_name, symbol_name_folded, container_name_folded, is_self_reference, is_mutual_recursion, source_symbol_id, target_symbol_id, target_symbol_key, target_qualifier, resolution_state, resolution_candidate_count"; + const string symbolReferencesColumns = "id, file_id, symbol_name, reference_kind, line, column_number, span_length, context, reference_line_id, container_kind, container_name, symbol_name_folded, container_name_folded, is_self_reference, is_mutual_recursion, source_symbol_id, target_symbol_id, target_symbol_key, target_qualifier, resolution_state, resolution_candidate_count"; const string oldSymbolReferences = "_symbol_references_reference_line_delete"; var quotedOldSymbolReferences = SqliteIdentifier.Quote(oldSymbolReferences); @@ -624,6 +628,7 @@ file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, reference_kind TEXT CHECK (reference_kind IN ({referenceKindCheck})), line INTEGER, column_number INTEGER, + span_length INTEGER, context TEXT, reference_line_id INTEGER REFERENCES reference_lines(id) ON DELETE SET NULL, container_kind TEXT CHECK (container_kind IS NULL OR container_kind IN ({symbolKindCheck})), @@ -640,7 +645,7 @@ container_kind TEXT CHECK (container_kind IS NULL OR container_kind IN ({symbol resolution_candidate_count INTEGER NOT NULL DEFAULT 0 ) """; - const string symbolReferencesColumns = "id, file_id, symbol_name, reference_kind, line, column_number, context, reference_line_id, container_kind, container_name, symbol_name_folded, container_name_folded, is_self_reference, is_mutual_recursion, source_symbol_id, target_symbol_id, target_symbol_key, target_qualifier, resolution_state, resolution_candidate_count"; + const string symbolReferencesColumns = "id, file_id, symbol_name, reference_kind, line, column_number, span_length, context, reference_line_id, container_kind, container_name, symbol_name_folded, container_name_folded, is_self_reference, is_mutual_recursion, source_symbol_id, target_symbol_id, target_symbol_key, target_qualifier, resolution_state, resolution_candidate_count"; const string oldReferenceLines = "_reference_lines_file_line_key"; const string oldSymbolReferences = "_symbol_references_file_line_key"; @@ -734,6 +739,7 @@ file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, reference_kind TEXT CHECK (reference_kind IN ({referenceKindCheck})), line INTEGER, column_number INTEGER, + span_length INTEGER, context TEXT, reference_line_id INTEGER REFERENCES reference_lines(id) ON DELETE SET NULL, container_kind TEXT CHECK (container_kind IS NULL OR container_kind IN ({symbolKindCheck})), @@ -750,7 +756,7 @@ container_kind TEXT CHECK (container_kind IS NULL OR container_kind IN ({symbol resolution_candidate_count INTEGER NOT NULL DEFAULT 0 ) """; - const string symbolReferencesColumns = "id, file_id, symbol_name, reference_kind, line, column_number, context, reference_line_id, container_kind, container_name, symbol_name_folded, container_name_folded, is_self_reference, is_mutual_recursion, source_symbol_id, target_symbol_id, target_symbol_key, target_qualifier, resolution_state, resolution_candidate_count"; + const string symbolReferencesColumns = "id, file_id, symbol_name, reference_kind, line, column_number, span_length, context, reference_line_id, container_kind, container_name, symbol_name_folded, container_name_folded, is_self_reference, is_mutual_recursion, source_symbol_id, target_symbol_id, target_symbol_key, target_qualifier, resolution_state, resolution_candidate_count"; var rebuilt = false; RunWithForeignKeysDisabledForMigration("EnsureKindCheckConstraintsCurrent", () => diff --git a/src/CodeIndex/Database/DbReader.GraphQueries.cs b/src/CodeIndex/Database/DbReader.GraphQueries.cs index 7236b9635..6b26c5d60 100644 --- a/src/CodeIndex/Database/DbReader.GraphQueries.cs +++ b/src/CodeIndex/Database/DbReader.GraphQueries.cs @@ -502,6 +502,9 @@ private List GetCalleesCore(string query, int limit, string? lang, var calleeGroupKindSql = rawKinds ? GetRawReferenceKindSql("r.reference_kind") : GetLogicalReferenceKindSql("r.reference_kind"); + var referenceSpanLengthSql = _referenceColumns.Contains("span_length") + ? "r.span_length" + : "NULL"; var sql = $@" WITH logical_references AS ( SELECT f.path, f.lang, r.container_kind, r.container_name, r.symbol_name, @@ -511,10 +514,8 @@ WITH logical_references AS ( COUNT(*) AS reference_count, {ReferenceWeightedScoreSql("r.reference_kind")} AS weighted_score, r.line, - CASE - WHEN r.column_number IS NULL THEN NULL - ELSE (CAST(r.line AS INTEGER) * 4294967296 + r.column_number) - END AS location_key + r.column_number, + {referenceSpanLengthSql} AS span_length FROM symbol_references r JOIN files f ON r.file_id = f.id WHERE r.container_name IS NOT NULL @@ -568,18 +569,30 @@ WHERE r.container_name IS NOT NULL if (lang != null) sql += " AND f.lang = @lang"; AppendPathFilters(ref sql, pathPatterns, excludePathPatterns, excludeTests); - sql += @" - GROUP BY f.path, f.lang, r.container_kind, r.container_name, r.symbol_name, r.file_id, r.line, r.column_number, r.reference_kind + sql += $@" + GROUP BY f.path, f.lang, r.container_kind, r.container_name, r.symbol_name, r.file_id, r.line, r.column_number, {referenceSpanLengthSql}, r.reference_kind + ), + ranked_call_sites AS ( + SELECT logical_references.*, + ROW_NUMBER() OVER ( + PARTITION BY path, lang, container_kind, container_name, symbol_name, reference_kind + ORDER BY CASE WHEN column_number IS NULL THEN 1 ELSE 0 END, + line, + column_number, + COALESCE(span_length, 0) + ) AS location_rank + FROM logical_references ) SELECT path, lang, container_kind, container_name, symbol_name, reference_kind, - COALESCE((MIN(location_key) / 4294967296), MIN(line)) AS first_line, - (MIN(location_key) % 4294967296) AS first_column, + MAX(CASE WHEN location_rank = 1 THEN line END) AS first_line, + MAX(CASE WHEN location_rank = 1 THEN column_number END) AS first_column, + MAX(CASE WHEN location_rank = 1 THEN span_length END) AS first_length, SUM(r.reference_count) AS reference_count, GROUP_CONCAT(DISTINCT reference_kind) AS reference_kinds, GROUP_CONCAT(r.count_reference_kind || ':' || r.reference_count) AS reference_kind_counts, SUM(r.weighted_score) AS weighted_score - FROM logical_references r + FROM ranked_call_sites r GROUP BY path, lang, container_kind, container_name, symbol_name, reference_kind"; sql += $" ORDER BY CASE WHEN @preferExactCase = 1 AND r.container_name = @rawQuery THEN 0 ELSE 1 END, {GetPathBucketOrderSql("r.path")}, CASE WHEN lower(r.container_name) = lower(@rankingQuery) THEN 0 ELSE 1 END, {BuildReferenceRankOrderSql(rankMode)}, r.path, first_line LIMIT @limit OFFSET @offset"; @@ -626,28 +639,27 @@ FROM logical_references r while (reader.TrackedRead()) { var primaryKind = reader.GetString(5); - var kindAggregate = TruncateReferenceKindAggregate(GetNullableString(reader, 9), out var kindsTruncated); - var countAggregate = TruncateReferenceKindAggregate(GetNullableString(reader, 10), out var countsTruncated); + var kindAggregate = TruncateReferenceKindAggregate(GetNullableString(reader, 10), out var kindsTruncated); + var countAggregate = TruncateReferenceKindAggregate(GetNullableString(reader, 11), out var countsTruncated); var kinds = ParseDistinctReferenceKinds(kindAggregate, primaryKind); - var counts = ParseReferenceKindCounts(countAggregate, primaryKind, reader.GetInt32(8)); - var calleeName = reader.GetString(4); + var counts = ParseReferenceKindCounts(countAggregate, primaryKind, reader.GetInt32(9)); results.Add(new CalleeResult { Path = reader.GetString(0), Lang = GetNullableString(reader, 1), CallerKind = GetNullableString(reader, 2), CallerName = GetNullableString(reader, 3), - CalleeName = calleeName, + CalleeName = reader.GetString(4), ReferenceKind = primaryKind, ReferenceKinds = kinds, HasMixedReferenceKinds = kinds.Count > 1, ReferenceKindCounts = counts, AggregateTruncated = kindsTruncated || countsTruncated, - ReferenceWeightScore = reader.GetDouble(11), + ReferenceWeightScore = reader.GetDouble(12), FirstLine = reader.GetInt32(6), FirstColumn = GetNullableInt32(reader, 7), - FirstLength = calleeName.Length, - ReferenceCount = reader.GetInt32(8), + FirstLength = GetNullableInt32(reader, 8), + ReferenceCount = reader.GetInt32(9), }); } return results; diff --git a/src/CodeIndex/Database/DbWriter.ReferenceSql.cs b/src/CodeIndex/Database/DbWriter.ReferenceSql.cs index 63990f499..902b9b593 100644 --- a/src/CodeIndex/Database/DbWriter.ReferenceSql.cs +++ b/src/CodeIndex/Database/DbWriter.ReferenceSql.cs @@ -9,7 +9,7 @@ private static string BuildReferenceInsertSql(int rowCount) var sql = CreateBatchSqlBuilder(rowCount, estimatedCharsPerRow: 256); sql.Append(@" INSERT INTO symbol_references ( - file_id, symbol_name, reference_kind, line, column_number, + file_id, symbol_name, reference_kind, line, column_number, span_length, context, reference_line_id, container_kind, container_name, symbol_name_folded, container_name_folded, is_self_reference, is_mutual_recursion, target_qualifier @@ -20,7 +20,7 @@ INSERT INTO symbol_references ( { if (row > 0) sql.Append(", "); - AppendBatchParameterTuple(sql, ref parameterIndex, columnCount: 14); + AppendBatchParameterTuple(sql, ref parameterIndex, columnCount: 15); } return sql.ToString(); } @@ -35,6 +35,7 @@ private static void AddReferenceInsertParameters(SqliteCommand cmd, int rowCount AddBatchParameter(cmd, ref parameterIndex, SqliteType.Text); AddBatchParameter(cmd, ref parameterIndex, SqliteType.Integer); AddBatchParameter(cmd, ref parameterIndex, SqliteType.Integer); + AddBatchParameter(cmd, ref parameterIndex, SqliteType.Integer); AddBatchParameter(cmd, ref parameterIndex, SqliteType.Text); AddBatchParameter(cmd, ref parameterIndex, SqliteType.Integer); AddBatchParameter(cmd, ref parameterIndex, SqliteType.Text); diff --git a/src/CodeIndex/Database/DbWriter.References.cs b/src/CodeIndex/Database/DbWriter.References.cs index c3726fc1e..3ba9a5608 100644 --- a/src/CodeIndex/Database/DbWriter.References.cs +++ b/src/CodeIndex/Database/DbWriter.References.cs @@ -1169,6 +1169,7 @@ private void InsertReferenceBatch( cmd.Parameters[parameterIndex++].Value = reference.ReferenceKind; cmd.Parameters[parameterIndex++].Value = reference.Line; cmd.Parameters[parameterIndex++].Value = reference.Column; + cmd.Parameters[parameterIndex++].Value = Math.Max(1, reference.SpanLength > 0 ? reference.SpanLength : reference.SymbolName.Length); cmd.Parameters[parameterIndex++].Value = DBNull.Value; cmd.Parameters[parameterIndex++].Value = previousReferenceLineId; cmd.Parameters[parameterIndex++].Value = (object?)reference.ContainerKind ?? DBNull.Value; diff --git a/src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.cs b/src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.cs index 4ff4930fd..987987637 100644 --- a/src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.cs @@ -171,7 +171,8 @@ public static void EmitCtorChainReferences( "call", context, lineNumber, - chainContainer); + chainContainer, + sourceLength: kindToken.Length); } while (chainMatches.MoveNext()); } diff --git a/src/CodeIndex/Indexer/References/Languages/JavaReferenceExtractor.cs b/src/CodeIndex/Indexer/References/Languages/JavaReferenceExtractor.cs index 3263bfc97..712ff2652 100644 --- a/src/CodeIndex/Indexer/References/Languages/JavaReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/References/Languages/JavaReferenceExtractor.cs @@ -125,7 +125,8 @@ public static void EmitCtorChainReferences( "call", context, lineNumber, - ctorContainer); + ctorContainer, + sourceLength: kindToken.Length); } public static (SymbolRecord Synthetic, int NameIndex, int OpenBraceIndex, int CloseBraceIndex)? diff --git a/src/CodeIndex/Indexer/References/Languages/KotlinReferenceExtractor.cs b/src/CodeIndex/Indexer/References/Languages/KotlinReferenceExtractor.cs index b4bfda7a7..639fadf3e 100644 --- a/src/CodeIndex/Indexer/References/Languages/KotlinReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/References/Languages/KotlinReferenceExtractor.cs @@ -441,7 +441,8 @@ public static void EmitCtorDelegationReferences( "call", context, lineNumber, - ctorContainer); + ctorContainer, + sourceLength: kindToken.Length); } while (matches.MoveNext()); } diff --git a/src/CodeIndex/Indexer/References/ReferenceExtractor.CoreCallClassification.cs b/src/CodeIndex/Indexer/References/ReferenceExtractor.CoreCallClassification.cs index 55c320d7c..ad7efaee3 100644 --- a/src/CodeIndex/Indexer/References/ReferenceExtractor.CoreCallClassification.cs +++ b/src/CodeIndex/Indexer/References/ReferenceExtractor.CoreCallClassification.cs @@ -147,7 +147,8 @@ private static bool TryAddCoreCallLikeReference( line.Context, line.LineNumber, callContainer, - line.Language); + line.Language, + sourceLength: name.Length); } return false; @@ -169,7 +170,8 @@ private static bool TryAddCoreCallLikeReference( line.LineNumber, callContainer, line.Language, - targetQualifier); + targetQualifier, + name.Length); return true; } if (line.Language == "rust" @@ -189,7 +191,8 @@ private static bool TryAddCoreCallLikeReference( line.Context, line.LineNumber, callContainer, - line.Language); + line.Language, + sourceLength: name.Length); return true; } if (line.Language == "python" @@ -209,7 +212,8 @@ private static bool TryAddCoreCallLikeReference( line.Context, line.LineNumber, callContainer, - line.Language); + line.Language, + sourceLength: name.Length); return true; } if (line.Language == "csharp" @@ -248,7 +252,8 @@ private static bool TryAddCoreCallLikeReference( line.Context, line.LineNumber, callContainer, - line.Language); + line.Language, + sourceLength: name.Length); if (line.Language == "csharp" && metadataKind == "attribute" && CSharpReferenceExtractor.TryGetCallerInfoAttributeTypeName( @@ -266,7 +271,8 @@ private static bool TryAddCoreCallLikeReference( line.Context, line.LineNumber, callContainer, - line.Language); + line.Language, + sourceLength: name.Length); } return true; } @@ -285,7 +291,8 @@ private static bool TryAddCoreCallLikeReference( "instantiate", line.Context, line.LineNumber, - callContainer); + callContainer, + sourceLength: name.Length); return true; } @@ -302,7 +309,8 @@ private static bool TryAddCoreCallLikeReference( "consumes_hook", line.Context, line.LineNumber, - callContainer); + callContainer, + sourceLength: name.Length); return true; } @@ -319,7 +327,8 @@ private static bool TryAddCoreCallLikeReference( ScientificNativeReferenceExtractor.Supports(line.Language) ? line.Language : null, - targetQualifier: targetQualifier); + targetQualifier: targetQualifier, + sourceLength: name.Length); return true; } diff --git a/src/CodeIndex/Indexer/References/ReferenceExtractor.ReferenceRecords.cs b/src/CodeIndex/Indexer/References/ReferenceExtractor.ReferenceRecords.cs index 345d38ea3..77448277b 100644 --- a/src/CodeIndex/Indexer/References/ReferenceExtractor.ReferenceRecords.cs +++ b/src/CodeIndex/Indexer/References/ReferenceExtractor.ReferenceRecords.cs @@ -233,7 +233,8 @@ internal static void AddReference( lineNumber, container, language, - targetQualifier); + targetQualifier, + match.Groups["name"].Length); } internal static void AddReference( @@ -247,7 +248,8 @@ internal static void AddReference( int lineNumber, SymbolRecord? container, string? language = null, - string? targetQualifier = null) + string? targetQualifier = null, + int? sourceLength = null) { var column = nameIndex + 1; var dedupeKey = CreateReferenceDedupeKey(fileId, language, lineNumber, column, referenceKind, name, container); @@ -268,6 +270,7 @@ internal static void AddReference( ReferenceKind = referenceKind, Line = lineNumber, Column = column, + SpanLength = Math.Max(1, sourceLength ?? name.Length), Context = context, ContainerKind = container?.Kind, ContainerName = container?.Name, diff --git a/src/CodeIndex/Indexer/References/ReferenceExtractor.cs b/src/CodeIndex/Indexer/References/ReferenceExtractor.cs index 267c8e4f6..3ff269ec4 100644 --- a/src/CodeIndex/Indexer/References/ReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/References/ReferenceExtractor.cs @@ -342,6 +342,8 @@ internal static bool TryAddReference(List references, Reference if (ReferenceLimitReached(references)) return false; + if (reference.SpanLength <= 0) + reference.SpanLength = Math.Max(1, reference.SymbolName.Length); references.Add(reference); return true; } diff --git a/src/CodeIndex/Models/QueryResults.cs b/src/CodeIndex/Models/QueryResults.cs index a04e4d1dc..b8094c0d9 100644 --- a/src/CodeIndex/Models/QueryResults.cs +++ b/src/CodeIndex/Models/QueryResults.cs @@ -739,7 +739,7 @@ public class CalleeResult // 代表 call-site の span を明示し、location consumer が表示文字列から列を // 復元しないようにする。legacy row の列は null のまま保持できる。 public int? FirstColumn { get; set; } - public int FirstLength { get; set; } + public int? FirstLength { get; set; } public int ReferenceCount { get; set; } [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? BodyContent { get; set; } diff --git a/src/CodeIndex/Models/ReferenceRecord.cs b/src/CodeIndex/Models/ReferenceRecord.cs index f89e34dae..413acd389 100644 --- a/src/CodeIndex/Models/ReferenceRecord.cs +++ b/src/CodeIndex/Models/ReferenceRecord.cs @@ -27,6 +27,9 @@ public class ReferenceRecord /// Column number (1-based) / 列番号(1始まり) public int Column { get; set; } + /// Physical source-token width in UTF-16 code units / 物理 source token の UTF-16 code unit 幅 + public int SpanLength { get; set; } + /// Trimmed source line for quick inspection / すばやい確認用のtrim済みソース行 public string Context { get; set; } = string.Empty; diff --git a/tests/CodeIndex.Tests/DbReaderTests.cs b/tests/CodeIndex.Tests/DbReaderTests.cs index 5adda6786..34ede1d18 100644 --- a/tests/CodeIndex.Tests/DbReaderTests.cs +++ b/tests/CodeIndex.Tests/DbReaderTests.cs @@ -2087,6 +2087,11 @@ public void GetCallees_PreservesFirstPreciseCallSiteAndLegacyLineOnlyFallback_Is " void Issue4841Target() { }", callLine, "}", + "class Issue4841VeryLongParent { }", + "class Issue4841Child : Issue4841VeryLongParent", + "{", + " Issue4841Child() : base() { }", + "}", ""); var expectedColumn = callLine.IndexOf(targetName, StringComparison.Ordinal) + 1; InsertIndexedFile("src/Issue4841Probe.cs", "csharp", source); @@ -2098,6 +2103,33 @@ public void GetCallees_PreservesFirstPreciseCallSiteAndLegacyLineOnlyFallback_Is Assert.Equal(targetName.Length, precise.FirstLength); Assert.Equal(2, precise.ReferenceCount); + var constructorChain = Assert.Single( + _reader.GetCallees("Issue4841Child", lang: "csharp", exact: true), + row => row.CalleeName == "Issue4841VeryLongParent"); + Assert.Equal("Issue4841VeryLongParent", constructorChain.CalleeName); + Assert.Equal(9, constructorChain.FirstLine); + Assert.Equal(24, constructorChain.FirstColumn); + Assert.Equal("base".Length, constructorChain.FirstLength); + + using (var command = _db.Connection.CreateCommand()) + { + command.CommandText = """ + UPDATE symbol_references + SET span_length = NULL + WHERE container_name = @caller + AND symbol_name = @target; + """; + command.Parameters.AddWithValue("@caller", callerName); + command.Parameters.AddWithValue("@target", targetName); + Assert.Equal(2, command.ExecuteNonQuery()); + } + + var spanless = Assert.Single(_reader.GetCallees(callerName, lang: "csharp", exact: true)); + + Assert.Equal(expectedColumn, spanless.FirstColumn); + Assert.Null(spanless.FirstLength); + Assert.Equal(2, spanless.ReferenceCount); + using (var command = _db.Connection.CreateCommand()) { command.CommandText = """ @@ -2115,7 +2147,7 @@ UPDATE symbol_references Assert.Equal(4, legacy.FirstLine); Assert.Null(legacy.FirstColumn); - Assert.Equal(targetName.Length, legacy.FirstLength); + Assert.Null(legacy.FirstLength); Assert.Equal(2, legacy.ReferenceCount); } diff --git a/tests/CodeIndex.Tests/McpServerToolsCallTests.cs b/tests/CodeIndex.Tests/McpServerToolsCallTests.cs index 3722d6039..1a859b2a6 100644 --- a/tests/CodeIndex.Tests/McpServerToolsCallTests.cs +++ b/tests/CodeIndex.Tests/McpServerToolsCallTests.cs @@ -1787,6 +1787,11 @@ public void ToolsCall_Callees_PreservesFirstCallSiteSpanInFullAndCompactRows_Iss " void Issue4841McpTarget() { }", callLine, "}", + "class Issue4841McpVeryLongParent { }", + "class Issue4841McpChild : Issue4841McpVeryLongParent", + "{", + " Issue4841McpChild() : base() { }", + "}", ""); var expectedColumn = callLine.IndexOf(targetName, StringComparison.Ordinal) + 1; InsertIndexedFile("src/Issue4841McpProbe.cs", "csharp", source); @@ -1839,6 +1844,30 @@ public void ToolsCall_Callees_PreservesFirstCallSiteSpanInFullAndCompactRows_Iss Assert.Equal(4, compactRow["line"]!.GetValue()); Assert.Equal(expectedColumn, compactRow["column"]!.GetValue()); + + var constructorChainRequest = new JsonObject + { + ["jsonrpc"] = "2.0", + ["id"] = 4843, + ["method"] = "tools/call", + ["params"] = new JsonObject + { + ["name"] = "callees", + ["arguments"] = new JsonObject + { + ["query"] = "Issue4841McpChild", + ["lang"] = "csharp", + ["exact"] = true, + }, + }, + }; + var constructorChainResponse = _server.HandleMessage(constructorChainRequest)!; + var constructorChainRow = constructorChainResponse["result"]!["structuredContent"]!["results"]! + .AsArray() + .Single(row => row!["calleeName"]!.GetValue() == "Issue4841McpVeryLongParent")!; + + Assert.Equal("Issue4841McpVeryLongParent", constructorChainRow["calleeName"]!.GetValue()); + Assert.Equal("base".Length, constructorChainRow["firstLength"]!.GetValue()); } [Fact] diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerReferencesTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerReferencesTests.cs index 8bcb450e7..a83631cf6 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerReferencesTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerReferencesTests.cs @@ -21,6 +21,11 @@ public void RunCallees_LocationFormatsPreserveFirstCallSiteSpanAndLegacyFallback " void Issue4841CliTarget() { }", callLine, "}", + "class Issue4841CliVeryLongParent { }", + "class Issue4841CliChild : Issue4841CliVeryLongParent", + "{", + " Issue4841CliChild() : base() { }", + "}", ""); var expectedColumn = callLine.IndexOf(targetName, StringComparison.Ordinal) + 1; var projectRoot = TestProjectHelper.CreateTempProject("cdidx_callees_locations_issue4841"); @@ -87,6 +92,105 @@ public void RunCallees_LocationFormatsPreserveFirstCallSiteSpanAndLegacyFallback Assert.Equal(expectedColumn, preciseRegion.GetProperty("startColumn").GetInt32()); Assert.Equal(expectedColumn + targetName.Length, preciseRegion.GetProperty("endColumn").GetInt32()); + string[] constructorChainArgs = ["Issue4841CliChild", "--db", dbPath, "--lang", "csharp", "--exact"]; + var (chainJsonExit, chainJsonStdout, chainJsonStderr) = CaptureConsole(() => QueryCommandRunner.RunCallees( + [.. constructorChainArgs, "--json"], + _jsonOptions)); + var chainJsonRow = ParseJsonLines(chainJsonStdout) + .Select(document => document.RootElement) + .Single(row => row.GetProperty("callee_name").GetString() == "Issue4841CliVeryLongParent"); + Assert.Equal(CommandExitCodes.Success, chainJsonExit); + Assert.Equal(string.Empty, chainJsonStderr); + Assert.Equal("Issue4841CliVeryLongParent", chainJsonRow.GetProperty("callee_name").GetString()); + Assert.Equal("base".Length, chainJsonRow.GetProperty("first_length").GetInt32()); + + var (chainLspExit, chainLspStdout, chainLspStderr) = CaptureConsole(() => QueryCommandRunner.RunCallees( + [.. constructorChainArgs, "--format", "lsp"], + _jsonOptions)); + using var chainLspDocument = ParseJsonOutput(chainLspStdout); + var chainRange = chainLspDocument.RootElement + .EnumerateArray() + .Select(location => location.GetProperty("range")) + .Single(range => + range.GetProperty("end").GetProperty("character").GetInt32() + - range.GetProperty("start").GetProperty("character").GetInt32() + == "base".Length); + Assert.Equal(CommandExitCodes.Success, chainLspExit); + Assert.Equal(string.Empty, chainLspStderr); + Assert.Equal( + chainRange.GetProperty("start").GetProperty("character").GetInt32() + "base".Length, + chainRange.GetProperty("end").GetProperty("character").GetInt32()); + + var (chainSarifExit, chainSarifStdout, chainSarifStderr) = CaptureConsole(() => QueryCommandRunner.RunCallees( + [.. constructorChainArgs, "--format", "sarif"], + _jsonOptions)); + using var chainSarifDocument = ParseJsonOutput(chainSarifStdout); + var chainRegion = chainSarifDocument.RootElement + .GetProperty("runs")[0] + .GetProperty("results") + .EnumerateArray() + .Select(result => result + .GetProperty("locations")[0] + .GetProperty("physicalLocation") + .GetProperty("region")) + .Single(region => + region.GetProperty("endColumn").GetInt32() + - region.GetProperty("startColumn").GetInt32() + == "base".Length); + Assert.Equal(CommandExitCodes.Success, chainSarifExit); + Assert.Equal(string.Empty, chainSarifStderr); + Assert.Equal( + chainRegion.GetProperty("startColumn").GetInt32() + "base".Length, + chainRegion.GetProperty("endColumn").GetInt32()); + + using (var db = new DbContext(DbOpenIntent.WriteIndex, dbPath)) + { + using var command = db.Connection.CreateCommand(); + command.CommandText = """ + UPDATE symbol_references + SET span_length = NULL + WHERE container_name = @caller + AND symbol_name = @target; + """; + command.Parameters.AddWithValue("@caller", callerName); + command.Parameters.AddWithValue("@target", targetName); + Assert.Equal(2, command.ExecuteNonQuery()); + } + + var (spanlessJsonExit, spanlessJsonStdout, spanlessJsonStderr) = CaptureConsole(() => QueryCommandRunner.RunCallees( + [.. baseArgs, "--json"], + _jsonOptions)); + var spanlessJsonRow = Assert.Single(ParseJsonLines(spanlessJsonStdout)).RootElement; + Assert.Equal(CommandExitCodes.Success, spanlessJsonExit); + Assert.Equal(string.Empty, spanlessJsonStderr); + Assert.Equal(expectedColumn, spanlessJsonRow.GetProperty("first_column").GetInt32()); + Assert.Equal(JsonValueKind.Null, spanlessJsonRow.GetProperty("first_length").ValueKind); + + var (spanlessLspExit, spanlessLspStdout, spanlessLspStderr) = CaptureConsole(() => QueryCommandRunner.RunCallees( + [.. baseArgs, "--format", "lsp"], + _jsonOptions)); + using var spanlessLspDocument = ParseJsonOutput(spanlessLspStdout); + var spanlessRange = spanlessLspDocument.RootElement[0].GetProperty("range"); + Assert.Equal(CommandExitCodes.Success, spanlessLspExit); + Assert.Equal(string.Empty, spanlessLspStderr); + Assert.Equal(expectedColumn - 1, spanlessRange.GetProperty("start").GetProperty("character").GetInt32()); + Assert.Equal(expectedColumn - 1, spanlessRange.GetProperty("end").GetProperty("character").GetInt32()); + + var (spanlessSarifExit, spanlessSarifStdout, spanlessSarifStderr) = CaptureConsole(() => QueryCommandRunner.RunCallees( + [.. baseArgs, "--format", "sarif"], + _jsonOptions)); + using var spanlessSarifDocument = ParseJsonOutput(spanlessSarifStdout); + var spanlessRegion = spanlessSarifDocument.RootElement + .GetProperty("runs")[0] + .GetProperty("results")[0] + .GetProperty("locations")[0] + .GetProperty("physicalLocation") + .GetProperty("region"); + Assert.Equal(CommandExitCodes.Success, spanlessSarifExit); + Assert.Equal(string.Empty, spanlessSarifStderr); + Assert.Equal(expectedColumn, spanlessRegion.GetProperty("startColumn").GetInt32()); + Assert.False(spanlessRegion.TryGetProperty("endColumn", out _)); + using (var db = new DbContext(DbOpenIntent.WriteIndex, dbPath)) { using var command = db.Connection.CreateCommand(); @@ -108,6 +212,7 @@ UPDATE symbol_references Assert.Equal(CommandExitCodes.Success, legacyJsonExit); Assert.Equal(string.Empty, legacyJsonStderr); Assert.Equal(JsonValueKind.Null, legacyJsonRow.GetProperty("first_column").ValueKind); + Assert.Equal(JsonValueKind.Null, legacyJsonRow.GetProperty("first_length").ValueKind); var (legacyQuickfixExit, legacyQuickfixStdout, legacyQuickfixStderr) = CaptureConsole(() => QueryCommandRunner.RunCallees( [.. baseArgs, "--format", "qf"], From 6b9fbc141eb70ed1f1f906fb9cc1d68ee6b4900f Mon Sep 17 00:00:00 2001 From: Widthdom Date: Mon, 27 Jul 2026 23:33:37 +0900 Subject: [PATCH 3/3] Address callee span review findings (#4841) --- changelog.d/unreleased/4841.fixed.md | 5 ++++ src/CodeIndex/Database/DbWriter.References.cs | 4 +++- .../PostExtractionHookMutationMaterializer.cs | 1 + .../ReferenceExtractor.CoreCallReferences.cs | 6 +++-- .../ReferenceExtractor.CoreLookups.cs | 1 + .../ReferenceExtractor.PrimaryConstructors.cs | 16 +++++++++++-- .../Indexer/References/ReferenceExtractor.cs | 2 -- .../Support/JvmMethodReferenceExtractor.cs | 7 ++++-- tests/CodeIndex.Tests/DbReaderTests.cs | 13 ++++++++++- .../PostExtractionHookContractTests.cs | 2 ++ .../ReferenceExtractorCSharpTests.cs | 12 +++++++++- .../ReferenceExtractorJvmTests.cs | 23 ++++++++++++++++--- 12 files changed, 78 insertions(+), 14 deletions(-) diff --git a/changelog.d/unreleased/4841.fixed.md b/changelog.d/unreleased/4841.fixed.md index 348be5463..84ad3aecb 100644 --- a/changelog.d/unreleased/4841.fixed.md +++ b/changelog.d/unreleased/4841.fixed.md @@ -5,8 +5,13 @@ issues: affected: - src/CodeIndex/Models/ReferenceRecord.cs - src/CodeIndex/Indexer/References/ReferenceExtractor.ReferenceRecords.cs + - src/CodeIndex/Indexer/References/ReferenceExtractor.PrimaryConstructors.cs + - src/CodeIndex/Indexer/References/ReferenceExtractor.CoreCallReferences.cs + - src/CodeIndex/Indexer/References/Support/JvmMethodReferenceExtractor.cs + - src/CodeIndex/Indexer/Hooks/PostExtractionHookMutationMaterializer.cs - src/CodeIndex/Database/DbContext.SchemaInitialization.cs - src/CodeIndex/Database/DbWriter.ReferenceSql.cs + - src/CodeIndex/Database/DbWriter.References.cs - src/CodeIndex/Database/DbReader.GraphQueries.cs - src/CodeIndex/Models/QueryResults.cs - src/CodeIndex/Cli/QueryCommandRunner.Graph.cs diff --git a/src/CodeIndex/Database/DbWriter.References.cs b/src/CodeIndex/Database/DbWriter.References.cs index 3ba9a5608..f039b9412 100644 --- a/src/CodeIndex/Database/DbWriter.References.cs +++ b/src/CodeIndex/Database/DbWriter.References.cs @@ -1169,7 +1169,9 @@ private void InsertReferenceBatch( cmd.Parameters[parameterIndex++].Value = reference.ReferenceKind; cmd.Parameters[parameterIndex++].Value = reference.Line; cmd.Parameters[parameterIndex++].Value = reference.Column; - cmd.Parameters[parameterIndex++].Value = Math.Max(1, reference.SpanLength > 0 ? reference.SpanLength : reference.SymbolName.Length); + cmd.Parameters[parameterIndex++].Value = + (object?)(reference.SpanLength > 0 ? reference.SpanLength : null) + ?? DBNull.Value; cmd.Parameters[parameterIndex++].Value = DBNull.Value; cmd.Parameters[parameterIndex++].Value = previousReferenceLineId; cmd.Parameters[parameterIndex++].Value = (object?)reference.ContainerKind ?? DBNull.Value; diff --git a/src/CodeIndex/Indexer/Hooks/PostExtractionHookMutationMaterializer.cs b/src/CodeIndex/Indexer/Hooks/PostExtractionHookMutationMaterializer.cs index 273edd63c..940fc6ade 100644 --- a/src/CodeIndex/Indexer/Hooks/PostExtractionHookMutationMaterializer.cs +++ b/src/CodeIndex/Indexer/Hooks/PostExtractionHookMutationMaterializer.cs @@ -117,6 +117,7 @@ private static ReferenceRecord CloneReference(ReferenceRecord reference) ReferenceKind = reference.ReferenceKind, Line = reference.Line, Column = reference.Column, + SpanLength = reference.SpanLength, Context = reference.Context, ContainerKind = reference.ContainerKind, ContainerName = reference.ContainerName, diff --git a/src/CodeIndex/Indexer/References/ReferenceExtractor.CoreCallReferences.cs b/src/CodeIndex/Indexer/References/ReferenceExtractor.CoreCallReferences.cs index 6cbc7795d..574253daf 100644 --- a/src/CodeIndex/Indexer/References/ReferenceExtractor.CoreCallReferences.cs +++ b/src/CodeIndex/Indexer/References/ReferenceExtractor.CoreCallReferences.cs @@ -54,7 +54,8 @@ private static Action CreatePowerShellParameterReferenceEmitter( line.Context, line.LineNumber, callContainer, - line.Language); + line.Language, + sourceLength: name.Length); }; private static Action CreateGradleDslReferenceEmitter( @@ -73,7 +74,8 @@ private static Action CreateGradleDslReferenceEmitter( line.Context, line.LineNumber, callContainer, - line.Language); + line.Language, + sourceLength: name.Length); }; private static void EmitCoreCallReferences(CoreCallReferenceContext call) diff --git a/src/CodeIndex/Indexer/References/ReferenceExtractor.CoreLookups.cs b/src/CodeIndex/Indexer/References/ReferenceExtractor.CoreLookups.cs index b882a08fb..1c4c86117 100644 --- a/src/CodeIndex/Indexer/References/ReferenceExtractor.CoreLookups.cs +++ b/src/CodeIndex/Indexer/References/ReferenceExtractor.CoreLookups.cs @@ -448,6 +448,7 @@ internal void EmitCSharpBclRegexWithoutTimeoutReferences(List r ReferenceKind = "bcl_regex_without_timeout", Line = reference.Line, Column = reference.Column, + SpanLength = reference.SpanLength, Context = reference.Context, ContainerKind = reference.ContainerKind, ContainerName = reference.ContainerName, diff --git a/src/CodeIndex/Indexer/References/ReferenceExtractor.PrimaryConstructors.cs b/src/CodeIndex/Indexer/References/ReferenceExtractor.PrimaryConstructors.cs index c297c77d8..0cf75a6a1 100644 --- a/src/CodeIndex/Indexer/References/ReferenceExtractor.PrimaryConstructors.cs +++ b/src/CodeIndex/Indexer/References/ReferenceExtractor.PrimaryConstructors.cs @@ -42,7 +42,8 @@ private static void AddChainReference( string referenceKind, string context, int lineNumber, - SymbolRecord? container) + SymbolRecord? container, + int? sourceLength = null) { var dedupeKey = CreateReferenceDedupeKey(fileId, null, lineNumber, column, referenceKind, name, container); if (!seen.Add(dedupeKey)) @@ -55,6 +56,7 @@ private static void AddChainReference( ReferenceKind = referenceKind, Line = lineNumber, Column = column, + SpanLength = Math.Max(1, sourceLength ?? name.Length), Context = context, ContainerKind = container?.Kind, ContainerName = container?.Name, @@ -96,7 +98,17 @@ private static void EmitMethodGroupReferences( continue; var container = resolveContainerForColumn(nameGroup.Index); - AddChainReference(references, seen, fileId, name, nameGroup.Index, "call", context, lineNumber, container); + AddChainReference( + references, + seen, + fileId, + name, + nameGroup.Index + 1, + "call", + context, + lineNumber, + container, + rawName.Length); } } diff --git a/src/CodeIndex/Indexer/References/ReferenceExtractor.cs b/src/CodeIndex/Indexer/References/ReferenceExtractor.cs index 3ff269ec4..267c8e4f6 100644 --- a/src/CodeIndex/Indexer/References/ReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/References/ReferenceExtractor.cs @@ -342,8 +342,6 @@ internal static bool TryAddReference(List references, Reference if (ReferenceLimitReached(references)) return false; - if (reference.SpanLength <= 0) - reference.SpanLength = Math.Max(1, reference.SymbolName.Length); references.Add(reference); return true; } diff --git a/src/CodeIndex/Indexer/References/Support/JvmMethodReferenceExtractor.cs b/src/CodeIndex/Indexer/References/Support/JvmMethodReferenceExtractor.cs index 60e2d5ae1..f183e7b11 100644 --- a/src/CodeIndex/Indexer/References/Support/JvmMethodReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/References/Support/JvmMethodReferenceExtractor.cs @@ -225,8 +225,10 @@ private static void AddChainReference( int lineNumber, SymbolRecord? container) { + var sourceLength = name.Length; name = NormalizeMethodReferenceName(language, name); - var dedupeKey = ReferenceExtractor.CreateReferenceDedupeKey(fileId, language, lineNumber, column, "call", name, container); + var oneBasedColumn = column + 1; + var dedupeKey = ReferenceExtractor.CreateReferenceDedupeKey(fileId, language, lineNumber, oneBasedColumn, "call", name, container); if (!seen.Add(dedupeKey)) return; @@ -236,7 +238,8 @@ private static void AddChainReference( SymbolName = name, ReferenceKind = "call", Line = lineNumber, - Column = column, + Column = oneBasedColumn, + SpanLength = sourceLength, Context = context, ContainerKind = container?.Kind, ContainerName = container?.Name, diff --git a/tests/CodeIndex.Tests/DbReaderTests.cs b/tests/CodeIndex.Tests/DbReaderTests.cs index 34ede1d18..65871f2ee 100644 --- a/tests/CodeIndex.Tests/DbReaderTests.cs +++ b/tests/CodeIndex.Tests/DbReaderTests.cs @@ -2081,11 +2081,13 @@ public void GetCallees_PreservesFirstPreciseCallSiteAndLegacyLineOnlyFallback_Is const string targetName = "Issue4841Target"; const string callerName = "Issue4841Caller"; const string callLine = " void Issue4841Caller() { var text = \"日本語\";\tIssue4841Target(); Issue4841Target(); }"; + const string methodGroupLine = " void Issue4841MethodGroupCaller() { System.Action callback = @Issue4841Target; }"; var source = string.Join('\n', "class Issue4841Probe", "{", " void Issue4841Target() { }", callLine, + methodGroupLine, "}", "class Issue4841VeryLongParent { }", "class Issue4841Child : Issue4841VeryLongParent", @@ -2094,6 +2096,7 @@ public void GetCallees_PreservesFirstPreciseCallSiteAndLegacyLineOnlyFallback_Is "}", ""); var expectedColumn = callLine.IndexOf(targetName, StringComparison.Ordinal) + 1; + var expectedMethodGroupColumn = methodGroupLine.IndexOf($"@{targetName}", StringComparison.Ordinal) + 1; InsertIndexedFile("src/Issue4841Probe.cs", "csharp", source); var precise = Assert.Single(_reader.GetCallees(callerName, lang: "csharp", exact: true)); @@ -2103,11 +2106,19 @@ public void GetCallees_PreservesFirstPreciseCallSiteAndLegacyLineOnlyFallback_Is Assert.Equal(targetName.Length, precise.FirstLength); Assert.Equal(2, precise.ReferenceCount); + var methodGroup = Assert.Single( + _reader.GetCallees("Issue4841MethodGroupCaller", lang: "csharp", exact: true), + row => row.CalleeName == targetName); + Assert.Equal(5, methodGroup.FirstLine); + Assert.Equal(expectedMethodGroupColumn, methodGroup.FirstColumn); + Assert.Equal(targetName.Length + 1, methodGroup.FirstLength); + Assert.Equal(1, methodGroup.ReferenceCount); + var constructorChain = Assert.Single( _reader.GetCallees("Issue4841Child", lang: "csharp", exact: true), row => row.CalleeName == "Issue4841VeryLongParent"); Assert.Equal("Issue4841VeryLongParent", constructorChain.CalleeName); - Assert.Equal(9, constructorChain.FirstLine); + Assert.Equal(10, constructorChain.FirstLine); Assert.Equal(24, constructorChain.FirstColumn); Assert.Equal("base".Length, constructorChain.FirstLength); diff --git a/tests/CodeIndex.Tests/PostExtractionHookContractTests.cs b/tests/CodeIndex.Tests/PostExtractionHookContractTests.cs index 314626ec3..70b2edc87 100644 --- a/tests/CodeIndex.Tests/PostExtractionHookContractTests.cs +++ b/tests/CodeIndex.Tests/PostExtractionHookContractTests.cs @@ -151,6 +151,7 @@ public void MutationMaterializer_RecomputesNimIdentityAfterHookMutation_Issue473 ReferenceKind = "call", Line = 2, Column = 1, + SpanLength = 4, ContainerName = "Run_Graph", IdentityContainerNameFolded = "stale", TargetQualifier = "pkg", @@ -166,6 +167,7 @@ public void MutationMaterializer_RecomputesNimIdentityAfterHookMutation_Issue473 var clonedReference = Assert.Single(clonedReferences); Assert.Equal("pkg", clonedReference.TargetQualifier); Assert.True(clonedReference.SuppressInferredTargetQualifier); + Assert.Equal(4, clonedReference.SpanLength); PostExtractionHookMutationMaterializer.RefreshLanguageIdentity("nim", symbols); PostExtractionHookMutationMaterializer.RefreshLanguageIdentity("nim", clonedReferences); diff --git a/tests/CodeIndex.Tests/ReferenceExtractorCSharpTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorCSharpTests.cs index 3f6cbbe4b..2be72e61c 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorCSharpTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorCSharpTests.cs @@ -793,7 +793,7 @@ public void Fire() { } public void Wire(IEnumerable xs) { - Action a = Fire; + Action a = @Fire; Func f = Compute; var g = this.Compute; var h = new Func(Compute); @@ -812,6 +812,16 @@ public void Wire(IEnumerable xs) Assert.Equal("function", reference.ContainerKind); Assert.Equal("Wire", reference.ContainerName); }); + var escapedMethodGroupLine = content + .Split('\n') + .Single(line => line.Contains("@Fire", StringComparison.Ordinal)); + var escapedMethodGroup = Assert.Single(references.Where(r => + r.SymbolName == "Fire" + && r.ReferenceKind == "call")); + Assert.Equal( + escapedMethodGroupLine.IndexOf("@Fire", StringComparison.Ordinal) + 1, + escapedMethodGroup.Column); + Assert.Equal("@Fire".Length, escapedMethodGroup.SpanLength); } [Fact] diff --git a/tests/CodeIndex.Tests/ReferenceExtractorJvmTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorJvmTests.cs index 3a0d0fa9b..53b0a6df7 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorJvmTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorJvmTests.cs @@ -87,10 +87,17 @@ public Supplier make() { var symbols = SymbolExtractor.Extract(1, "java", content); var references = ReferenceExtractor.Extract(1, "java", content, symbols); - Assert.Contains(references, r => + var intValueReference = Assert.Single(references.Where(r => r.SymbolName == "intValue" && r.ReferenceKind == "call" - && r.ContainerName == "sum"); + && r.ContainerName == "sum")); + var intValueLine = content + .Split('\n') + .Single(line => line.Contains("Integer::intValue", StringComparison.Ordinal)); + Assert.Equal( + intValueLine.IndexOf("intValue", StringComparison.Ordinal) + 1, + intValueReference.Column); + Assert.Equal("intValue".Length, intValueReference.SpanLength); Assert.Contains(references, r => r.SymbolName == "Integer" && r.ReferenceKind == "type_reference" @@ -951,7 +958,17 @@ class Demo { Assert.Contains(symbols, s => s.Name == "render name" && s.Kind == "function"); Assert.Contains(references, r => r.SymbolName == "User" && r.ReferenceKind == "type_reference"); - Assert.Contains(references, r => r.SymbolName == "render name" && r.ReferenceKind == "call"); + var renderReference = Assert.Single(references.Where(r => + r.SymbolName == "render name" + && r.ReferenceKind == "call")); + var renderLine = content + .Split('\n') + .Single(line => line.Contains("`render name`", StringComparison.Ordinal) + && line.Contains("::", StringComparison.Ordinal)); + Assert.Equal( + renderLine.IndexOf("`render name`", StringComparison.Ordinal) + 1, + renderReference.Column); + Assert.Equal("`render name`".Length, renderReference.SpanLength); Assert.DoesNotContain(references, r => r.SymbolName == "`render name`" && r.ReferenceKind == "call"); }