Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions changelog.d/unreleased/1777.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
category: fixed
issues:
- 1777
affected:
- src/CodeIndex/Database/DbReader.GraphQueries.cs
- src/CodeIndex/Cli/QueryCommandRunner.cs
- tests/CodeIndex.Tests/DbReaderTests.cs
---

## English

- **Caller/callee reference kind counts now preserve per-kind histograms (#1777)** - grouped `callers` / `callees` rows now populate `reference_kind_counts` / `referenceKindCounts` from the actual kind distribution, so a row with five `call` edges and one `instantiate` edge no longer collapses to an indistinguishable distinct-kind list.

## 日本語

- **caller/callee の reference kind count が kind ごとのヒストグラムを保持するようになりました (#1777)** - grouped `callers` / `callees` 行は実際の kind 分布から `reference_kind_counts` / `referenceKindCounts` を埋めるため、5 件の `call` と 1 件の `instantiate` を持つ行が distinct kind の一覧だけに潰れなくなりました。
38 changes: 23 additions & 15 deletions src/CodeIndex/Cli/QueryCommandRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -852,10 +852,10 @@ public static int RunCallers(string[] cmdArgs, JsonSerializerOptions jsonOptions
}
else
{
var kindColumnWidth = ComputeReferenceKindColumnWidth(results, r => FormatReferenceKindLabel(r.ReferenceKind, r.ReferenceKinds, r.HasMixedReferenceKinds));
var kindColumnWidth = ComputeReferenceKindColumnWidth(results, r => FormatReferenceKindLabel(r.ReferenceKind, r.ReferenceKinds, r.HasMixedReferenceKinds, r.ReferenceKindCounts));
foreach (var r in results)
{
var kindLabel = FormatReferenceKindLabel(r.ReferenceKind, r.ReferenceKinds, r.HasMixedReferenceKinds);
var kindLabel = FormatReferenceKindLabel(r.ReferenceKind, r.ReferenceKinds, r.HasMixedReferenceKinds, r.ReferenceKindCounts);
Console.WriteLine($"{kindLabel.PadRight(kindColumnWidth)} {r.CallerKind ?? "?",-10} {r.CallerName ?? "<top-level>",-32} {r.Path}:{r.FirstLine} -> {r.CalleeName} ({r.ReferenceCount} refs)");
}
var callerFileCount = results.Select(r => r.Path).Distinct().Count();
Expand Down Expand Up @@ -978,10 +978,10 @@ public static int RunCallees(string[] cmdArgs, JsonSerializerOptions jsonOptions
}
else
{
var kindColumnWidth = ComputeReferenceKindColumnWidth(results, r => FormatReferenceKindLabel(r.ReferenceKind, r.ReferenceKinds, r.HasMixedReferenceKinds));
var kindColumnWidth = ComputeReferenceKindColumnWidth(results, r => FormatReferenceKindLabel(r.ReferenceKind, r.ReferenceKinds, r.HasMixedReferenceKinds, r.ReferenceKindCounts));
foreach (var r in results)
{
var kindLabel = FormatReferenceKindLabel(r.ReferenceKind, r.ReferenceKinds, r.HasMixedReferenceKinds);
var kindLabel = FormatReferenceKindLabel(r.ReferenceKind, r.ReferenceKinds, r.HasMixedReferenceKinds, r.ReferenceKindCounts);
Console.WriteLine($"{kindLabel.PadRight(kindColumnWidth)} {r.CalleeName,-32} {r.Path}:{r.FirstLine} <- {r.CallerName ?? "<top-level>"} ({r.ReferenceCount} refs)");
}
var calleeFileCount = results.Select(r => r.Path).Distinct().Count();
Expand Down Expand Up @@ -5009,17 +5009,25 @@ private static string GetUsageLineOrThrow(string commandName) =>
ConsoleUi.GetUsageLine(commandName)
?? throw new InvalidOperationException($"Missing usage line for command '{commandName}'.");

// Human-readable reference_kind label for a grouped caller/callee row. When the
// group spans multiple kinds (e.g. `call` + `subscribe`), render them joined with
// `+` so the operator sees that the grouped row hides mixed semantics (issue #501).
// 単一ラベルに畳まれた reference_kind を人間向けに整形する。複数 kind が混在する
// 行 (`call` + `subscribe` など) は `+` 区切りで並べ、畳まれて見えなくなる意味の
// 違いを運用者が気付けるようにする (issue #501)。
private static string FormatReferenceKindLabel(string primary, IReadOnlyList<string> kinds, bool hasMixed)
{
if (!hasMixed || kinds == null || kinds.Count <= 1)
return primary ?? string.Empty;
return string.Join("+", kinds);
// Human-readable reference_kind label for a grouped caller/callee row. Counts
// keep high-volume relationships visible without requiring JSON re-querying.
// grouped caller/callee 行の人間向け reference_kind ラベル。count を併記して、
// JSON で再取得しなくても高頻度の関係が見えるようにする。
private static string FormatReferenceKindLabel(string primary, IReadOnlyList<string> kinds, bool hasMixed, IReadOnlyDictionary<string, int>? counts)
{
if (counts == null || counts.Count == 0)
{
if (!hasMixed || kinds == null || kinds.Count <= 1)
return primary ?? string.Empty;
return string.Join("+", kinds);
}

var orderedKinds = kinds is { Count: > 0 } && kinds.Any(kind => counts.TryGetValue(kind, out var count) && count > 0)
? kinds
: counts.Keys.Where(kind => counts[kind] > 0).OrderBy(kind => kind, StringComparer.Ordinal).ToArray();
return string.Join(", ", orderedKinds
.Where(kind => counts.TryGetValue(kind, out var count) && count > 0)
.Select(kind => counts[kind] == 1 ? kind : $"{kind} x{counts[kind]}"));
}

// Pick a column width that fits every label in the current batch so mixed-kind
Expand Down
77 changes: 40 additions & 37 deletions src/CodeIndex/Database/DbReader.GraphQueries.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,8 @@ public List<CallerResult> GetCallers(string query, int limit = 20, string? lang
WITH logical_references AS (
SELECT f.path, f.lang, r.container_kind, r.container_name, r.symbol_name,
" + groupedReferenceKindSql + @" AS reference_kind,
" + ReferenceKindCountSql("r.reference_kind", "call") + @" AS call_count,
" + ReferenceKindCountSql("r.reference_kind", "instantiate") + @" AS instantiate_count,
" + ReferenceKindCountSql("r.reference_kind", "subscribe") + @" AS subscribe_count,
r.reference_kind AS raw_reference_kind,
COUNT(*) AS reference_count,
" + ReferenceWeightedScoreSql("r.reference_kind") + @" AS weighted_score,
r.line,
MAX(" + selfReferenceSql + @") AS is_self_reference,
Expand All @@ -53,9 +52,7 @@ AND r.reference_kind IN " + CallGraphReferenceKindsSql + @"
SELECT f.path, f.lang, " + BuildCallerKindProjectionSql("r") + @" AS container_kind, " + BuildCallerNameProjectionSql("r") + @" AS container_name, r.symbol_name,
r.reference_kind, MIN(r.line) AS first_line, COUNT(*) AS reference_count,
GROUP_CONCAT(DISTINCT r.reference_kind) AS reference_kinds,
" + ReferenceKindCountSql("r.reference_kind", "call") + @" AS call_count,
" + ReferenceKindCountSql("r.reference_kind", "instantiate") + @" AS instantiate_count,
" + ReferenceKindCountSql("r.reference_kind", "subscribe") + @" AS subscribe_count,
r.reference_kind || ':' || COUNT(*) AS reference_kind_counts,
" + ReferenceWeightedScoreSql("r.reference_kind") + @" AS weighted_score,
MAX(" + selfReferenceSql + @") AS is_self_reference,
MAX(" + mutualRecursionSql + @") AS is_mutual_recursion
Expand Down Expand Up @@ -107,15 +104,13 @@ FROM symbol_references r
if (referenceKind == null)
{
sql += @"
GROUP BY f.path, f.lang, r.container_kind, r.container_name, r.symbol_name, r.file_id, r.line, r.column_number, " + groupedReferenceKindGroupSql + @"
GROUP BY f.path, f.lang, r.container_kind, r.container_name, r.symbol_name, r.file_id, r.line, r.column_number, " + groupedReferenceKindGroupSql + @", r.reference_kind
)
SELECT path, lang, " + BuildCallerKindProjectionSql("r") + @" AS container_kind, " + BuildCallerNameProjectionSql("r") + @" AS container_name, symbol_name,
" + (rawKinds ? GetGroupedCallerReferenceKindSql("r.reference_kind") : "MIN(r.reference_kind)") + @" AS reference_kind,
MIN(line) AS first_line, COUNT(*) AS reference_count,
MIN(line) AS first_line, SUM(r.reference_count) AS reference_count,
GROUP_CONCAT(DISTINCT r.reference_kind) AS reference_kinds,
SUM(r.call_count) AS call_count,
SUM(r.instantiate_count) AS instantiate_count,
SUM(r.subscribe_count) AS subscribe_count,
GROUP_CONCAT(r.raw_reference_kind || ':' || r.reference_count) AS reference_kind_counts,
SUM(r.weighted_score) AS weighted_score,
MAX(r.is_self_reference) AS is_self_reference,
MAX(r.is_mutual_recursion) AS is_mutual_recursion
Expand Down Expand Up @@ -162,7 +157,7 @@ FROM logical_references r
{
var primaryKind = reader.GetString(5);
var kinds = ParseDistinctReferenceKinds(GetNullableString(reader, 8), primaryKind);
var counts = BuildReferenceKindCounts(reader.GetInt32(9), reader.GetInt32(10), reader.GetInt32(11));
var counts = ParseReferenceKindCounts(GetNullableString(reader, 9), primaryKind, reader.GetInt32(7));
results.Add(new CallerResult
{
Path = reader.GetString(0),
Expand All @@ -174,11 +169,11 @@ FROM logical_references r
ReferenceKinds = kinds,
HasMixedReferenceKinds = kinds.Count > 1,
ReferenceKindCounts = counts,
ReferenceWeightScore = reader.GetDouble(12),
ReferenceWeightScore = reader.GetDouble(10),
FirstLine = reader.GetInt32(6),
ReferenceCount = reader.GetInt32(7),
HasSelfReference = reader.GetInt32(13) != 0,
HasMutualRecursion = reader.GetInt32(14) != 0,
HasSelfReference = reader.GetInt32(11) != 0,
HasMutualRecursion = reader.GetInt32(12) != 0,
});
}
return results;
Expand Down Expand Up @@ -380,9 +375,8 @@ public List<CalleeResult> GetCallees(string query, int limit = 20, string? lang
WITH logical_references AS (
SELECT f.path, f.lang, r.container_kind, r.container_name, r.symbol_name,
{preferredCalleeKindSql} AS reference_kind,
{ReferenceKindCountSql("r.reference_kind", "call")} AS call_count,
{ReferenceKindCountSql("r.reference_kind", "instantiate")} AS instantiate_count,
{ReferenceKindCountSql("r.reference_kind", "subscribe")} AS subscribe_count,
r.reference_kind AS raw_reference_kind,
COUNT(*) AS reference_count,
{ReferenceWeightedScoreSql("r.reference_kind")} AS weighted_score,
r.line
FROM symbol_references r
Expand All @@ -394,9 +388,7 @@ AND r.reference_kind IN {CallGraphReferenceKindsSql}
SELECT f.path, f.lang, r.container_kind, r.container_name, r.symbol_name,
r.reference_kind, MIN(r.line) AS first_line, COUNT(*) AS reference_count,
GROUP_CONCAT(DISTINCT r.reference_kind) AS reference_kinds,
" + ReferenceKindCountSql("r.reference_kind", "call") + @" AS call_count,
" + ReferenceKindCountSql("r.reference_kind", "instantiate") + @" AS instantiate_count,
" + ReferenceKindCountSql("r.reference_kind", "subscribe") + @" AS subscribe_count,
r.reference_kind || ':' || COUNT(*) AS reference_kind_counts,
" + ReferenceWeightedScoreSql("r.reference_kind") + @" AS weighted_score
FROM symbol_references r
JOIN files f ON r.file_id = f.id
Expand Down Expand Up @@ -440,14 +432,12 @@ FROM symbol_references r
if (referenceKind == null)
{
sql += @"
GROUP BY f.path, f.lang, r.container_kind, r.container_name, r.symbol_name, r.file_id, r.line, r.column_number
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, COUNT(*) AS reference_count,
reference_kind, MIN(line) AS first_line, SUM(r.reference_count) AS reference_count,
GROUP_CONCAT(DISTINCT reference_kind) AS reference_kinds,
SUM(r.call_count) AS call_count,
SUM(r.instantiate_count) AS instantiate_count,
SUM(r.subscribe_count) AS subscribe_count,
GROUP_CONCAT(r.raw_reference_kind || ':' || r.reference_count) AS reference_kind_counts,
SUM(r.weighted_score) AS weighted_score
FROM logical_references r
GROUP BY path, lang, container_kind, container_name, symbol_name, reference_kind";
Expand Down Expand Up @@ -495,7 +485,7 @@ FROM logical_references r
{
var primaryKind = reader.GetString(5);
var kinds = ParseDistinctReferenceKinds(GetNullableString(reader, 8), primaryKind);
var counts = BuildReferenceKindCounts(reader.GetInt32(9), reader.GetInt32(10), reader.GetInt32(11));
var counts = ParseReferenceKindCounts(GetNullableString(reader, 9), primaryKind, reader.GetInt32(7));
results.Add(new CalleeResult
{
Path = reader.GetString(0),
Expand All @@ -507,7 +497,7 @@ FROM logical_references r
ReferenceKinds = kinds,
HasMixedReferenceKinds = kinds.Count > 1,
ReferenceKindCounts = counts,
ReferenceWeightScore = reader.GetDouble(12),
ReferenceWeightScore = reader.GetDouble(10),
FirstLine = reader.GetInt32(6),
ReferenceCount = reader.GetInt32(7),
});
Expand Down Expand Up @@ -688,9 +678,6 @@ FROM symbol_references r
return ExecuteCountSummary(cmd);
}

private static string ReferenceKindCountSql(string columnSql, string kind) =>
$"SUM(CASE WHEN {columnSql} = '{kind}' THEN 1 ELSE 0 END)";

private static string ReferenceWeightedScoreSql(string columnSql) => $@"
SUM(CASE {columnSql}
WHEN 'instantiate' THEN 3.0
Expand All @@ -707,14 +694,30 @@ ELSE 0.0
_ => "weighted_score DESC, reference_count DESC",
};

private static IReadOnlyDictionary<string, int> BuildReferenceKindCounts(int callCount, int instantiateCount, int subscribeCount)
private static IReadOnlyDictionary<string, int> ParseReferenceKindCounts(string? aggregate, string primaryKind, int fallbackCount)
{
return new Dictionary<string, int>(StringComparer.Ordinal)
var counts = new Dictionary<string, int>(StringComparer.Ordinal);
counts["call"] = 0;
counts["instantiate"] = 0;
counts["subscribe"] = 0;
if (!string.IsNullOrWhiteSpace(aggregate))
{
["call"] = callCount,
["instantiate"] = instantiateCount,
["subscribe"] = subscribeCount,
};
foreach (var entry in aggregate.Split(','))
{
var separator = entry.LastIndexOf(':');
if (separator <= 0 || separator == entry.Length - 1)
continue;
var kind = entry[..separator].Trim();
if (kind.Length == 0 || !int.TryParse(entry[(separator + 1)..], out var count))
continue;
counts[kind] = counts.TryGetValue(kind, out var existing)
? existing + count
: count;
}
}
if (counts.Count == 0 && !string.IsNullOrEmpty(primaryKind))
counts[primaryKind] = fallbackCount;
return counts;
}

/// <summary>
Expand Down
54 changes: 52 additions & 2 deletions tests/CodeIndex.Tests/DbReaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10467,7 +10467,7 @@ public void GetCallers_ExposesDistinctReferenceKindsForMixedGroups()
ChunkIndex = 0,
StartLine = 1,
EndLine = 12,
Content = "public class MixedOwner { public void Setup() { Changed += Handler; Changed(); } }\n",
Content = "public class MixedOwner { public void Setup() { Changed += Handler; Changed(); Changed(); Changed(); Changed(); Changed(); } }\n",
}
]);
_writer.InsertReferences([
Expand All @@ -10493,14 +10493,60 @@ public void GetCallers_ExposesDistinctReferenceKindsForMixedGroups()
ContainerKind = "function",
ContainerName = "Setup",
},
new ReferenceRecord
{
FileId = fileId,
SymbolName = "Changed",
ReferenceKind = "call",
Line = 1,
Column = 73,
Context = "Changed();",
ContainerKind = "function",
ContainerName = "Setup",
},
new ReferenceRecord
{
FileId = fileId,
SymbolName = "Changed",
ReferenceKind = "call",
Line = 1,
Column = 84,
Context = "Changed();",
ContainerKind = "function",
ContainerName = "Setup",
},
new ReferenceRecord
{
FileId = fileId,
SymbolName = "Changed",
ReferenceKind = "call",
Line = 1,
Column = 95,
Context = "Changed();",
ContainerKind = "function",
ContainerName = "Setup",
},
new ReferenceRecord
{
FileId = fileId,
SymbolName = "Changed",
ReferenceKind = "call",
Line = 1,
Column = 106,
Context = "Changed();",
ContainerKind = "function",
ContainerName = "Setup",
},
]);

var caller = Assert.Single(_reader.GetCallers("Changed", lang: "csharp", exact: true, pathPatterns: ["mixed_kind_caller"]));
Assert.Equal("Setup", caller.CallerName);
Assert.Equal("Changed", caller.CalleeName);
Assert.Equal(2, caller.ReferenceCount);
Assert.Equal(6, caller.ReferenceCount);
Assert.True(caller.HasMixedReferenceKinds);
Assert.Equal(new[] { "event", "invoke" }, caller.ReferenceKinds);
Assert.Equal(5, caller.ReferenceKindCounts["call"]);
Assert.Equal(1, caller.ReferenceKindCounts["subscribe"]);
Assert.Equal("event", caller.ReferenceKind);

// `callees` rows are already split per kind, so each grouped row stays
Expand All @@ -10513,12 +10559,16 @@ public void GetCallers_ExposesDistinctReferenceKindsForMixedGroups()
Assert.Equal("event", callees[0].ReferenceKind);
Assert.False(callees[0].HasMixedReferenceKinds);
Assert.Equal(new[] { "event" }, callees[0].ReferenceKinds);
Assert.Equal(1, callees[0].ReferenceKindCounts["subscribe"]);
Assert.Equal("invoke", callees[1].ReferenceKind);
Assert.False(callees[1].HasMixedReferenceKinds);
Assert.Equal(new[] { "invoke" }, callees[1].ReferenceKinds);
Assert.Equal(5, callees[1].ReferenceKindCounts["call"]);

var rawCaller = Assert.Single(_reader.GetCallers("Changed", lang: "csharp", exact: true, pathPatterns: ["mixed_kind_caller"], rawKinds: true));
Assert.Equal(new[] { "call", "subscribe" }, rawCaller.ReferenceKinds);
Assert.Equal(5, rawCaller.ReferenceKindCounts["call"]);
Assert.Equal(1, rawCaller.ReferenceKindCounts["subscribe"]);
Assert.Equal("subscribe", rawCaller.ReferenceKind);
}

Expand Down
Loading
Loading