traversal.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675
  1. /**
  2. * Graph Traversal Algorithms
  3. *
  4. * BFS and DFS traversal for the code knowledge graph.
  5. */
  6. import { Node, Edge, Subgraph, TraversalOptions, EdgeKind } from '../types';
  7. import { QueryBuilder } from '../db/queries';
  8. /**
  9. * Default traversal options
  10. */
  11. const DEFAULT_OPTIONS: Required<TraversalOptions> = {
  12. maxDepth: Infinity,
  13. edgeKinds: [],
  14. nodeKinds: [],
  15. direction: 'outgoing',
  16. limit: 1000,
  17. includeStart: true,
  18. };
  19. /**
  20. * Result of a single traversal step
  21. */
  22. interface TraversalStep {
  23. node: Node;
  24. edge: Edge | null;
  25. depth: number;
  26. }
  27. /**
  28. * Graph traverser for BFS and DFS traversal
  29. */
  30. export class GraphTraverser {
  31. private queries: QueryBuilder;
  32. constructor(queries: QueryBuilder) {
  33. this.queries = queries;
  34. }
  35. /**
  36. * Traverse the graph using breadth-first search
  37. *
  38. * @param startId - Starting node ID
  39. * @param options - Traversal options
  40. * @returns Subgraph containing traversed nodes and edges
  41. */
  42. traverseBFS(startId: string, options: TraversalOptions = {}): Subgraph {
  43. const opts = { ...DEFAULT_OPTIONS, ...options };
  44. const startNode = this.queries.getNodeById(startId);
  45. if (!startNode) {
  46. return { nodes: new Map(), edges: [], roots: [] };
  47. }
  48. const nodes = new Map<string, Node>();
  49. const edges: Edge[] = [];
  50. const visited = new Set<string>();
  51. const queue: TraversalStep[] = [{ node: startNode, edge: null, depth: 0 }];
  52. if (opts.includeStart) {
  53. nodes.set(startNode.id, startNode);
  54. }
  55. while (queue.length > 0 && nodes.size < opts.limit) {
  56. const step = queue.shift()!;
  57. const { node, edge, depth } = step;
  58. if (visited.has(node.id)) {
  59. continue;
  60. }
  61. visited.add(node.id);
  62. // Add edge to result
  63. if (edge) {
  64. edges.push(edge);
  65. }
  66. // Check depth limit
  67. if (depth >= opts.maxDepth) {
  68. continue;
  69. }
  70. // Get adjacent edges, prioritizing structural edges (contains, calls)
  71. // over reference edges so BFS discovers internal structure before
  72. // fanning out to external references (e.g., component usages in templates).
  73. const adjacentEdges = this.getAdjacentEdges(node.id, opts.direction, opts.edgeKinds);
  74. adjacentEdges.sort((a, b) => {
  75. const priority = (e: Edge) => e.kind === 'contains' ? 0 : e.kind === 'calls' ? 1 : 2;
  76. return priority(a) - priority(b);
  77. });
  78. // Batch-fetch the unvisited neighbors in one query (was N+1 per BFS step).
  79. const wantIds = adjacentEdges
  80. .map((e) => (e.source === node.id ? e.target : e.source))
  81. .filter((id) => !visited.has(id));
  82. const neighborNodes = wantIds.length > 0 ? this.queries.getNodesByIds(wantIds) : new Map();
  83. for (const adjEdge of adjacentEdges) {
  84. const nextNodeId = adjEdge.source === node.id ? adjEdge.target : adjEdge.source;
  85. if (visited.has(nextNodeId)) continue;
  86. const nextNode = neighborNodes.get(nextNodeId);
  87. if (!nextNode) continue;
  88. if (opts.nodeKinds && opts.nodeKinds.length > 0 && !opts.nodeKinds.includes(nextNode.kind)) {
  89. continue;
  90. }
  91. nodes.set(nextNode.id, nextNode);
  92. queue.push({ node: nextNode, edge: adjEdge, depth: depth + 1 });
  93. }
  94. }
  95. return {
  96. nodes,
  97. edges,
  98. roots: [startId],
  99. };
  100. }
  101. /**
  102. * Traverse the graph using depth-first search
  103. *
  104. * @param startId - Starting node ID
  105. * @param options - Traversal options
  106. * @returns Subgraph containing traversed nodes and edges
  107. */
  108. traverseDFS(startId: string, options: TraversalOptions = {}): Subgraph {
  109. const opts = { ...DEFAULT_OPTIONS, ...options };
  110. const startNode = this.queries.getNodeById(startId);
  111. if (!startNode) {
  112. return { nodes: new Map(), edges: [], roots: [] };
  113. }
  114. const nodes = new Map<string, Node>();
  115. const edges: Edge[] = [];
  116. const visited = new Set<string>();
  117. if (opts.includeStart) {
  118. nodes.set(startNode.id, startNode);
  119. }
  120. this.dfsRecursive(startNode, 0, opts, nodes, edges, visited);
  121. return {
  122. nodes,
  123. edges,
  124. roots: [startId],
  125. };
  126. }
  127. /**
  128. * Recursive DFS helper
  129. */
  130. private dfsRecursive(
  131. node: Node,
  132. depth: number,
  133. opts: Required<TraversalOptions>,
  134. nodes: Map<string, Node>,
  135. edges: Edge[],
  136. visited: Set<string>
  137. ): void {
  138. if (visited.has(node.id) || nodes.size >= opts.limit || depth >= opts.maxDepth) {
  139. return;
  140. }
  141. visited.add(node.id);
  142. // Get adjacent edges
  143. const adjacentEdges = this.getAdjacentEdges(node.id, opts.direction, opts.edgeKinds);
  144. // Batch-fetch unvisited neighbors (was N+1 per DFS step).
  145. const wantIds = adjacentEdges
  146. .map((e) => (e.source === node.id ? e.target : e.source))
  147. .filter((id) => !visited.has(id));
  148. const neighborNodes = wantIds.length > 0 ? this.queries.getNodesByIds(wantIds) : new Map();
  149. for (const edge of adjacentEdges) {
  150. const nextNodeId = edge.source === node.id ? edge.target : edge.source;
  151. if (visited.has(nextNodeId)) continue;
  152. const nextNode = neighborNodes.get(nextNodeId);
  153. if (!nextNode) continue;
  154. // Apply node kind filter
  155. if (opts.nodeKinds && opts.nodeKinds.length > 0 && !opts.nodeKinds.includes(nextNode.kind)) {
  156. continue;
  157. }
  158. // Add node and edge to result
  159. nodes.set(nextNode.id, nextNode);
  160. edges.push(edge);
  161. // Recurse
  162. this.dfsRecursive(nextNode, depth + 1, opts, nodes, edges, visited);
  163. }
  164. }
  165. /**
  166. * Get adjacent edges based on direction
  167. */
  168. private getAdjacentEdges(
  169. nodeId: string,
  170. direction: 'outgoing' | 'incoming' | 'both',
  171. edgeKinds?: EdgeKind[]
  172. ): Edge[] {
  173. const kinds = edgeKinds && edgeKinds.length > 0 ? edgeKinds : undefined;
  174. if (direction === 'outgoing') {
  175. return this.queries.getOutgoingEdges(nodeId, kinds);
  176. } else if (direction === 'incoming') {
  177. return this.queries.getIncomingEdges(nodeId, kinds);
  178. } else {
  179. // Both directions
  180. const outgoing = this.queries.getOutgoingEdges(nodeId, kinds);
  181. const incoming = this.queries.getIncomingEdges(nodeId, kinds);
  182. return [...outgoing, ...incoming];
  183. }
  184. }
  185. /**
  186. * Find all callers of a function/method
  187. *
  188. * @param nodeId - ID of the function/method node
  189. * @param maxDepth - Maximum depth to traverse (default: 1)
  190. * @returns Array of nodes that call this function
  191. */
  192. getCallers(nodeId: string, maxDepth: number = 1): Array<{ node: Node; edge: Edge }> {
  193. const result: Array<{ node: Node; edge: Edge }> = [];
  194. const visited = new Set<string>();
  195. this.getCallersRecursive(nodeId, maxDepth, 0, result, visited);
  196. return result;
  197. }
  198. private getCallersRecursive(
  199. nodeId: string,
  200. maxDepth: number,
  201. currentDepth: number,
  202. result: Array<{ node: Node; edge: Edge }>,
  203. visited: Set<string>
  204. ): void {
  205. if (currentDepth >= maxDepth || visited.has(nodeId)) {
  206. return;
  207. }
  208. visited.add(nodeId);
  209. // `instantiates` counts as a caller: constructing a class (`Foo(...)` /
  210. // `new Foo()`) is calling its constructor, so the instantiation site is a
  211. // caller of the class. Without it, `callers <Class>` surfaced only the
  212. // importing file (via `imports`) and missed every construction site —
  213. // the opposite of "what breaks if I change this class?" (#774).
  214. const incomingEdges = this.queries.getIncomingEdges(nodeId, ['calls', 'references', 'imports', 'instantiates']);
  215. if (incomingEdges.length === 0) return;
  216. // Batch-fetch all caller nodes in one round-trip instead of one
  217. // getNodeById per edge (was N+1 — meaningful on functions with many callers).
  218. const sourceIds = incomingEdges.map((e) => e.source);
  219. const callerNodes = this.queries.getNodesByIds(sourceIds);
  220. for (const edge of incomingEdges) {
  221. const callerNode = callerNodes.get(edge.source);
  222. if (callerNode && !visited.has(callerNode.id)) {
  223. result.push({ node: callerNode, edge });
  224. this.getCallersRecursive(callerNode.id, maxDepth, currentDepth + 1, result, visited);
  225. }
  226. }
  227. }
  228. /**
  229. * Find all functions/methods called by a function
  230. *
  231. * @param nodeId - ID of the function/method node
  232. * @param maxDepth - Maximum depth to traverse (default: 1)
  233. * @returns Array of nodes called by this function
  234. */
  235. getCallees(nodeId: string, maxDepth: number = 1): Array<{ node: Node; edge: Edge }> {
  236. const result: Array<{ node: Node; edge: Edge }> = [];
  237. const visited = new Set<string>();
  238. this.getCalleesRecursive(nodeId, maxDepth, 0, result, visited);
  239. return result;
  240. }
  241. private getCalleesRecursive(
  242. nodeId: string,
  243. maxDepth: number,
  244. currentDepth: number,
  245. result: Array<{ node: Node; edge: Edge }>,
  246. visited: Set<string>
  247. ): void {
  248. if (currentDepth >= maxDepth || visited.has(nodeId)) {
  249. return;
  250. }
  251. visited.add(nodeId);
  252. // Symmetric with getCallers: a function that constructs a class
  253. // (`Foo(...)` / `new Foo()`) has that class as a callee, so callers and
  254. // callees stay inverses of each other and `trace` can cross the
  255. // instantiation boundary (function → class → its methods) (#774).
  256. const outgoingEdges = this.queries.getOutgoingEdges(nodeId, ['calls', 'references', 'imports', 'instantiates']);
  257. if (outgoingEdges.length === 0) return;
  258. // Batch-fetch callee nodes (was N+1 — see getCallersRecursive note).
  259. const targetIds = outgoingEdges.map((e) => e.target);
  260. const calleeNodes = this.queries.getNodesByIds(targetIds);
  261. for (const edge of outgoingEdges) {
  262. const calleeNode = calleeNodes.get(edge.target);
  263. if (calleeNode && !visited.has(calleeNode.id)) {
  264. result.push({ node: calleeNode, edge });
  265. this.getCalleesRecursive(calleeNode.id, maxDepth, currentDepth + 1, result, visited);
  266. }
  267. }
  268. }
  269. /**
  270. * Get the call graph for a function (both callers and callees)
  271. *
  272. * @param nodeId - ID of the function/method node
  273. * @param depth - Maximum depth in each direction (default: 2)
  274. * @returns Subgraph containing the call graph
  275. */
  276. getCallGraph(nodeId: string, depth: number = 2): Subgraph {
  277. const focalNode = this.queries.getNodeById(nodeId);
  278. if (!focalNode) {
  279. return { nodes: new Map(), edges: [], roots: [] };
  280. }
  281. const nodes = new Map<string, Node>();
  282. const edges: Edge[] = [];
  283. // Add focal node
  284. nodes.set(focalNode.id, focalNode);
  285. // Get callers
  286. const callers = this.getCallers(nodeId, depth);
  287. for (const { node, edge } of callers) {
  288. nodes.set(node.id, node);
  289. edges.push(edge);
  290. }
  291. // Get callees
  292. const callees = this.getCallees(nodeId, depth);
  293. for (const { node, edge } of callees) {
  294. nodes.set(node.id, node);
  295. edges.push(edge);
  296. }
  297. return {
  298. nodes,
  299. edges,
  300. roots: [nodeId],
  301. };
  302. }
  303. /**
  304. * Get the type hierarchy for a class/interface
  305. *
  306. * @param nodeId - ID of the class/interface node
  307. * @returns Subgraph containing the type hierarchy
  308. */
  309. getTypeHierarchy(nodeId: string): Subgraph {
  310. const focalNode = this.queries.getNodeById(nodeId);
  311. if (!focalNode) {
  312. return { nodes: new Map(), edges: [], roots: [] };
  313. }
  314. const nodes = new Map<string, Node>();
  315. const edges: Edge[] = [];
  316. const visited = new Set<string>();
  317. // Add focal node
  318. nodes.set(focalNode.id, focalNode);
  319. // Get ancestors (what this extends/implements)
  320. this.getTypeAncestors(nodeId, nodes, edges, visited);
  321. // Get descendants (what extends/implements this)
  322. this.getTypeDescendants(nodeId, nodes, edges, visited);
  323. return {
  324. nodes,
  325. edges,
  326. roots: [nodeId],
  327. };
  328. }
  329. private getTypeAncestors(
  330. nodeId: string,
  331. nodes: Map<string, Node>,
  332. edges: Edge[],
  333. visited: Set<string>
  334. ): void {
  335. if (visited.has(nodeId)) {
  336. return;
  337. }
  338. visited.add(nodeId);
  339. const outgoingEdges = this.queries.getOutgoingEdges(nodeId, ['extends', 'implements']);
  340. if (outgoingEdges.length === 0) return;
  341. const parents = this.queries.getNodesByIds(outgoingEdges.map((e) => e.target));
  342. for (const edge of outgoingEdges) {
  343. const parentNode = parents.get(edge.target);
  344. if (parentNode && !nodes.has(parentNode.id)) {
  345. nodes.set(parentNode.id, parentNode);
  346. edges.push(edge);
  347. this.getTypeAncestors(parentNode.id, nodes, edges, visited);
  348. }
  349. }
  350. }
  351. private getTypeDescendants(
  352. nodeId: string,
  353. nodes: Map<string, Node>,
  354. edges: Edge[],
  355. visited: Set<string>
  356. ): void {
  357. if (visited.has(nodeId)) {
  358. return;
  359. }
  360. visited.add(nodeId);
  361. const incomingEdges = this.queries.getIncomingEdges(nodeId, ['extends', 'implements']);
  362. if (incomingEdges.length === 0) return;
  363. const children = this.queries.getNodesByIds(incomingEdges.map((e) => e.source));
  364. for (const edge of incomingEdges) {
  365. const childNode = children.get(edge.source);
  366. if (childNode && !nodes.has(childNode.id)) {
  367. nodes.set(childNode.id, childNode);
  368. edges.push(edge);
  369. this.getTypeDescendants(childNode.id, nodes, edges, visited);
  370. }
  371. }
  372. }
  373. /**
  374. * Find all usages of a symbol
  375. *
  376. * @param nodeId - ID of the symbol node
  377. * @returns Array of nodes and edges that reference this symbol
  378. */
  379. findUsages(nodeId: string): Array<{ node: Node; edge: Edge }> {
  380. const result: Array<{ node: Node; edge: Edge }> = [];
  381. // Get all incoming edges (references, calls, type_of, etc.)
  382. const incomingEdges = this.queries.getIncomingEdges(nodeId);
  383. if (incomingEdges.length === 0) return result;
  384. // Batch-fetch source nodes (was N+1).
  385. const sources = this.queries.getNodesByIds(incomingEdges.map((e) => e.source));
  386. for (const edge of incomingEdges) {
  387. const sourceNode = sources.get(edge.source);
  388. if (sourceNode) result.push({ node: sourceNode, edge });
  389. }
  390. return result;
  391. }
  392. /**
  393. * Calculate the impact radius of a node
  394. *
  395. * Returns all nodes that could be affected by changes to this node.
  396. *
  397. * @param nodeId - ID of the node
  398. * @param maxDepth - Maximum depth to traverse (default: 3)
  399. * @returns Subgraph containing potentially impacted nodes
  400. */
  401. getImpactRadius(nodeId: string, maxDepth: number = 3): Subgraph {
  402. const focalNode = this.queries.getNodeById(nodeId);
  403. if (!focalNode) {
  404. return { nodes: new Map(), edges: [], roots: [] };
  405. }
  406. const nodes = new Map<string, Node>();
  407. const edges: Edge[] = [];
  408. const visited = new Set<string>();
  409. // Add focal node
  410. nodes.set(focalNode.id, focalNode);
  411. // Traverse incoming edges to find all dependents
  412. this.getImpactRecursive(nodeId, maxDepth, 0, nodes, edges, visited);
  413. return {
  414. nodes,
  415. edges,
  416. roots: [nodeId],
  417. };
  418. }
  419. private getImpactRecursive(
  420. nodeId: string,
  421. maxDepth: number,
  422. currentDepth: number,
  423. nodes: Map<string, Node>,
  424. edges: Edge[],
  425. visited: Set<string>
  426. ): void {
  427. if (currentDepth >= maxDepth || visited.has(nodeId)) {
  428. return;
  429. }
  430. visited.add(nodeId);
  431. // For container nodes (classes, interfaces, structs, etc.), also traverse
  432. // into their children so that callers of contained methods appear in impact
  433. const focalNode = this.queries.getNodeById(nodeId);
  434. if (focalNode) {
  435. const containerKinds = new Set(['class', 'interface', 'struct', 'trait', 'protocol', 'module', 'enum']);
  436. if (containerKinds.has(focalNode.kind)) {
  437. const containsEdges = this.queries.getOutgoingEdges(nodeId, ['contains']);
  438. if (containsEdges.length > 0) {
  439. const children = this.queries.getNodesByIds(containsEdges.map((e) => e.target));
  440. for (const edge of containsEdges) {
  441. const childNode = children.get(edge.target);
  442. if (childNode && !visited.has(childNode.id)) {
  443. nodes.set(childNode.id, childNode);
  444. edges.push(edge);
  445. // Recurse into children at the same depth (they're part of the same symbol)
  446. this.getImpactRecursive(childNode.id, maxDepth, currentDepth, nodes, edges, visited);
  447. }
  448. }
  449. }
  450. }
  451. }
  452. // Get all incoming edges (things that depend on this node). Exclude
  453. // `contains`: a container "contains" its members but does not *depend* on
  454. // them, so following it upward would climb to the parent class and then
  455. // re-expand every sibling member — exploding impact for a leaf symbol. (#536)
  456. const incomingEdges = this.queries.getIncomingEdges(nodeId).filter((e) => e.kind !== 'contains');
  457. if (incomingEdges.length === 0) return;
  458. const sources = this.queries.getNodesByIds(incomingEdges.map((e) => e.source));
  459. for (const edge of incomingEdges) {
  460. const sourceNode = sources.get(edge.source);
  461. if (sourceNode && !nodes.has(sourceNode.id)) {
  462. nodes.set(sourceNode.id, sourceNode);
  463. edges.push(edge);
  464. this.getImpactRecursive(sourceNode.id, maxDepth, currentDepth + 1, nodes, edges, visited);
  465. }
  466. }
  467. }
  468. /**
  469. * Find the shortest path between two nodes
  470. *
  471. * @param fromId - Starting node ID
  472. * @param toId - Target node ID
  473. * @param edgeKinds - Edge types to consider (all if empty)
  474. * @returns Array of nodes and edges forming the path, or null if no path exists
  475. */
  476. findPath(
  477. fromId: string,
  478. toId: string,
  479. edgeKinds: EdgeKind[] = []
  480. ): Array<{ node: Node; edge: Edge | null }> | null {
  481. const fromNode = this.queries.getNodeById(fromId);
  482. const toNode = this.queries.getNodeById(toId);
  483. if (!fromNode || !toNode) {
  484. return null;
  485. }
  486. // BFS to find shortest path
  487. const visited = new Set<string>();
  488. const queue: Array<{ nodeId: string; path: Array<{ node: Node; edge: Edge | null }> }> = [
  489. { nodeId: fromId, path: [{ node: fromNode, edge: null }] },
  490. ];
  491. while (queue.length > 0) {
  492. const { nodeId, path } = queue.shift()!;
  493. if (nodeId === toId) {
  494. return path;
  495. }
  496. if (visited.has(nodeId)) {
  497. continue;
  498. }
  499. visited.add(nodeId);
  500. // Get outgoing edges
  501. const outgoingEdges = this.queries.getOutgoingEdges(
  502. nodeId,
  503. edgeKinds.length > 0 ? edgeKinds : undefined
  504. );
  505. if (outgoingEdges.length === 0) continue;
  506. // Batch-fetch only the unvisited targets (was N+1 per BFS frontier).
  507. const wantIds = outgoingEdges
  508. .map((e) => e.target)
  509. .filter((id) => !visited.has(id));
  510. const nextNodes = wantIds.length > 0 ? this.queries.getNodesByIds(wantIds) : new Map();
  511. for (const edge of outgoingEdges) {
  512. if (!visited.has(edge.target)) {
  513. const nextNode = nextNodes.get(edge.target);
  514. if (nextNode) {
  515. queue.push({
  516. nodeId: edge.target,
  517. path: [...path, { node: nextNode, edge }],
  518. });
  519. }
  520. }
  521. }
  522. }
  523. return null; // No path found
  524. }
  525. /**
  526. * Get the containment hierarchy for a node (ancestors)
  527. *
  528. * @param nodeId - ID of the node
  529. * @returns Array of ancestor nodes from immediate parent to root
  530. */
  531. getAncestors(nodeId: string): Node[] {
  532. const ancestors: Node[] = [];
  533. const visited = new Set<string>();
  534. let currentId = nodeId;
  535. while (true) {
  536. if (visited.has(currentId)) {
  537. break;
  538. }
  539. visited.add(currentId);
  540. // Look for 'contains' edges pointing to this node
  541. const containingEdges = this.queries.getIncomingEdges(currentId, ['contains']);
  542. const firstEdge = containingEdges[0];
  543. if (!firstEdge) {
  544. break;
  545. }
  546. // Typically there should be at most one containing parent
  547. const parentNode = this.queries.getNodeById(firstEdge.source);
  548. if (parentNode) {
  549. ancestors.push(parentNode);
  550. currentId = parentNode.id;
  551. } else {
  552. break;
  553. }
  554. }
  555. return ancestors;
  556. }
  557. /**
  558. * Get immediate children of a node
  559. *
  560. * @param nodeId - ID of the node
  561. * @returns Array of child nodes
  562. */
  563. getChildren(nodeId: string): Node[] {
  564. const containsEdges = this.queries.getOutgoingEdges(nodeId, ['contains']);
  565. if (containsEdges.length === 0) return [];
  566. // Batch-fetch (was N+1).
  567. const childNodes = this.queries.getNodesByIds(containsEdges.map((e) => e.target));
  568. const children: Node[] = [];
  569. for (const edge of containsEdges) {
  570. const childNode = childNodes.get(edge.target);
  571. if (childNode) children.push(childNode);
  572. }
  573. return children;
  574. }
  575. }