queries.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  1. /**
  2. * Graph Query Functions
  3. *
  4. * Higher-level query functions built on top of traversal algorithms.
  5. */
  6. import { Node, Edge, Context, Subgraph, EdgeKind } from '../types';
  7. import { QueryBuilder } from '../db/queries';
  8. import { GraphTraverser } from './traversal';
  9. /**
  10. * Graph query manager for complex queries
  11. */
  12. export class GraphQueryManager {
  13. private queries: QueryBuilder;
  14. private traverser: GraphTraverser;
  15. constructor(queries: QueryBuilder) {
  16. this.queries = queries;
  17. this.traverser = new GraphTraverser(queries);
  18. }
  19. /**
  20. * Get full context for a node
  21. *
  22. * Returns the focal node along with its ancestors, children,
  23. * and both incoming and outgoing references.
  24. *
  25. * @param nodeId - ID of the focal node
  26. * @returns Context object with all related information
  27. */
  28. getContext(nodeId: string): Context {
  29. const focal = this.queries.getNodeById(nodeId);
  30. if (!focal) {
  31. throw new Error(`Node not found: ${nodeId}`);
  32. }
  33. // Get ancestors (containment hierarchy)
  34. const ancestors = this.traverser.getAncestors(nodeId);
  35. // Get children
  36. const children = this.traverser.getChildren(nodeId);
  37. // Get incoming references (things that reference this node)
  38. const incomingEdges = this.queries.getIncomingEdges(nodeId);
  39. const incomingRefs: Array<{ node: Node; edge: Edge }> = [];
  40. for (const edge of incomingEdges) {
  41. // Skip containment edges (already in ancestors)
  42. if (edge.kind === 'contains') {
  43. continue;
  44. }
  45. const node = this.queries.getNodeById(edge.source);
  46. if (node) {
  47. incomingRefs.push({ node, edge });
  48. }
  49. }
  50. // Get outgoing references (things this node references)
  51. const outgoingEdges = this.queries.getOutgoingEdges(nodeId);
  52. const outgoingRefs: Array<{ node: Node; edge: Edge }> = [];
  53. for (const edge of outgoingEdges) {
  54. // Skip containment edges (already in children)
  55. if (edge.kind === 'contains') {
  56. continue;
  57. }
  58. const node = this.queries.getNodeById(edge.target);
  59. if (node) {
  60. outgoingRefs.push({ node, edge });
  61. }
  62. }
  63. // Get type information (type_of, returns edges)
  64. const types: Node[] = [];
  65. const typeEdgeKinds: EdgeKind[] = ['type_of', 'returns'];
  66. for (const kind of typeEdgeKinds) {
  67. const typeEdges = this.queries.getOutgoingEdges(nodeId, [kind]);
  68. for (const edge of typeEdges) {
  69. const typeNode = this.queries.getNodeById(edge.target);
  70. if (typeNode && !types.some((t) => t.id === typeNode.id)) {
  71. types.push(typeNode);
  72. }
  73. }
  74. }
  75. // Get relevant imports
  76. const imports: Node[] = [];
  77. const fileNode = ancestors.find((a) => a.kind === 'file');
  78. if (fileNode) {
  79. const importEdges = this.queries.getOutgoingEdges(fileNode.id, ['imports']);
  80. for (const edge of importEdges) {
  81. const importNode = this.queries.getNodeById(edge.target);
  82. if (importNode) {
  83. imports.push(importNode);
  84. }
  85. }
  86. }
  87. return {
  88. focal,
  89. ancestors,
  90. children,
  91. incomingRefs,
  92. outgoingRefs,
  93. types,
  94. imports,
  95. };
  96. }
  97. /**
  98. * Get dependencies of a file
  99. *
  100. * Returns all files that this file imports from.
  101. *
  102. * @param filePath - Path to the file
  103. * @returns Array of file paths this file depends on
  104. */
  105. getFileDependencies(filePath: string): string[] {
  106. const nodes = this.queries.getNodesByFile(filePath);
  107. const fileNode = nodes.find((n) => n.kind === 'file');
  108. if (!fileNode) {
  109. return [];
  110. }
  111. const dependencies = new Set<string>();
  112. const importEdges = this.queries.getOutgoingEdges(fileNode.id, ['imports']);
  113. for (const edge of importEdges) {
  114. const targetNode = this.queries.getNodeById(edge.target);
  115. if (targetNode && targetNode.filePath !== filePath) {
  116. dependencies.add(targetNode.filePath);
  117. }
  118. }
  119. return Array.from(dependencies);
  120. }
  121. /**
  122. * Get dependents of a file
  123. *
  124. * Returns all files that import from this file.
  125. *
  126. * @param filePath - Path to the file
  127. * @returns Array of file paths that depend on this file
  128. */
  129. getFileDependents(filePath: string): string[] {
  130. const nodes = this.queries.getNodesByFile(filePath);
  131. const dependents = new Set<string>();
  132. // For each exported symbol in this file, find imports
  133. for (const node of nodes) {
  134. if (node.isExported) {
  135. const incomingEdges = this.queries.getIncomingEdges(node.id, ['imports']);
  136. for (const edge of incomingEdges) {
  137. const sourceNode = this.queries.getNodeById(edge.source);
  138. if (sourceNode && sourceNode.filePath !== filePath) {
  139. dependents.add(sourceNode.filePath);
  140. }
  141. }
  142. }
  143. }
  144. return Array.from(dependents);
  145. }
  146. /**
  147. * Get all symbols exported by a file
  148. *
  149. * @param filePath - Path to the file
  150. * @returns Array of exported nodes
  151. */
  152. getExportedSymbols(filePath: string): Node[] {
  153. const nodes = this.queries.getNodesByFile(filePath);
  154. return nodes.filter((n) => n.isExported);
  155. }
  156. /**
  157. * Find symbols by qualified name pattern
  158. *
  159. * @param pattern - Pattern to match (supports * wildcard)
  160. * @returns Array of matching nodes
  161. */
  162. findByQualifiedName(pattern: string): Node[] {
  163. // Convert glob pattern to regex
  164. const regexPattern = pattern
  165. .replace(/[.+^${}()|[\]\\]/g, '\\$&')
  166. .replace(/\*/g, '.*')
  167. .replace(/\?/g, '.');
  168. const regex = new RegExp(`^${regexPattern}$`);
  169. // This is inefficient for large graphs - would need FTS index on qualified_name
  170. // For now, use kind-based filtering if possible
  171. const allNodes: Node[] = [];
  172. const kinds: Node['kind'][] = [
  173. 'class',
  174. 'function',
  175. 'method',
  176. 'interface',
  177. 'type_alias',
  178. 'variable',
  179. 'constant',
  180. ];
  181. for (const kind of kinds) {
  182. const nodes = this.queries.getNodesByKind(kind);
  183. for (const node of nodes) {
  184. if (regex.test(node.qualifiedName)) {
  185. allNodes.push(node);
  186. }
  187. }
  188. }
  189. return allNodes;
  190. }
  191. /**
  192. * Get the module/package structure
  193. *
  194. * Returns a tree structure of files organized by directory.
  195. *
  196. * @returns Map of directory paths to contained files
  197. */
  198. getModuleStructure(): Map<string, string[]> {
  199. const files = this.queries.getAllFiles();
  200. const structure = new Map<string, string[]>();
  201. for (const file of files) {
  202. const parts = file.path.split('/');
  203. const dir = parts.slice(0, -1).join('/') || '.';
  204. if (!structure.has(dir)) {
  205. structure.set(dir, []);
  206. }
  207. structure.get(dir)!.push(file.path);
  208. }
  209. return structure;
  210. }
  211. /**
  212. * Find circular dependencies in the graph
  213. *
  214. * @returns Array of cycles, each cycle is an array of node IDs
  215. */
  216. findCircularDependencies(): string[][] {
  217. const files = this.queries.getAllFiles();
  218. const cycles: string[][] = [];
  219. const visited = new Set<string>();
  220. const recursionStack = new Set<string>();
  221. const dfs = (filePath: string, path: string[]): void => {
  222. if (recursionStack.has(filePath)) {
  223. // Found a cycle
  224. const cycleStart = path.indexOf(filePath);
  225. if (cycleStart !== -1) {
  226. cycles.push(path.slice(cycleStart));
  227. }
  228. return;
  229. }
  230. if (visited.has(filePath)) {
  231. return;
  232. }
  233. visited.add(filePath);
  234. recursionStack.add(filePath);
  235. const dependencies = this.getFileDependencies(filePath);
  236. for (const dep of dependencies) {
  237. dfs(dep, [...path, filePath]);
  238. }
  239. recursionStack.delete(filePath);
  240. };
  241. for (const file of files) {
  242. if (!visited.has(file.path)) {
  243. dfs(file.path, []);
  244. }
  245. }
  246. return cycles;
  247. }
  248. /**
  249. * Get complexity metrics for a node
  250. *
  251. * @param nodeId - ID of the node
  252. * @returns Object containing various complexity metrics
  253. */
  254. getNodeMetrics(nodeId: string): {
  255. incomingEdgeCount: number;
  256. outgoingEdgeCount: number;
  257. callCount: number;
  258. callerCount: number;
  259. childCount: number;
  260. depth: number;
  261. } {
  262. const incomingEdges = this.queries.getIncomingEdges(nodeId);
  263. const outgoingEdges = this.queries.getOutgoingEdges(nodeId);
  264. const callEdges = outgoingEdges.filter((e) => e.kind === 'calls');
  265. const callerEdges = incomingEdges.filter((e) => e.kind === 'calls');
  266. const containsEdges = outgoingEdges.filter((e) => e.kind === 'contains');
  267. const ancestors = this.traverser.getAncestors(nodeId);
  268. return {
  269. incomingEdgeCount: incomingEdges.length,
  270. outgoingEdgeCount: outgoingEdges.length,
  271. callCount: callEdges.length,
  272. callerCount: callerEdges.length,
  273. childCount: containsEdges.length,
  274. depth: ancestors.length,
  275. };
  276. }
  277. /**
  278. * Find dead code (nodes with no incoming references)
  279. *
  280. * @param kinds - Node kinds to check (default: functions, methods, classes)
  281. * @returns Array of unreferenced nodes
  282. */
  283. findDeadCode(kinds?: Node['kind'][]): Node[] {
  284. const targetKinds = kinds || ['function', 'method', 'class'];
  285. const deadCode: Node[] = [];
  286. for (const kind of targetKinds) {
  287. const nodes = this.queries.getNodesByKind(kind);
  288. for (const node of nodes) {
  289. // Skip exported symbols (they may be used externally)
  290. if (node.isExported) {
  291. continue;
  292. }
  293. const incomingEdges = this.queries.getIncomingEdges(node.id);
  294. // Filter out containment edges
  295. const references = incomingEdges.filter((e) => e.kind !== 'contains');
  296. if (references.length === 0) {
  297. deadCode.push(node);
  298. }
  299. }
  300. }
  301. return deadCode;
  302. }
  303. /**
  304. * Get subgraph containing nodes matching a filter
  305. *
  306. * @param filter - Filter function to select nodes
  307. * @param includeEdges - Whether to include edges between matching nodes
  308. * @returns Subgraph containing matching nodes
  309. */
  310. getFilteredSubgraph(
  311. filter: (node: Node) => boolean,
  312. includeEdges: boolean = true
  313. ): Subgraph {
  314. const nodes = new Map<string, Node>();
  315. const edges: Edge[] = [];
  316. // Get all nodes of common kinds
  317. const kinds: Node['kind'][] = [
  318. 'file',
  319. 'module',
  320. 'class',
  321. 'struct',
  322. 'interface',
  323. 'trait',
  324. 'function',
  325. 'method',
  326. 'variable',
  327. 'constant',
  328. 'enum',
  329. 'type_alias',
  330. ];
  331. for (const kind of kinds) {
  332. const kindNodes = this.queries.getNodesByKind(kind);
  333. for (const node of kindNodes) {
  334. if (filter(node)) {
  335. nodes.set(node.id, node);
  336. }
  337. }
  338. }
  339. // Include edges between matching nodes
  340. if (includeEdges) {
  341. for (const nodeId of nodes.keys()) {
  342. const outgoing = this.queries.getOutgoingEdges(nodeId);
  343. for (const edge of outgoing) {
  344. if (nodes.has(edge.target)) {
  345. edges.push(edge);
  346. }
  347. }
  348. }
  349. }
  350. return {
  351. nodes,
  352. edges,
  353. roots: [],
  354. };
  355. }
  356. /**
  357. * Access the underlying traverser for direct traversal operations
  358. */
  359. getTraverser(): GraphTraverser {
  360. return this.traverser;
  361. }
  362. }