* Handle codegraph_callers
(args: Record<string, unknown>)
| 1570 | * Handle codegraph_callers |
| 1571 | */ |
| 1572 | private async handleCallers(args: Record<string, unknown>): Promise<ToolResult> { |
| 1573 | const symbol = this.validateString(args.symbol, 'symbol'); |
| 1574 | if (typeof symbol !== 'string') return symbol; |
| 1575 | |
| 1576 | const cg = this.getCodeGraph(args.projectPath as string | undefined); |
| 1577 | const limit = clamp((args.limit as number) || 20, 1, 100); |
| 1578 | const fileFilter = typeof args.file === 'string' ? args.file : undefined; |
| 1579 | |
| 1580 | const allMatches = this.findAllSymbols(cg, symbol); |
| 1581 | if (allMatches.nodes.length === 0) { |
| 1582 | return this.textResult(`Symbol "${symbol}" not found in the codebase`); |
| 1583 | } |
| 1584 | |
| 1585 | const { groups, filteredOut } = this.groupDefinitions(allMatches.nodes, fileFilter); |
| 1586 | const filterNote = filteredOut |
| 1587 | ? `\n\n> **Note:** no definition of "${symbol}" matches file "${fileFilter}" — showing all definitions instead.` |
| 1588 | : ''; |
| 1589 | |
| 1590 | const collect = (defNodes: Node[]) => { |
| 1591 | const seen = new Set<string>(); |
| 1592 | const callers: Node[] = []; |
| 1593 | const labels = new Map<string, string>(); |
| 1594 | for (const node of defNodes) { |
| 1595 | for (const c of cg.getCallers(node.id)) { |
| 1596 | if (!seen.has(c.node.id)) { |
| 1597 | seen.add(c.node.id); |
| 1598 | callers.push(c.node); |
| 1599 | const label = this.edgeLabel(c.edge); |
| 1600 | if (label) labels.set(c.node.id, label); |
| 1601 | } |
| 1602 | } |
| 1603 | } |
| 1604 | return { callers, labels }; |
| 1605 | }; |
| 1606 | |
| 1607 | // Single definition (or same-file overloads): the familiar flat list. |
| 1608 | if (groups.length === 1) { |
| 1609 | const { callers, labels } = collect(groups[0]!); |
| 1610 | if (callers.length === 0) { |
| 1611 | return this.textResult(`No callers found for "${symbol}"${allMatches.note}${filterNote}`); |
| 1612 | } |
| 1613 | // A successful `file` narrowing makes the multi-symbol aggregation note |
| 1614 | // stale — suppress it. |
| 1615 | const note = fileFilter && !filteredOut ? '' : allMatches.note; |
| 1616 | const formatted = this.formatNodeList(callers.slice(0, limit), `Callers of ${symbol}`, labels) + note + filterNote; |
| 1617 | return this.textResult(this.truncateOutput(formatted)); |
| 1618 | } |
| 1619 | |
| 1620 | // Multiple DISTINCT definitions (#764): one section per definition so an |
| 1621 | // agent never mistakes one app's callers for another's. Narrow with |
| 1622 | // `file` to focus a single definition. |
| 1623 | const lines: string[] = [ |
| 1624 | `**Callers of ${symbol} — ${groups.length} distinct definitions (narrow with \`file\`)**`, |
| 1625 | ]; |
| 1626 | for (const group of groups) { |
| 1627 | const { callers, labels } = collect(group); |
| 1628 | lines.push('', this.definitionHeading(group)); |
| 1629 | if (callers.length === 0) { |
no test coverage detected