mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-14 22:43:17 +08:00
fix(callers): list every call site of a caller, and a function that calls itself (#3077)
getCallers keeps one row per caller (issue #1086) with the caller's first edge only, and seeds its reported set with the symbol itself, so codegraph callers showed a caller that calls the symbol on two lines once, at the line its own declaration starts on, and left a recursive call out, though the index stores every one of those edges and each is a line to edit when the symbol changes. The CLI also merged edges on source, target and kind, which dropped a second call site from --json. A new GraphTraverser.getCallSites (CodeGraph.getCallSites) reads the direct callers or callees with every edge: one row per neighbour in the order getCallers / getCallees list them, every site kept, and the symbol's own row, flagged recursive, when it calls itself. getCallers and getCallees are unchanged, so ranking, explore's Flow and blast radius, impact and the viewer read exactly as before; a test pins that every non-recursive row of getCallSites equals getCallers' row and first edge. src/graph/call-sites.ts holds the one wording of a site list (9, 10, +N more) the surfaces share. - codegraph callers prints a caller at its call-site lines (up to 10, then +N more), a recursive row as "(calls itself)", and --json gives each row lines and recursive, with every site in edges. Callees keep the callee's declaration line, where a call leads. - codegraph_callers / codegraph_callees read the same rows (10 sites per caller), and codegraph_node's trail shows callers at their sites, 3 at most, and the symbol once among its callers when it calls itself. Validation: new callers-call-sites.test.ts (7 tests) fails 7 of 7 with main's sources and passes with the fix; 11 neighbouring callers, trail and lookup test files (217 tests) pass. Before/after on one index per repo (expressjs/express 0dee2c52, excalidraw d3a9c2a1, gin-gonic/gin 0f09c3a9): trails changed for 108/253, 3630/4180 and 557/1395 symbols, nearly all callers moving from the declaration line to the call sites; recursive rows newly shown 2/20/7. Budget: trail median 115->117, 354->360, 185->187 chars, largest growth +131/+212/+91 chars, and the busiest caller rows (67, 300 and 100 sites) stay one capped entry. codegraph_callers on each repo's 15 busiest callees: largest answer 1025->1358, 1882->2127, 1790->1805 chars. Not in this change: the issue's own recursion, child.walk() on a for...of element over children: TreeNode[], is not linked at all on main (the receiver's element type isn't inferred), so it needs a resolution fix of its own; this.walk() and a plain recursive call are. Reimplements PR #2994 by @johnhldiver-beep. Fixes #2909 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
59eb0d2820
commit
fa03fa6bb4
+39
-16
@@ -72,6 +72,7 @@ import { getTelemetry, TELEMETRY_DOCS, recordIndexEvent } from '../telemetry';
|
||||
import { BROWSER_ENV, DEFAULT_UI_PORT } from '../ui-server/constants';
|
||||
import type { UiServerHandle } from '../ui-server';
|
||||
import { lookupSymbolNodes, describeSymbolNode, groupDefinitions } from '../graph/symbol-lookup';
|
||||
import { callSiteLines, formatCallSiteLines } from '../graph/call-sites';
|
||||
import { goPackageReaderFor } from '../graph/go-package';
|
||||
import type { Node, Edge } from '../types';
|
||||
import { isTestPath } from '../search/query-utils';
|
||||
@@ -416,6 +417,9 @@ function cliNode(node: Node) {
|
||||
return { name: node.name, kind: node.kind, filePath: node.filePath, startLine: node.startLine };
|
||||
}
|
||||
|
||||
/** Call-site lines `codegraph callers` prints for one caller; `--json` lists them all. */
|
||||
const CLI_CALL_SITES_SHOWN = 10;
|
||||
|
||||
/** Attribute a group's edges to every overload of this definition. */
|
||||
function cliDefinition(group: Node[]) {
|
||||
const head = group[0]!;
|
||||
@@ -2419,29 +2423,47 @@ for (const direction of ['callers', 'callees'] as const) {
|
||||
const note = filteredOut
|
||||
? `no definition of "${symbol}" matches file "${options.file}" — showing all definitions instead.`
|
||||
: undefined;
|
||||
// Every site of each neighbour (#2909): a caller that calls the symbol on
|
||||
// two lines has both, and a symbol that calls itself is a row of its own.
|
||||
const collected = groups.map((group) => {
|
||||
const nodes = new Map<string, Node>();
|
||||
const edges = new Map<string, Edge>();
|
||||
const recursive = new Set<string>();
|
||||
for (const target of group) {
|
||||
const connections = direction === 'callers' ? cg.getCallers(target.id) : cg.getCallees(target.id);
|
||||
for (const { node, edge } of connections) {
|
||||
nodes.set(node.id, node);
|
||||
edges.set(`${edge.source}->${edge.target}:${edge.kind}`, edge);
|
||||
for (const row of cg.getCallSites(target.id, direction)) {
|
||||
nodes.set(row.node.id, row.node);
|
||||
if (row.recursive) recursive.add(row.node.id);
|
||||
for (const edge of row.edges) {
|
||||
edges.set(`${edge.source}|${edge.target}|${edge.kind}|${edge.line ?? -1}|${edge.column ?? -1}`, edge);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { group, nodes: [...nodes.values()], edges: [...edges.values()] };
|
||||
return { group, nodes: [...nodes.values()], edges: [...edges.values()], recursive };
|
||||
});
|
||||
|
||||
const relationships = (node: Node, edges: Edge[]) => [...new Set(edges
|
||||
.filter((edge) => (direction === 'callers' ? edge.source : edge.target) === node.id)
|
||||
.map((edge) => edge.kind))];
|
||||
const ownEdges = (node: Node, edges: Edge[]) =>
|
||||
edges.filter((edge) => (direction === 'callers' ? edge.source : edge.target) === node.id);
|
||||
const relationships = (node: Node, edges: Edge[]) => [...new Set(ownEdges(node, edges).map((edge) => edge.kind))];
|
||||
// A caller is listed at the lines its calls sit on, the lines to edit; a
|
||||
// callee at its own declaration, where a call leads.
|
||||
const location = (node: Node, edges: Edge[]) => {
|
||||
const lines = direction === 'callers' ? callSiteLines(ownEdges(node, edges)) : [];
|
||||
if (lines.length > 0) return `:${formatCallSiteLines(lines, CLI_CALL_SITES_SHOWN)}`;
|
||||
return node.startLine ? `:${node.startLine}` : '';
|
||||
};
|
||||
const row = (node: Node, edges: Edge[], recursive: boolean) => ({
|
||||
...cliNode(node),
|
||||
relationships: relationships(node, edges),
|
||||
lines: callSiteLines(ownEdges(node, edges)),
|
||||
recursive,
|
||||
});
|
||||
if (options.json) {
|
||||
const definitions = collected.map(({ group, nodes, edges }) => {
|
||||
const definitions = collected.map(({ group, nodes, edges, recursive }) => {
|
||||
const limited = nodes.slice(0, limit);
|
||||
const shown = new Set(limited.map((node) => node.id));
|
||||
return {
|
||||
...cliDefinition(group),
|
||||
[direction]: limited.map((node) => ({ id: node.id, ...cliNode(node), relationships: relationships(node, edges) })),
|
||||
[direction]: limited.map((node) => ({ id: node.id, ...row(node, edges, recursive.has(node.id)) })),
|
||||
edges: edges.filter((edge) => shown.has(direction === 'callers' ? edge.source : edge.target)),
|
||||
total: nodes.length,
|
||||
limit,
|
||||
@@ -2453,6 +2475,7 @@ for (const direction of ['callers', 'callees'] as const) {
|
||||
for (const node of nodes) union.set(node.id, node);
|
||||
}
|
||||
const total = union.size;
|
||||
const allEdges = collected.flatMap((entry) => entry.edges);
|
||||
console.log(JSON.stringify({
|
||||
symbol,
|
||||
targets: groups.flat().map((node) => cliDefinition([node]).definition),
|
||||
@@ -2462,8 +2485,8 @@ for (const direction of ['callers', 'callees'] as const) {
|
||||
filteredOut,
|
||||
note,
|
||||
definitions,
|
||||
[direction]: [...union.values()].slice(0, limit).map((node) => ({ ...cliNode(node),
|
||||
relationships: relationships(node, collected.flatMap((entry) => entry.edges)) })),
|
||||
[direction]: [...union.values()].slice(0, limit).map((node) =>
|
||||
row(node, allEdges, collected.some((entry) => entry.recursive.has(node.id)))),
|
||||
total,
|
||||
limit,
|
||||
truncated: total > limit,
|
||||
@@ -2473,7 +2496,7 @@ for (const direction of ['callers', 'callees'] as const) {
|
||||
if (ambiguous) {
|
||||
console.log(chalk.bold(`\n${title} of "${symbol}" — ${groups.length} distinct definitions (narrow with --file):`));
|
||||
}
|
||||
for (const { group, nodes, edges } of collected) {
|
||||
for (const { group, nodes, edges, recursive } of collected) {
|
||||
const limited = nodes.slice(0, limit);
|
||||
const total = nodes.length;
|
||||
const truncated = total > limit;
|
||||
@@ -2489,11 +2512,11 @@ for (const direction of ['callers', 'callees'] as const) {
|
||||
else info(`No ${direction} found for "${symbol}"`);
|
||||
}
|
||||
for (const node of limited) {
|
||||
const loc = node.startLine ? `:${node.startLine}` : '';
|
||||
const kinds = relationships(node, edges).filter((kind) => kind !== 'calls');
|
||||
const relation = kinds.length ? ` [${kinds.join(', ')}]` : '';
|
||||
console.log(chalk.cyan(node.kind.padEnd(12)) + chalk.white(node.name) + chalk.dim(relation));
|
||||
console.log(chalk.dim(` ${node.filePath}${loc}`));
|
||||
const itself = recursive.has(node.id) ? ' (calls itself)' : '';
|
||||
console.log(chalk.cyan(node.kind.padEnd(12)) + chalk.white(node.name) + chalk.dim(itself + relation));
|
||||
console.log(chalk.dim(` ${node.filePath}${location(node, edges)}`));
|
||||
console.log();
|
||||
}
|
||||
if (truncated) console.log(chalk.dim(`Showing ${limited.length} of ${total}; pass --limit to widen.`));
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* How a caller's call sites read, wherever callers are listed: `codegraph
|
||||
* callers`, codegraph_callers and the trail of codegraph_node (#2909). Each
|
||||
* site is a line to edit when the symbol changes, so the list says them all,
|
||||
* up to a cap that keeps one busy caller from crowding out the rest.
|
||||
*/
|
||||
import type { Edge } from '../types';
|
||||
|
||||
/** The lines `edges` sit on, ascending, once each. An edge with no line adds none. */
|
||||
export function callSiteLines(edges: readonly Edge[]): number[] {
|
||||
const lines = new Set<number>();
|
||||
for (const edge of edges) {
|
||||
if (typeof edge.line === 'number' && edge.line > 0) lines.add(edge.line);
|
||||
}
|
||||
return [...lines].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
/** `9, 10, 14`, or past `max` lines `9, 10, 14, +7 more`. */
|
||||
export function formatCallSiteLines(lines: readonly number[], max: number): string {
|
||||
const shown = lines.slice(0, max).join(', ');
|
||||
return lines.length > max ? `${shown}, +${lines.length - max} more` : shown;
|
||||
}
|
||||
@@ -5,6 +5,8 @@
|
||||
*/
|
||||
|
||||
export { GraphTraverser } from './traversal';
|
||||
export type { CallSiteRow } from './traversal';
|
||||
export { callSiteLines, formatCallSiteLines } from './call-sites';
|
||||
export { GraphQueryManager } from './queries';
|
||||
export {
|
||||
buildTypeHierarchy,
|
||||
|
||||
+67
-5
@@ -28,6 +28,34 @@ interface TraversalStep {
|
||||
depth: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The edges that make one symbol another's caller. `instantiates` counts:
|
||||
* constructing a class (`Foo(...)` / `new Foo()`) is calling its constructor,
|
||||
* so the instantiation site is a caller of the class (#774).
|
||||
*/
|
||||
const CALL_EDGE_KINDS: EdgeKind[] = ['calls', 'references', 'imports', 'instantiates', 'navigates'];
|
||||
|
||||
/** One edge's site: two edges are the same only on source, target, kind and position. */
|
||||
function edgeSite(e: Edge): string {
|
||||
return `${e.source}|${e.target}|${e.kind}|${e.line ?? -1}|${e.column ?? -1}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* A direct caller (or callee) of a symbol with every edge between them, from
|
||||
* {@link GraphTraverser.getCallSites}.
|
||||
*/
|
||||
export interface CallSiteRow {
|
||||
/** The caller, or for callees the callee. */
|
||||
node: Node;
|
||||
/**
|
||||
* Every edge between the two, once per site, in index order. The first is
|
||||
* the edge {@link GraphTraverser.getCallers} reports for this neighbour.
|
||||
*/
|
||||
edges: Edge[];
|
||||
/** The symbol calls itself: this row's node is the symbol asked about. */
|
||||
recursive: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Graph traverser for BFS and DFS traversal
|
||||
*/
|
||||
@@ -67,8 +95,6 @@ export class GraphTraverser {
|
||||
// Edge-identity dedup so a `direction:'both'` scan — which encounters A→B
|
||||
// from both endpoints — records each edge once.
|
||||
const seenEdges = new Set<string>();
|
||||
const edgeKey = (e: Edge) =>
|
||||
`${e.source}|${e.target}|${e.kind}|${e.line ?? -1}|${e.column ?? -1}`;
|
||||
const queue: TraversalStep[] = [{ node: startNode, edge: null, depth: 0 }];
|
||||
|
||||
if (opts.includeStart) {
|
||||
@@ -129,7 +155,7 @@ export class GraphTraverser {
|
||||
// adjacency scan (rather than once per dequeue) is what preserves
|
||||
// parallel edges to the same target (#1090); `nextNode` is guaranteed
|
||||
// to be in `nodes` at this point (just added, or already in-set).
|
||||
const ek = edgeKey(adjEdge);
|
||||
const ek = edgeSite(adjEdge);
|
||||
if (!seenEdges.has(ek)) {
|
||||
seenEdges.add(ek);
|
||||
edges.push(adjEdge);
|
||||
@@ -308,7 +334,7 @@ export class GraphTraverser {
|
||||
// caller of the class. Without it, `callers <Class>` surfaced only the
|
||||
// importing file (via `imports`) and missed every construction site —
|
||||
// the opposite of "what breaks if I change this class?" (#774).
|
||||
const incomingEdges = this.queries.getIncomingEdges(nodeId, ['calls', 'references', 'imports', 'instantiates', 'navigates']);
|
||||
const incomingEdges = this.queries.getIncomingEdges(nodeId, CALL_EDGE_KINDS);
|
||||
if (incomingEdges.length === 0) return;
|
||||
|
||||
// Batch-fetch all caller nodes in one round-trip instead of one
|
||||
@@ -363,7 +389,7 @@ export class GraphTraverser {
|
||||
// (`Foo(...)` / `new Foo()`) has that class as a callee, so callers and
|
||||
// callees stay inverses of each other and `trace` can cross the
|
||||
// instantiation boundary (function → class → its methods) (#774).
|
||||
const outgoingEdges = this.queries.getOutgoingEdges(nodeId, ['calls', 'references', 'imports', 'instantiates', 'navigates']);
|
||||
const outgoingEdges = this.queries.getOutgoingEdges(nodeId, CALL_EDGE_KINDS);
|
||||
if (outgoingEdges.length === 0) return;
|
||||
|
||||
// Batch-fetch callee nodes (was N+1 — see getCallersRecursive note).
|
||||
@@ -381,6 +407,42 @@ export class GraphTraverser {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The direct callers (or callees) of a symbol, each with every edge that
|
||||
* joins them: one row per neighbour, in the order {@link getCallers} and
|
||||
* {@link getCallees} list them. Those keep a neighbour's first edge only and
|
||||
* leave out the symbol itself; here a caller that calls it on two lines has
|
||||
* both sites, and a symbol that calls itself is a row of its own, flagged
|
||||
* `recursive`. Each of those sites is a line to edit when the symbol
|
||||
* changes, so `codegraph callers`, codegraph_callers and codegraph_node's
|
||||
* trail list them (#2909). The walks that rank and trace keep their own
|
||||
* one-row-per-neighbour reading.
|
||||
*/
|
||||
getCallSites(nodeId: string, direction: 'callers' | 'callees'): CallSiteRow[] {
|
||||
const edges = direction === 'callers'
|
||||
? this.queries.getIncomingEdges(nodeId, CALL_EDGE_KINDS)
|
||||
: this.queries.getOutgoingEdges(nodeId, CALL_EDGE_KINDS);
|
||||
if (edges.length === 0) return [];
|
||||
const neighbour = (e: Edge) => (direction === 'callers' ? e.source : e.target);
|
||||
const nodes = this.queries.getNodesByIds(edges.map(neighbour));
|
||||
const rows = new Map<string, CallSiteRow>();
|
||||
const sites = new Set<string>();
|
||||
for (const edge of edges) {
|
||||
const node = nodes.get(neighbour(edge));
|
||||
if (!node) continue;
|
||||
const site = edgeSite(edge);
|
||||
if (sites.has(site)) continue;
|
||||
sites.add(site);
|
||||
let row = rows.get(node.id);
|
||||
if (!row) {
|
||||
row = { node, edges: [], recursive: node.id === nodeId };
|
||||
rows.set(node.id, row);
|
||||
}
|
||||
row.edges.push(edge);
|
||||
}
|
||||
return [...rows.values()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the call graph for a function (both callers and callees)
|
||||
*
|
||||
|
||||
@@ -56,6 +56,7 @@ import {
|
||||
import { hasSynthesisPattern, isJsxChildCandidate } from './resolution/callback-synthesizer';
|
||||
import { createImportReader, reopenNameMatchedImports, reopenReferencesThroughAddedFiles, reopenReferencesThroughBarrels, reopenReferencesThroughChangedDefaults, reopenReferencesThroughDeclaredPackages, reopenReferencesThroughRetargetedModules } from './resolution/module-importers';
|
||||
import { GraphTraverser, GraphQueryManager } from './graph';
|
||||
import type { CallSiteRow } from './graph';
|
||||
import { findNamedCopybooks, type NamedCopybook } from './graph/cobol-copybooks';
|
||||
import { ContextBuilder, createContextBuilder } from './context';
|
||||
import { Mutex, FileLock, escapesRoot } from './utils';
|
||||
@@ -2863,6 +2864,18 @@ export class CodeGraph {
|
||||
return this.traverser.getCallees(nodeId, maxDepth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Direct callers or callees of a function/method, each with every site that
|
||||
* joins them, the symbol itself included when it calls itself
|
||||
*
|
||||
* @param nodeId - ID of the function/method node
|
||||
* @param direction - `'callers'` or `'callees'`
|
||||
* @returns One row per neighbour, in the order getCallers / getCallees list them
|
||||
*/
|
||||
getCallSites(nodeId: string, direction: 'callers' | 'callees'): CallSiteRow[] {
|
||||
return this.traverser.getCallSites(nodeId, direction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the impact radius of a node
|
||||
*
|
||||
|
||||
+93
-53
@@ -57,6 +57,8 @@ import { guardLabel, guardsForFileSync, siteKey, supportsBranchGuards, warmBranc
|
||||
import { findDynamicBoundaries, type BoundarySite } from '../graph/dynamic-boundary-report';
|
||||
import { countImplementers } from '../graph/type-hierarchy';
|
||||
import { describeSynthesizedHop } from '../graph/synthesized-hop';
|
||||
import { callSiteLines, formatCallSiteLines } from '../graph/call-sites';
|
||||
import type { CallSiteRow } from '../graph/traversal';
|
||||
import { isCopybookInclude, MAX_COPYBOOK_INCLUDES, type NamedCopybook } from '../graph/cobol-copybooks';
|
||||
import {
|
||||
findAllSymbols,
|
||||
@@ -2015,6 +2017,9 @@ export function getStaticTools(): ToolDefinition[] {
|
||||
*/
|
||||
const DEFAULT_MCP_TOOLS = new Set(['explore']);
|
||||
|
||||
/** Call-site lines codegraph_callers shows per caller (#2909), so one busy caller stays one line. */
|
||||
const MCP_CALL_SITES_SHOWN = 10;
|
||||
|
||||
/** realpath when the path exists, the path itself otherwise — never throws. */
|
||||
function canonicalPath(p: string): string {
|
||||
try {
|
||||
@@ -3347,27 +3352,35 @@ export class ToolHandler {
|
||||
? `\n\n> **Note:** no definition of "${symbol}" matches file "${fileFilter}" — showing all definitions instead.`
|
||||
: '';
|
||||
|
||||
// Every site of each caller (#2909): one row per caller, at the lines its
|
||||
// calls sit on, and the symbol itself when it calls itself.
|
||||
const collect = (defNodes: Node[]) => {
|
||||
const seen = new Set<string>();
|
||||
const callers: Node[] = [];
|
||||
const labels = new Map<string, string>();
|
||||
const sites = new Map<string, Edge[]>();
|
||||
const recursive = new Set<string>();
|
||||
for (const node of defNodes) {
|
||||
for (const c of cg.getCallers(node.id)) {
|
||||
if (!seen.has(c.node.id)) {
|
||||
seen.add(c.node.id);
|
||||
callers.push(c.node);
|
||||
answerPaths.add(c.node.filePath);
|
||||
const label = this.edgeLabel(c.edge);
|
||||
if (label) labels.set(c.node.id, label);
|
||||
for (const c of cg.getCallSites(node.id, 'callers')) {
|
||||
const known = sites.get(c.node.id);
|
||||
if (known) {
|
||||
known.push(...c.edges);
|
||||
continue;
|
||||
}
|
||||
sites.set(c.node.id, [...c.edges]);
|
||||
if (c.recursive) recursive.add(c.node.id);
|
||||
callers.push(c.node);
|
||||
answerPaths.add(c.node.filePath);
|
||||
const label = this.edgeLabel(c.edges[0]!);
|
||||
if (label) labels.set(c.node.id, label);
|
||||
}
|
||||
}
|
||||
return { callers, labels };
|
||||
const locations = new Map(callers.map((n) => [n.id, this.callSiteLocation(n, sites.get(n.id) ?? [])]));
|
||||
return { callers, labels, locations, recursive };
|
||||
};
|
||||
|
||||
// Single definition (or same-file overloads): the familiar flat list.
|
||||
if (groups.length === 1) {
|
||||
const { callers, labels } = collect(groups[0]!);
|
||||
const { callers, labels, locations, recursive } = collect(groups[0]!);
|
||||
if (callers.length === 0) {
|
||||
return this.answerResult(cg, `No callers found for "${symbol}"${allMatches.note}${filterNote}`, answerPaths);
|
||||
}
|
||||
@@ -3379,7 +3392,7 @@ export class ToolHandler {
|
||||
const cut = callers.length > limit
|
||||
? `\n\n> Showing ${limit} of ${callers.length} callers; pass \`limit\` (up to 100) to widen.`
|
||||
: '';
|
||||
const formatted = this.formatNodeList(callers.slice(0, limit), `Callers of ${symbol}`, labels) + cut + note + filterNote;
|
||||
const formatted = this.formatNodeList(callers.slice(0, limit), `Callers of ${symbol}`, labels, locations, recursive) + cut + note + filterNote;
|
||||
return this.answerResult(cg, this.truncateOutput(formatted), answerPaths);
|
||||
}
|
||||
|
||||
@@ -3390,16 +3403,14 @@ export class ToolHandler {
|
||||
`**Callers of ${symbol} — ${groups.length} distinct definitions (narrow with \`file\`)**`,
|
||||
];
|
||||
for (const group of groups) {
|
||||
const { callers, labels } = collect(group);
|
||||
const { callers, labels, locations, recursive } = collect(group);
|
||||
lines.push('', this.definitionHeading(group));
|
||||
if (callers.length === 0) {
|
||||
lines.push('- (no callers)');
|
||||
continue;
|
||||
}
|
||||
for (const node of callers.slice(0, limit)) {
|
||||
const location = node.startLine ? `:${node.startLine}` : '';
|
||||
const label = labels.get(node.id);
|
||||
lines.push(`- ${node.name} (${node.kind}) - ${node.filePath}${location}${label ? ` — via ${label}` : ''}`);
|
||||
lines.push(this.formatNodeListEntry(node, labels, locations, recursive));
|
||||
}
|
||||
if (callers.length > limit) {
|
||||
lines.push(`- … +${callers.length - limit} more (pass \`limit\` to widen)`);
|
||||
@@ -3430,26 +3441,30 @@ export class ToolHandler {
|
||||
? `\n\n> **Note:** no definition of "${symbol}" matches file "${fileFilter}" — showing all definitions instead.`
|
||||
: '';
|
||||
|
||||
// Callees are listed at their own declarations, where a call leads; a
|
||||
// symbol that calls itself is one of them (#2909).
|
||||
const collect = (defNodes: Node[]) => {
|
||||
const seen = new Set<string>();
|
||||
const callees: Node[] = [];
|
||||
const labels = new Map<string, string>();
|
||||
const recursive = new Set<string>();
|
||||
for (const node of defNodes) {
|
||||
for (const c of cg.getCallees(node.id)) {
|
||||
for (const c of cg.getCallSites(node.id, 'callees')) {
|
||||
if (!seen.has(c.node.id)) {
|
||||
seen.add(c.node.id);
|
||||
if (c.recursive) recursive.add(c.node.id);
|
||||
callees.push(c.node);
|
||||
answerPaths.add(c.node.filePath);
|
||||
const label = this.edgeLabel(c.edge);
|
||||
const label = this.edgeLabel(c.edges[0]!);
|
||||
if (label) labels.set(c.node.id, label);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { callees, labels };
|
||||
return { callees, labels, recursive };
|
||||
};
|
||||
|
||||
if (groups.length === 1) {
|
||||
const { callees, labels } = collect(groups[0]!);
|
||||
const { callees, labels, recursive } = collect(groups[0]!);
|
||||
if (callees.length === 0) {
|
||||
return this.answerResult(cg, `No callees found for "${symbol}"${allMatches.note}${filterNote}`, answerPaths);
|
||||
}
|
||||
@@ -3461,7 +3476,7 @@ export class ToolHandler {
|
||||
const cut = callees.length > limit
|
||||
? `\n\n> Showing ${limit} of ${callees.length} callees; pass \`limit\` (up to 100) to widen.`
|
||||
: '';
|
||||
const formatted = this.formatNodeList(callees.slice(0, limit), `Callees of ${symbol}`, labels) + cut + note + filterNote;
|
||||
const formatted = this.formatNodeList(callees.slice(0, limit), `Callees of ${symbol}`, labels, undefined, recursive) + cut + note + filterNote;
|
||||
return this.answerResult(cg, this.truncateOutput(formatted), answerPaths);
|
||||
}
|
||||
|
||||
@@ -3470,16 +3485,14 @@ export class ToolHandler {
|
||||
`**Callees of ${symbol} — ${groups.length} distinct definitions (narrow with \`file\`)**`,
|
||||
];
|
||||
for (const group of groups) {
|
||||
const { callees, labels } = collect(group);
|
||||
const { callees, labels, recursive } = collect(group);
|
||||
lines.push('', this.definitionHeading(group));
|
||||
if (callees.length === 0) {
|
||||
lines.push('- (no callees)');
|
||||
continue;
|
||||
}
|
||||
for (const node of callees.slice(0, limit)) {
|
||||
const location = node.startLine ? `:${node.startLine}` : '';
|
||||
const label = labels.get(node.id);
|
||||
lines.push(`- ${node.name} (${node.kind}) - ${node.filePath}${location}${label ? ` — via ${label}` : ''}`);
|
||||
lines.push(this.formatNodeListEntry(node, labels, undefined, recursive));
|
||||
}
|
||||
if (callees.length > limit) {
|
||||
lines.push(`- … +${callees.length - limit} more (pass \`limit\` to widen)`);
|
||||
@@ -8404,30 +8417,32 @@ export class ToolHandler {
|
||||
*/
|
||||
private formatTrail(cg: CodeGraph, node: Node): string {
|
||||
const TRAIL_CAP = 12;
|
||||
const fmt = (e: { node: Node; edge: Edge }) => {
|
||||
const base = `${e.node.name} (${e.node.filePath}:${e.node.startLine})`;
|
||||
const synth = this.synthEdgeNote(e.edge);
|
||||
/** Call-site lines shown per caller, so one busy caller can't crowd the trail. */
|
||||
const TRAIL_CALL_SITES = 3;
|
||||
const fmt = (row: CallSiteRow, where: string) => {
|
||||
const base = `${row.node.name} (${where})`;
|
||||
const synth = this.synthEdgeNote(row.edges[0]!);
|
||||
return synth ? `${base} [${synth}]` : base;
|
||||
};
|
||||
const collect = (edges: Array<{ node: Node; edge: Edge }>): Array<{ node: Node; edge: Edge }> => {
|
||||
const seen = new Set<string>([node.id]);
|
||||
const out: Array<{ node: Node; edge: Edge }> = [];
|
||||
for (const e of edges) {
|
||||
if (seen.has(e.node.id)) continue;
|
||||
seen.add(e.node.id);
|
||||
out.push(e);
|
||||
}
|
||||
return out;
|
||||
};
|
||||
const callees = collect(cg.getCallees(node.id));
|
||||
const callers = collect(cg.getCallers(node.id));
|
||||
// A callee reads at its declaration, where following it leads. A caller
|
||||
// reads at the lines its calls sit on, every one a line to edit when this
|
||||
// symbol changes (#2909), and a symbol that calls itself is among its own
|
||||
// callers, once.
|
||||
const callees = cg.getCallSites(node.id, 'callees').filter((row) => !row.recursive);
|
||||
const callers = cg.getCallSites(node.id, 'callers');
|
||||
if (callees.length === 0 && callers.length === 0) return '';
|
||||
const callee = (row: CallSiteRow) => fmt(row, `${row.node.filePath}:${row.node.startLine}`);
|
||||
const caller = (row: CallSiteRow) => {
|
||||
const sites = callSiteLines(row.edges);
|
||||
const at = sites.length > 0 ? formatCallSiteLines(sites, TRAIL_CALL_SITES) : String(row.node.startLine);
|
||||
return fmt(row, `${row.node.filePath}:${at}${row.recursive ? ', calls itself' : ''}`);
|
||||
};
|
||||
const lines: string[] = ['', '**Trail — codegraph_node any of these to follow it (no Read needed)**'];
|
||||
if (callees.length > 0) {
|
||||
lines.push(`**Calls →** ${callees.slice(0, TRAIL_CAP).map(fmt).join(', ')}${callees.length > TRAIL_CAP ? `, +${callees.length - TRAIL_CAP} more` : ''}`);
|
||||
lines.push(`**Calls →** ${callees.slice(0, TRAIL_CAP).map(callee).join(', ')}${callees.length > TRAIL_CAP ? `, +${callees.length - TRAIL_CAP} more` : ''}`);
|
||||
}
|
||||
if (callers.length > 0) {
|
||||
lines.push(`**Called by ←** ${callers.slice(0, TRAIL_CAP).map(fmt).join(', ')}${callers.length > TRAIL_CAP ? `, +${callers.length - TRAIL_CAP} more` : ''}`);
|
||||
lines.push(`**Called by ←** ${callers.slice(0, TRAIL_CAP).map(caller).join(', ')}${callers.length > TRAIL_CAP ? `, +${callers.length - TRAIL_CAP} more` : ''}`);
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
@@ -8861,22 +8876,47 @@ export class ToolHandler {
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
private formatNodeList(nodes: Node[], title: string, labels?: Map<string, string>): string {
|
||||
private formatNodeList(
|
||||
nodes: Node[],
|
||||
title: string,
|
||||
labels?: Map<string, string>,
|
||||
locations?: Map<string, string>,
|
||||
recursive?: Set<string>,
|
||||
): string {
|
||||
const lines: string[] = [`**${title} (${nodes.length} found)**`, ''];
|
||||
|
||||
for (const node of nodes) {
|
||||
const location = node.startLine ? `:${node.startLine}` : '';
|
||||
// Compact: just name, kind, location — plus the relationship when it
|
||||
// isn't a plain call (callback registration, instantiation, …).
|
||||
const label = labels?.get(node.id);
|
||||
lines.push(
|
||||
`- ${node.name} (${node.kind}) - ${node.filePath}${location}${label ? ` — via ${label}` : ''}`
|
||||
);
|
||||
}
|
||||
|
||||
for (const node of nodes) lines.push(this.formatNodeListEntry(node, labels, locations, recursive));
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* One callers/callees entry. Compact: name, kind, location (the caller's
|
||||
* call sites when `locations` has them, else the declaration's line), the
|
||||
* relationship when it isn't a plain call (callback registration,
|
||||
* instantiation, …), and whether the symbol calls itself.
|
||||
*/
|
||||
private formatNodeListEntry(
|
||||
node: Node,
|
||||
labels?: Map<string, string>,
|
||||
locations?: Map<string, string>,
|
||||
recursive?: Set<string>,
|
||||
): string {
|
||||
const location = locations?.get(node.id) ?? (node.startLine ? `:${node.startLine}` : '');
|
||||
const label = labels?.get(node.id);
|
||||
const itself = recursive?.has(node.id) ? ' — calls itself' : '';
|
||||
return `- ${node.name} (${node.kind}) - ${node.filePath}${location}${label ? ` — via ${label}` : ''}${itself}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* `:9, 10` — where a caller's calls sit, the lines to edit when the symbol
|
||||
* changes (#2909), capped at {@link MCP_CALL_SITES_SHOWN}; the caller's own
|
||||
* line when its edges carry none.
|
||||
*/
|
||||
private callSiteLocation(node: Node, edges: readonly Edge[]): string {
|
||||
const lines = callSiteLines(edges);
|
||||
if (lines.length > 0) return `:${formatCallSiteLines(lines, MCP_CALL_SITES_SHOWN)}`;
|
||||
return node.startLine ? `:${node.startLine}` : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship label for a non-`calls` edge in callers/callees lists. A
|
||||
* function-as-value edge (#756) is the high-signal one: `callers(cb)`
|
||||
|
||||
Reference in New Issue
Block a user