* Handle codegraph_callers
(args: Record<string, unknown>)
| 1535 | * Handle codegraph_callers |
| 1536 | */ |
| 1537 | private async handleCallers(args: Record<string, unknown>): Promise<ToolResult> { |
| 1538 | const symbol = this.validateString(args.symbol, 'symbol'); |
| 1539 | if (typeof symbol !== 'string') return symbol; |
| 1540 | |
| 1541 | const cg = this.getCodeGraph(args.projectPath as string | undefined); |
| 1542 | const limit = clamp((args.limit as number) || 20, 1, 100); |
| 1543 | const fileFilter = typeof args.file === 'string' ? args.file : undefined; |
| 1544 | |
| 1545 | const allMatches = this.findAllSymbols(cg, symbol); |
| 1546 | if (allMatches.nodes.length === 0) { |
| 1547 | return this.textResult(`Symbol "${symbol}" not found in the codebase`); |
| 1548 | } |
| 1549 | |
| 1550 | const { groups, filteredOut } = this.groupDefinitions(allMatches.nodes, fileFilter); |
| 1551 | const filterNote = filteredOut |
| 1552 | ? `\n\n> **Note:** no definition of "${symbol}" matches file "${fileFilter}" — showing all definitions instead.` |
| 1553 | : ''; |
| 1554 | |
| 1555 | const collect = (defNodes: Node[]) => { |
| 1556 | const seen = new Set<string>(); |
| 1557 | const callers: Node[] = []; |
| 1558 | const labels = new Map<string, string>(); |
| 1559 | for (const node of defNodes) { |
| 1560 | for (const c of cg.getCallers(node.id)) { |
| 1561 | if (!seen.has(c.node.id)) { |
| 1562 | seen.add(c.node.id); |
| 1563 | callers.push(c.node); |
| 1564 | const label = this.edgeLabel(c.edge); |
| 1565 | if (label) labels.set(c.node.id, label); |
| 1566 | } |
| 1567 | } |
| 1568 | } |
| 1569 | return { callers, labels }; |
| 1570 | }; |
| 1571 | |
| 1572 | // Single definition (or same-file overloads): the familiar flat list. |
| 1573 | if (groups.length === 1) { |
| 1574 | const { callers, labels } = collect(groups[0]!); |
| 1575 | if (callers.length === 0) { |
| 1576 | return this.textResult(`No callers found for "${symbol}"${allMatches.note}${filterNote}`); |
| 1577 | } |
| 1578 | // A successful `file` narrowing makes the multi-symbol aggregation note |
| 1579 | // stale — suppress it. |
| 1580 | const note = fileFilter && !filteredOut ? '' : allMatches.note; |
| 1581 | const formatted = this.formatNodeList(callers.slice(0, limit), `Callers of ${symbol}`, labels) + note + filterNote; |
| 1582 | return this.textResult(this.truncateOutput(formatted)); |
| 1583 | } |
| 1584 | |
| 1585 | // Multiple DISTINCT definitions (#764): one section per definition so an |
| 1586 | // agent never mistakes one app's callers for another's. Narrow with |
| 1587 | // `file` to focus a single definition. |
| 1588 | const lines: string[] = [ |
| 1589 | `**Callers of ${symbol} — ${groups.length} distinct definitions (narrow with \`file\`)**`, |
| 1590 | ]; |
| 1591 | for (const group of groups) { |
| 1592 | const { callers, labels } = collect(group); |
| 1593 | lines.push('', this.definitionHeading(group)); |
| 1594 | if (callers.length === 0) { |
no test coverage detected