(
nodeId: string,
maxDepth: number,
currentDepth: number,
nodes: Map<string, Node>,
edges: Edge[],
visited: Set<string>
)
| 541 | } |
| 542 | |
| 543 | private getImpactRecursive( |
| 544 | nodeId: string, |
| 545 | maxDepth: number, |
| 546 | currentDepth: number, |
| 547 | nodes: Map<string, Node>, |
| 548 | edges: Edge[], |
| 549 | visited: Set<string> |
| 550 | ): void { |
| 551 | // Mark visited before the depth check so a node collected at the depth |
| 552 | // boundary still lands in `visited`. Otherwise it could sit in `nodes` but |
| 553 | // not `visited`, and the two loops below — which used different sets to |
| 554 | // gate re-processing — would disagree about it (#1089). |
| 555 | if (visited.has(nodeId)) { |
| 556 | return; |
| 557 | } |
| 558 | visited.add(nodeId); |
| 559 | if (currentDepth >= maxDepth) { |
| 560 | return; |
| 561 | } |
| 562 | |
| 563 | // For container nodes (classes, interfaces, structs, etc.), also traverse |
| 564 | // into their children so that callers of contained methods appear in impact |
| 565 | const focalNode = this.queries.getNodeById(nodeId); |
| 566 | if (focalNode) { |
| 567 | const containerKinds = new Set(['class', 'interface', 'struct', 'trait', 'protocol', 'module', 'enum']); |
| 568 | if (containerKinds.has(focalNode.kind)) { |
| 569 | const containsEdges = this.queries.getOutgoingEdges(nodeId, ['contains']); |
| 570 | if (containsEdges.length > 0) { |
| 571 | const children = this.queries.getNodesByIds(containsEdges.map((e) => e.target)); |
| 572 | for (const edge of containsEdges) { |
| 573 | const childNode = children.get(edge.target); |
| 574 | if (childNode && !visited.has(childNode.id)) { |
| 575 | nodes.set(childNode.id, childNode); |
| 576 | edges.push(edge); |
| 577 | // Recurse into children at the same depth (they're part of the same symbol) |
| 578 | this.getImpactRecursive(childNode.id, maxDepth, currentDepth, nodes, edges, visited); |
| 579 | } |
| 580 | } |
| 581 | } |
| 582 | } |
| 583 | } |
| 584 | |
| 585 | // Get all incoming edges (things that depend on this node). Exclude |
| 586 | // `contains`: a container "contains" its members but does not *depend* on |
| 587 | // them, so following it upward would climb to the parent class and then |
| 588 | // re-expand every sibling member — exploding impact for a leaf symbol. (#536) |
| 589 | const incomingEdges = this.queries.getIncomingEdges(nodeId).filter((e) => e.kind !== 'contains'); |
| 590 | if (incomingEdges.length === 0) return; |
| 591 | const sources = this.queries.getNodesByIds(incomingEdges.map((e) => e.source)); |
| 592 | |
| 593 | for (const edge of incomingEdges) { |
| 594 | const sourceNode = sources.get(edge.source); |
| 595 | if (!sourceNode) continue; |
| 596 | // Record the dependency edge unconditionally. The gate used to also gate |
| 597 | // edge collection (`!nodes.has(...)`), so a second incoming edge into a |
| 598 | // node already collected via another path was silently dropped from |
| 599 | // `edges` even though it's a real dependency (#1089). Each node's incoming |
| 600 | // edges are fetched once (nodes are expanded once), so no edge repeats. |
no test coverage detected