traversal.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641
  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. for (const adjEdge of adjacentEdges) {
  79. // Determine next node: for 'both' direction, edges can be either
  80. // incoming or outgoing, so pick whichever end is not the current node
  81. const nextNodeId = adjEdge.source === node.id ? adjEdge.target : adjEdge.source;
  82. if (visited.has(nextNodeId)) {
  83. continue;
  84. }
  85. const nextNode = this.queries.getNodeById(nextNodeId);
  86. if (!nextNode) {
  87. continue;
  88. }
  89. // Apply node kind filter
  90. if (opts.nodeKinds && opts.nodeKinds.length > 0 && !opts.nodeKinds.includes(nextNode.kind)) {
  91. continue;
  92. }
  93. // Add node to result
  94. nodes.set(nextNode.id, nextNode);
  95. // Queue for further traversal
  96. queue.push({ node: nextNode, edge: adjEdge, depth: depth + 1 });
  97. }
  98. }
  99. return {
  100. nodes,
  101. edges,
  102. roots: [startId],
  103. };
  104. }
  105. /**
  106. * Traverse the graph using depth-first search
  107. *
  108. * @param startId - Starting node ID
  109. * @param options - Traversal options
  110. * @returns Subgraph containing traversed nodes and edges
  111. */
  112. traverseDFS(startId: string, options: TraversalOptions = {}): Subgraph {
  113. const opts = { ...DEFAULT_OPTIONS, ...options };
  114. const startNode = this.queries.getNodeById(startId);
  115. if (!startNode) {
  116. return { nodes: new Map(), edges: [], roots: [] };
  117. }
  118. const nodes = new Map<string, Node>();
  119. const edges: Edge[] = [];
  120. const visited = new Set<string>();
  121. if (opts.includeStart) {
  122. nodes.set(startNode.id, startNode);
  123. }
  124. this.dfsRecursive(startNode, 0, opts, nodes, edges, visited);
  125. return {
  126. nodes,
  127. edges,
  128. roots: [startId],
  129. };
  130. }
  131. /**
  132. * Recursive DFS helper
  133. */
  134. private dfsRecursive(
  135. node: Node,
  136. depth: number,
  137. opts: Required<TraversalOptions>,
  138. nodes: Map<string, Node>,
  139. edges: Edge[],
  140. visited: Set<string>
  141. ): void {
  142. if (visited.has(node.id) || nodes.size >= opts.limit || depth >= opts.maxDepth) {
  143. return;
  144. }
  145. visited.add(node.id);
  146. // Get adjacent edges
  147. const adjacentEdges = this.getAdjacentEdges(node.id, opts.direction, opts.edgeKinds);
  148. for (const edge of adjacentEdges) {
  149. // Determine next node: for 'both' direction, edges can be either
  150. // incoming or outgoing, so pick whichever end is not the current node
  151. const nextNodeId = edge.source === node.id ? edge.target : edge.source;
  152. if (visited.has(nextNodeId)) {
  153. continue;
  154. }
  155. const nextNode = this.queries.getNodeById(nextNodeId);
  156. if (!nextNode) {
  157. continue;
  158. }
  159. // Apply node kind filter
  160. if (opts.nodeKinds && opts.nodeKinds.length > 0 && !opts.nodeKinds.includes(nextNode.kind)) {
  161. continue;
  162. }
  163. // Add node and edge to result
  164. nodes.set(nextNode.id, nextNode);
  165. edges.push(edge);
  166. // Recurse
  167. this.dfsRecursive(nextNode, depth + 1, opts, nodes, edges, visited);
  168. }
  169. }
  170. /**
  171. * Get adjacent edges based on direction
  172. */
  173. private getAdjacentEdges(
  174. nodeId: string,
  175. direction: 'outgoing' | 'incoming' | 'both',
  176. edgeKinds?: EdgeKind[]
  177. ): Edge[] {
  178. const kinds = edgeKinds && edgeKinds.length > 0 ? edgeKinds : undefined;
  179. if (direction === 'outgoing') {
  180. return this.queries.getOutgoingEdges(nodeId, kinds);
  181. } else if (direction === 'incoming') {
  182. return this.queries.getIncomingEdges(nodeId, kinds);
  183. } else {
  184. // Both directions
  185. const outgoing = this.queries.getOutgoingEdges(nodeId, kinds);
  186. const incoming = this.queries.getIncomingEdges(nodeId, kinds);
  187. return [...outgoing, ...incoming];
  188. }
  189. }
  190. /**
  191. * Find all callers of a function/method
  192. *
  193. * @param nodeId - ID of the function/method node
  194. * @param maxDepth - Maximum depth to traverse (default: 1)
  195. * @returns Array of nodes that call this function
  196. */
  197. getCallers(nodeId: string, maxDepth: number = 1): Array<{ node: Node; edge: Edge }> {
  198. const result: Array<{ node: Node; edge: Edge }> = [];
  199. const visited = new Set<string>();
  200. this.getCallersRecursive(nodeId, maxDepth, 0, result, visited);
  201. return result;
  202. }
  203. private getCallersRecursive(
  204. nodeId: string,
  205. maxDepth: number,
  206. currentDepth: number,
  207. result: Array<{ node: Node; edge: Edge }>,
  208. visited: Set<string>
  209. ): void {
  210. if (currentDepth >= maxDepth || visited.has(nodeId)) {
  211. return;
  212. }
  213. visited.add(nodeId);
  214. const incomingEdges = this.queries.getIncomingEdges(nodeId, ['calls', 'references', 'imports']);
  215. for (const edge of incomingEdges) {
  216. const callerNode = this.queries.getNodeById(edge.source);
  217. if (callerNode && !visited.has(callerNode.id)) {
  218. result.push({ node: callerNode, edge });
  219. this.getCallersRecursive(callerNode.id, maxDepth, currentDepth + 1, result, visited);
  220. }
  221. }
  222. }
  223. /**
  224. * Find all functions/methods called by a function
  225. *
  226. * @param nodeId - ID of the function/method node
  227. * @param maxDepth - Maximum depth to traverse (default: 1)
  228. * @returns Array of nodes called by this function
  229. */
  230. getCallees(nodeId: string, maxDepth: number = 1): Array<{ node: Node; edge: Edge }> {
  231. const result: Array<{ node: Node; edge: Edge }> = [];
  232. const visited = new Set<string>();
  233. this.getCalleesRecursive(nodeId, maxDepth, 0, result, visited);
  234. return result;
  235. }
  236. private getCalleesRecursive(
  237. nodeId: string,
  238. maxDepth: number,
  239. currentDepth: number,
  240. result: Array<{ node: Node; edge: Edge }>,
  241. visited: Set<string>
  242. ): void {
  243. if (currentDepth >= maxDepth || visited.has(nodeId)) {
  244. return;
  245. }
  246. visited.add(nodeId);
  247. const outgoingEdges = this.queries.getOutgoingEdges(nodeId, ['calls', 'references', 'imports']);
  248. for (const edge of outgoingEdges) {
  249. const calleeNode = this.queries.getNodeById(edge.target);
  250. if (calleeNode && !visited.has(calleeNode.id)) {
  251. result.push({ node: calleeNode, edge });
  252. this.getCalleesRecursive(calleeNode.id, maxDepth, currentDepth + 1, result, visited);
  253. }
  254. }
  255. }
  256. /**
  257. * Get the call graph for a function (both callers and callees)
  258. *
  259. * @param nodeId - ID of the function/method node
  260. * @param depth - Maximum depth in each direction (default: 2)
  261. * @returns Subgraph containing the call graph
  262. */
  263. getCallGraph(nodeId: string, depth: number = 2): Subgraph {
  264. const focalNode = this.queries.getNodeById(nodeId);
  265. if (!focalNode) {
  266. return { nodes: new Map(), edges: [], roots: [] };
  267. }
  268. const nodes = new Map<string, Node>();
  269. const edges: Edge[] = [];
  270. // Add focal node
  271. nodes.set(focalNode.id, focalNode);
  272. // Get callers
  273. const callers = this.getCallers(nodeId, depth);
  274. for (const { node, edge } of callers) {
  275. nodes.set(node.id, node);
  276. edges.push(edge);
  277. }
  278. // Get callees
  279. const callees = this.getCallees(nodeId, depth);
  280. for (const { node, edge } of callees) {
  281. nodes.set(node.id, node);
  282. edges.push(edge);
  283. }
  284. return {
  285. nodes,
  286. edges,
  287. roots: [nodeId],
  288. };
  289. }
  290. /**
  291. * Get the type hierarchy for a class/interface
  292. *
  293. * @param nodeId - ID of the class/interface node
  294. * @returns Subgraph containing the type hierarchy
  295. */
  296. getTypeHierarchy(nodeId: string): Subgraph {
  297. const focalNode = this.queries.getNodeById(nodeId);
  298. if (!focalNode) {
  299. return { nodes: new Map(), edges: [], roots: [] };
  300. }
  301. const nodes = new Map<string, Node>();
  302. const edges: Edge[] = [];
  303. const visited = new Set<string>();
  304. // Add focal node
  305. nodes.set(focalNode.id, focalNode);
  306. // Get ancestors (what this extends/implements)
  307. this.getTypeAncestors(nodeId, nodes, edges, visited);
  308. // Get descendants (what extends/implements this)
  309. this.getTypeDescendants(nodeId, nodes, edges, visited);
  310. return {
  311. nodes,
  312. edges,
  313. roots: [nodeId],
  314. };
  315. }
  316. private getTypeAncestors(
  317. nodeId: string,
  318. nodes: Map<string, Node>,
  319. edges: Edge[],
  320. visited: Set<string>
  321. ): void {
  322. if (visited.has(nodeId)) {
  323. return;
  324. }
  325. visited.add(nodeId);
  326. const outgoingEdges = this.queries.getOutgoingEdges(nodeId, ['extends', 'implements']);
  327. for (const edge of outgoingEdges) {
  328. const parentNode = this.queries.getNodeById(edge.target);
  329. if (parentNode && !nodes.has(parentNode.id)) {
  330. nodes.set(parentNode.id, parentNode);
  331. edges.push(edge);
  332. this.getTypeAncestors(parentNode.id, nodes, edges, visited);
  333. }
  334. }
  335. }
  336. private getTypeDescendants(
  337. nodeId: string,
  338. nodes: Map<string, Node>,
  339. edges: Edge[],
  340. visited: Set<string>
  341. ): void {
  342. if (visited.has(nodeId)) {
  343. return;
  344. }
  345. visited.add(nodeId);
  346. const incomingEdges = this.queries.getIncomingEdges(nodeId, ['extends', 'implements']);
  347. for (const edge of incomingEdges) {
  348. const childNode = this.queries.getNodeById(edge.source);
  349. if (childNode && !nodes.has(childNode.id)) {
  350. nodes.set(childNode.id, childNode);
  351. edges.push(edge);
  352. this.getTypeDescendants(childNode.id, nodes, edges, visited);
  353. }
  354. }
  355. }
  356. /**
  357. * Find all usages of a symbol
  358. *
  359. * @param nodeId - ID of the symbol node
  360. * @returns Array of nodes and edges that reference this symbol
  361. */
  362. findUsages(nodeId: string): Array<{ node: Node; edge: Edge }> {
  363. const result: Array<{ node: Node; edge: Edge }> = [];
  364. // Get all incoming edges (references, calls, type_of, etc.)
  365. const incomingEdges = this.queries.getIncomingEdges(nodeId);
  366. for (const edge of incomingEdges) {
  367. const sourceNode = this.queries.getNodeById(edge.source);
  368. if (sourceNode) {
  369. result.push({ node: sourceNode, edge });
  370. }
  371. }
  372. return result;
  373. }
  374. /**
  375. * Calculate the impact radius of a node
  376. *
  377. * Returns all nodes that could be affected by changes to this node.
  378. *
  379. * @param nodeId - ID of the node
  380. * @param maxDepth - Maximum depth to traverse (default: 3)
  381. * @returns Subgraph containing potentially impacted nodes
  382. */
  383. getImpactRadius(nodeId: string, maxDepth: number = 3): Subgraph {
  384. const focalNode = this.queries.getNodeById(nodeId);
  385. if (!focalNode) {
  386. return { nodes: new Map(), edges: [], roots: [] };
  387. }
  388. const nodes = new Map<string, Node>();
  389. const edges: Edge[] = [];
  390. const visited = new Set<string>();
  391. // Add focal node
  392. nodes.set(focalNode.id, focalNode);
  393. // Traverse incoming edges to find all dependents
  394. this.getImpactRecursive(nodeId, maxDepth, 0, nodes, edges, visited);
  395. return {
  396. nodes,
  397. edges,
  398. roots: [nodeId],
  399. };
  400. }
  401. private getImpactRecursive(
  402. nodeId: string,
  403. maxDepth: number,
  404. currentDepth: number,
  405. nodes: Map<string, Node>,
  406. edges: Edge[],
  407. visited: Set<string>
  408. ): void {
  409. if (currentDepth >= maxDepth || visited.has(nodeId)) {
  410. return;
  411. }
  412. visited.add(nodeId);
  413. // For container nodes (classes, interfaces, structs, etc.), also traverse
  414. // into their children so that callers of contained methods appear in impact
  415. const focalNode = this.queries.getNodeById(nodeId);
  416. if (focalNode) {
  417. const containerKinds = new Set(['class', 'interface', 'struct', 'trait', 'protocol', 'module', 'enum']);
  418. if (containerKinds.has(focalNode.kind)) {
  419. const containsEdges = this.queries.getOutgoingEdges(nodeId, ['contains']);
  420. for (const edge of containsEdges) {
  421. const childNode = this.queries.getNodeById(edge.target);
  422. if (childNode && !visited.has(childNode.id)) {
  423. nodes.set(childNode.id, childNode);
  424. edges.push(edge);
  425. // Recurse into children at the same depth (they're part of the same symbol)
  426. this.getImpactRecursive(childNode.id, maxDepth, currentDepth, nodes, edges, visited);
  427. }
  428. }
  429. }
  430. }
  431. // Get all incoming edges (things that depend on this node)
  432. const incomingEdges = this.queries.getIncomingEdges(nodeId);
  433. for (const edge of incomingEdges) {
  434. const sourceNode = this.queries.getNodeById(edge.source);
  435. if (sourceNode && !nodes.has(sourceNode.id)) {
  436. nodes.set(sourceNode.id, sourceNode);
  437. edges.push(edge);
  438. this.getImpactRecursive(sourceNode.id, maxDepth, currentDepth + 1, nodes, edges, visited);
  439. }
  440. }
  441. }
  442. /**
  443. * Find the shortest path between two nodes
  444. *
  445. * @param fromId - Starting node ID
  446. * @param toId - Target node ID
  447. * @param edgeKinds - Edge types to consider (all if empty)
  448. * @returns Array of nodes and edges forming the path, or null if no path exists
  449. */
  450. findPath(
  451. fromId: string,
  452. toId: string,
  453. edgeKinds: EdgeKind[] = []
  454. ): Array<{ node: Node; edge: Edge | null }> | null {
  455. const fromNode = this.queries.getNodeById(fromId);
  456. const toNode = this.queries.getNodeById(toId);
  457. if (!fromNode || !toNode) {
  458. return null;
  459. }
  460. // BFS to find shortest path
  461. const visited = new Set<string>();
  462. const queue: Array<{ nodeId: string; path: Array<{ node: Node; edge: Edge | null }> }> = [
  463. { nodeId: fromId, path: [{ node: fromNode, edge: null }] },
  464. ];
  465. while (queue.length > 0) {
  466. const { nodeId, path } = queue.shift()!;
  467. if (nodeId === toId) {
  468. return path;
  469. }
  470. if (visited.has(nodeId)) {
  471. continue;
  472. }
  473. visited.add(nodeId);
  474. // Get outgoing edges
  475. const outgoingEdges = this.queries.getOutgoingEdges(
  476. nodeId,
  477. edgeKinds.length > 0 ? edgeKinds : undefined
  478. );
  479. for (const edge of outgoingEdges) {
  480. if (!visited.has(edge.target)) {
  481. const nextNode = this.queries.getNodeById(edge.target);
  482. if (nextNode) {
  483. queue.push({
  484. nodeId: edge.target,
  485. path: [...path, { node: nextNode, edge }],
  486. });
  487. }
  488. }
  489. }
  490. }
  491. return null; // No path found
  492. }
  493. /**
  494. * Get the containment hierarchy for a node (ancestors)
  495. *
  496. * @param nodeId - ID of the node
  497. * @returns Array of ancestor nodes from immediate parent to root
  498. */
  499. getAncestors(nodeId: string): Node[] {
  500. const ancestors: Node[] = [];
  501. const visited = new Set<string>();
  502. let currentId = nodeId;
  503. while (true) {
  504. if (visited.has(currentId)) {
  505. break;
  506. }
  507. visited.add(currentId);
  508. // Look for 'contains' edges pointing to this node
  509. const containingEdges = this.queries.getIncomingEdges(currentId, ['contains']);
  510. const firstEdge = containingEdges[0];
  511. if (!firstEdge) {
  512. break;
  513. }
  514. // Typically there should be at most one containing parent
  515. const parentNode = this.queries.getNodeById(firstEdge.source);
  516. if (parentNode) {
  517. ancestors.push(parentNode);
  518. currentId = parentNode.id;
  519. } else {
  520. break;
  521. }
  522. }
  523. return ancestors;
  524. }
  525. /**
  526. * Get immediate children of a node
  527. *
  528. * @param nodeId - ID of the node
  529. * @returns Array of child nodes
  530. */
  531. getChildren(nodeId: string): Node[] {
  532. const containsEdges = this.queries.getOutgoingEdges(nodeId, ['contains']);
  533. const children: Node[] = [];
  534. for (const edge of containsEdges) {
  535. const childNode = this.queries.getNodeById(edge.target);
  536. if (childNode) {
  537. children.push(childNode);
  538. }
  539. }
  540. return children;
  541. }
  542. }