* Handle codegraph_callers
(args: Record<string, unknown>)
| 3078 | * Handle codegraph_callers |
| 3079 | */ |
| 3080 | private async handleCallers(args: Record<string, unknown>): Promise<ToolResult> { |
| 3081 | const symbol = this.validateString(args.symbol, 'symbol'); |
| 3082 | if (typeof symbol !== 'string') return symbol; |
| 3083 | |
| 3084 | const cg = this.getCodeGraph(args.projectPath as string | undefined); |
| 3085 | const limit = clamp((args.limit as number) || 20, 1, 100); |
| 3086 | const fileFilter = typeof args.file === 'string' ? args.file : undefined; |
| 3087 | |
| 3088 | const allMatches = this.findAllSymbols(cg, symbol); |
| 3089 | if (allMatches.nodes.length === 0) { |
| 3090 | return this.textResult(`Symbol "${symbol}" not found in the codebase${allMatches.note}`); |
| 3091 | } |
| 3092 | |
| 3093 | const { groups, filteredOut } = this.groupDefinitions(allMatches.nodes, fileFilter); |
| 3094 | const answerPaths = new Set(groups.flat().map(node => node.filePath)); |
| 3095 | const filterNote = filteredOut |
| 3096 | ? `\n\n> **Note:** no definition of "${symbol}" matches file "${fileFilter}" — showing all definitions instead.` |
| 3097 | : ''; |
| 3098 | |
| 3099 | const collect = (defNodes: Node[]) => { |
| 3100 | const seen = new Set<string>(); |
| 3101 | const callers: Node[] = []; |
| 3102 | const labels = new Map<string, string>(); |
| 3103 | for (const node of defNodes) { |
| 3104 | for (const c of cg.getCallers(node.id)) { |
| 3105 | if (!seen.has(c.node.id)) { |
| 3106 | seen.add(c.node.id); |
| 3107 | callers.push(c.node); |
| 3108 | answerPaths.add(c.node.filePath); |
| 3109 | const label = this.edgeLabel(c.edge); |
| 3110 | if (label) labels.set(c.node.id, label); |
| 3111 | } |
| 3112 | } |
| 3113 | } |
| 3114 | return { callers, labels }; |
| 3115 | }; |
| 3116 | |
| 3117 | // Single definition (or same-file overloads): the familiar flat list. |
| 3118 | if (groups.length === 1) { |
| 3119 | const { callers, labels } = collect(groups[0]!); |
| 3120 | if (callers.length === 0) { |
| 3121 | return this.answerResult(cg, `No callers found for "${symbol}"${allMatches.note}${filterNote}`, answerPaths); |
| 3122 | } |
| 3123 | // A successful `file` narrowing makes the multi-symbol aggregation note |
| 3124 | // stale — suppress it. |
| 3125 | const note = fileFilter && !filteredOut ? '' : allMatches.note; |
| 3126 | // Say when the cap cut the list (#1639, #1674): a truncated answer with |
| 3127 | // no marker reads as the complete set, and an agent under-counts from it. |
| 3128 | const cut = callers.length > limit |
| 3129 | ? `\n\n> Showing ${limit} of ${callers.length} callers; pass \`limit\` (up to 100) to widen.` |
| 3130 | : ''; |
| 3131 | const formatted = this.formatNodeList(callers.slice(0, limit), `Callers of ${symbol}`, labels) + cut + note + filterNote; |
| 3132 | return this.answerResult(cg, this.truncateOutput(formatted), answerPaths); |
| 3133 | } |
| 3134 | |
| 3135 | // Multiple DISTINCT definitions (#764): one section per definition so an |
| 3136 | // agent never mistakes one app's callers for another's. Narrow with |
| 3137 | // `file` to focus a single definition. |
no test coverage detected