* Handle codegraph_callers
(args: Record<string, unknown>)
| 1642 | * Handle codegraph_callers |
| 1643 | */ |
| 1644 | private async handleCallers(args: Record<string, unknown>): Promise<ToolResult> { |
| 1645 | const symbol = this.validateString(args.symbol, 'symbol'); |
| 1646 | if (typeof symbol !== 'string') return symbol; |
| 1647 | |
| 1648 | const cg = this.getCodeGraph(args.projectPath as string | undefined); |
| 1649 | const limit = clamp((args.limit as number) || 20, 1, 100); |
| 1650 | const fileFilter = typeof args.file === 'string' ? args.file : undefined; |
| 1651 | |
| 1652 | const allMatches = this.findAllSymbols(cg, symbol); |
| 1653 | if (allMatches.nodes.length === 0) { |
| 1654 | return this.textResult(`Symbol "${symbol}" not found in the codebase`); |
| 1655 | } |
| 1656 | |
| 1657 | const { groups, filteredOut } = this.groupDefinitions(allMatches.nodes, fileFilter); |
| 1658 | const filterNote = filteredOut |
| 1659 | ? `\n\n> **Note:** no definition of "${symbol}" matches file "${fileFilter}" — showing all definitions instead.` |
| 1660 | : ''; |
| 1661 | |
| 1662 | const collect = (defNodes: Node[]) => { |
| 1663 | const seen = new Set<string>(); |
| 1664 | const callers: Node[] = []; |
| 1665 | const labels = new Map<string, string>(); |
| 1666 | for (const node of defNodes) { |
| 1667 | for (const c of cg.getCallers(node.id)) { |
| 1668 | if (!seen.has(c.node.id)) { |
| 1669 | seen.add(c.node.id); |
| 1670 | callers.push(c.node); |
| 1671 | const label = this.edgeLabel(c.edge); |
| 1672 | if (label) labels.set(c.node.id, label); |
| 1673 | } |
| 1674 | } |
| 1675 | } |
| 1676 | return { callers, labels }; |
| 1677 | }; |
| 1678 | |
| 1679 | // Single definition (or same-file overloads): the familiar flat list. |
| 1680 | if (groups.length === 1) { |
| 1681 | const { callers, labels } = collect(groups[0]!); |
| 1682 | if (callers.length === 0) { |
| 1683 | return this.textResult(`No callers found for "${symbol}"${allMatches.note}${filterNote}`); |
| 1684 | } |
| 1685 | // A successful `file` narrowing makes the multi-symbol aggregation note |
| 1686 | // stale — suppress it. |
| 1687 | const note = fileFilter && !filteredOut ? '' : allMatches.note; |
| 1688 | const formatted = this.formatNodeList(callers.slice(0, limit), `Callers of ${symbol}`, labels) + note + filterNote; |
| 1689 | return this.textResult(this.truncateOutput(formatted)); |
| 1690 | } |
| 1691 | |
| 1692 | // Multiple DISTINCT definitions (#764): one section per definition so an |
| 1693 | // agent never mistakes one app's callers for another's. Narrow with |
| 1694 | // `file` to focus a single definition. |
| 1695 | const lines: string[] = [ |
| 1696 | `**Callers of ${symbol} — ${groups.length} distinct definitions (narrow with \`file\`)**`, |
| 1697 | ]; |
| 1698 | for (const group of groups) { |
| 1699 | const { callers, labels } = collect(group); |
| 1700 | lines.push('', this.definitionHeading(group)); |
| 1701 | if (callers.length === 0) { |
no test coverage detected