| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416 |
- /**
- * Graph Query Functions
- *
- * Higher-level query functions built on top of traversal algorithms.
- */
- import { Node, Edge, Context, Subgraph, EdgeKind } from '../types';
- import { QueryBuilder } from '../db/queries';
- import { GraphTraverser } from './traversal';
- /**
- * Graph query manager for complex queries
- */
- export class GraphQueryManager {
- private queries: QueryBuilder;
- private traverser: GraphTraverser;
- constructor(queries: QueryBuilder) {
- this.queries = queries;
- this.traverser = new GraphTraverser(queries);
- }
- /**
- * Get full context for a node
- *
- * Returns the focal node along with its ancestors, children,
- * and both incoming and outgoing references.
- *
- * @param nodeId - ID of the focal node
- * @returns Context object with all related information
- */
- getContext(nodeId: string): Context {
- const focal = this.queries.getNodeById(nodeId);
- if (!focal) {
- throw new Error(`Node not found: ${nodeId}`);
- }
- // Get ancestors (containment hierarchy)
- const ancestors = this.traverser.getAncestors(nodeId);
- // Get children
- const children = this.traverser.getChildren(nodeId);
- // Get incoming references (things that reference this node)
- const incomingEdges = this.queries.getIncomingEdges(nodeId);
- const incomingRefs: Array<{ node: Node; edge: Edge }> = [];
- for (const edge of incomingEdges) {
- // Skip containment edges (already in ancestors)
- if (edge.kind === 'contains') {
- continue;
- }
- const node = this.queries.getNodeById(edge.source);
- if (node) {
- incomingRefs.push({ node, edge });
- }
- }
- // Get outgoing references (things this node references)
- const outgoingEdges = this.queries.getOutgoingEdges(nodeId);
- const outgoingRefs: Array<{ node: Node; edge: Edge }> = [];
- for (const edge of outgoingEdges) {
- // Skip containment edges (already in children)
- if (edge.kind === 'contains') {
- continue;
- }
- const node = this.queries.getNodeById(edge.target);
- if (node) {
- outgoingRefs.push({ node, edge });
- }
- }
- // Get type information (type_of, returns edges)
- const types: Node[] = [];
- const typeEdgeKinds: EdgeKind[] = ['type_of', 'returns'];
- for (const kind of typeEdgeKinds) {
- const typeEdges = this.queries.getOutgoingEdges(nodeId, [kind]);
- for (const edge of typeEdges) {
- const typeNode = this.queries.getNodeById(edge.target);
- if (typeNode && !types.some((t) => t.id === typeNode.id)) {
- types.push(typeNode);
- }
- }
- }
- // Get relevant imports
- const imports: Node[] = [];
- const fileNode = ancestors.find((a) => a.kind === 'file');
- if (fileNode) {
- const importEdges = this.queries.getOutgoingEdges(fileNode.id, ['imports']);
- for (const edge of importEdges) {
- const importNode = this.queries.getNodeById(edge.target);
- if (importNode) {
- imports.push(importNode);
- }
- }
- }
- return {
- focal,
- ancestors,
- children,
- incomingRefs,
- outgoingRefs,
- types,
- imports,
- };
- }
- /**
- * Get dependencies of a file
- *
- * Returns all files that this file imports from.
- *
- * @param filePath - Path to the file
- * @returns Array of file paths this file depends on
- */
- getFileDependencies(filePath: string): string[] {
- const nodes = this.queries.getNodesByFile(filePath);
- const fileNode = nodes.find((n) => n.kind === 'file');
- if (!fileNode) {
- return [];
- }
- const dependencies = new Set<string>();
- const importEdges = this.queries.getOutgoingEdges(fileNode.id, ['imports']);
- for (const edge of importEdges) {
- const targetNode = this.queries.getNodeById(edge.target);
- if (targetNode && targetNode.filePath !== filePath) {
- dependencies.add(targetNode.filePath);
- }
- }
- return Array.from(dependencies);
- }
- /**
- * Get dependents of a file
- *
- * Returns all files that import from this file.
- *
- * @param filePath - Path to the file
- * @returns Array of file paths that depend on this file
- */
- getFileDependents(filePath: string): string[] {
- const nodes = this.queries.getNodesByFile(filePath);
- const dependents = new Set<string>();
- // For each exported symbol in this file, find imports
- for (const node of nodes) {
- if (node.isExported) {
- const incomingEdges = this.queries.getIncomingEdges(node.id, ['imports']);
- for (const edge of incomingEdges) {
- const sourceNode = this.queries.getNodeById(edge.source);
- if (sourceNode && sourceNode.filePath !== filePath) {
- dependents.add(sourceNode.filePath);
- }
- }
- }
- }
- return Array.from(dependents);
- }
- /**
- * Get all symbols exported by a file
- *
- * @param filePath - Path to the file
- * @returns Array of exported nodes
- */
- getExportedSymbols(filePath: string): Node[] {
- const nodes = this.queries.getNodesByFile(filePath);
- return nodes.filter((n) => n.isExported);
- }
- /**
- * Find symbols by qualified name pattern
- *
- * @param pattern - Pattern to match (supports * wildcard)
- * @returns Array of matching nodes
- */
- findByQualifiedName(pattern: string): Node[] {
- // Convert glob pattern to regex
- const regexPattern = pattern
- .replace(/[.+^${}()|[\]\\]/g, '\\$&')
- .replace(/\*/g, '.*')
- .replace(/\?/g, '.');
- const regex = new RegExp(`^${regexPattern}$`);
- // This is inefficient for large graphs - would need FTS index on qualified_name
- // For now, use kind-based filtering if possible
- const allNodes: Node[] = [];
- const kinds: Node['kind'][] = [
- 'class',
- 'function',
- 'method',
- 'interface',
- 'type_alias',
- 'variable',
- 'constant',
- ];
- for (const kind of kinds) {
- const nodes = this.queries.getNodesByKind(kind);
- for (const node of nodes) {
- if (regex.test(node.qualifiedName)) {
- allNodes.push(node);
- }
- }
- }
- return allNodes;
- }
- /**
- * Get the module/package structure
- *
- * Returns a tree structure of files organized by directory.
- *
- * @returns Map of directory paths to contained files
- */
- getModuleStructure(): Map<string, string[]> {
- const files = this.queries.getAllFiles();
- const structure = new Map<string, string[]>();
- for (const file of files) {
- const parts = file.path.split('/');
- const dir = parts.slice(0, -1).join('/') || '.';
- if (!structure.has(dir)) {
- structure.set(dir, []);
- }
- structure.get(dir)!.push(file.path);
- }
- return structure;
- }
- /**
- * Find circular dependencies in the graph
- *
- * @returns Array of cycles, each cycle is an array of node IDs
- */
- findCircularDependencies(): string[][] {
- const files = this.queries.getAllFiles();
- const cycles: string[][] = [];
- const visited = new Set<string>();
- const recursionStack = new Set<string>();
- const dfs = (filePath: string, path: string[]): void => {
- if (recursionStack.has(filePath)) {
- // Found a cycle
- const cycleStart = path.indexOf(filePath);
- if (cycleStart !== -1) {
- cycles.push(path.slice(cycleStart));
- }
- return;
- }
- if (visited.has(filePath)) {
- return;
- }
- visited.add(filePath);
- recursionStack.add(filePath);
- const dependencies = this.getFileDependencies(filePath);
- for (const dep of dependencies) {
- dfs(dep, [...path, filePath]);
- }
- recursionStack.delete(filePath);
- };
- for (const file of files) {
- if (!visited.has(file.path)) {
- dfs(file.path, []);
- }
- }
- return cycles;
- }
- /**
- * Get complexity metrics for a node
- *
- * @param nodeId - ID of the node
- * @returns Object containing various complexity metrics
- */
- getNodeMetrics(nodeId: string): {
- incomingEdgeCount: number;
- outgoingEdgeCount: number;
- callCount: number;
- callerCount: number;
- childCount: number;
- depth: number;
- } {
- const incomingEdges = this.queries.getIncomingEdges(nodeId);
- const outgoingEdges = this.queries.getOutgoingEdges(nodeId);
- const callEdges = outgoingEdges.filter((e) => e.kind === 'calls');
- const callerEdges = incomingEdges.filter((e) => e.kind === 'calls');
- const containsEdges = outgoingEdges.filter((e) => e.kind === 'contains');
- const ancestors = this.traverser.getAncestors(nodeId);
- return {
- incomingEdgeCount: incomingEdges.length,
- outgoingEdgeCount: outgoingEdges.length,
- callCount: callEdges.length,
- callerCount: callerEdges.length,
- childCount: containsEdges.length,
- depth: ancestors.length,
- };
- }
- /**
- * Find dead code (nodes with no incoming references)
- *
- * @param kinds - Node kinds to check (default: functions, methods, classes)
- * @returns Array of unreferenced nodes
- */
- findDeadCode(kinds?: Node['kind'][]): Node[] {
- const targetKinds = kinds || ['function', 'method', 'class'];
- const deadCode: Node[] = [];
- for (const kind of targetKinds) {
- const nodes = this.queries.getNodesByKind(kind);
- for (const node of nodes) {
- // Skip exported symbols (they may be used externally)
- if (node.isExported) {
- continue;
- }
- const incomingEdges = this.queries.getIncomingEdges(node.id);
- // Filter out containment edges
- const references = incomingEdges.filter((e) => e.kind !== 'contains');
- if (references.length === 0) {
- deadCode.push(node);
- }
- }
- }
- return deadCode;
- }
- /**
- * Get subgraph containing nodes matching a filter
- *
- * @param filter - Filter function to select nodes
- * @param includeEdges - Whether to include edges between matching nodes
- * @returns Subgraph containing matching nodes
- */
- getFilteredSubgraph(
- filter: (node: Node) => boolean,
- includeEdges: boolean = true
- ): Subgraph {
- const nodes = new Map<string, Node>();
- const edges: Edge[] = [];
- // Get all nodes of common kinds
- const kinds: Node['kind'][] = [
- 'file',
- 'module',
- 'class',
- 'struct',
- 'interface',
- 'trait',
- 'function',
- 'method',
- 'variable',
- 'constant',
- 'enum',
- 'type_alias',
- ];
- for (const kind of kinds) {
- const kindNodes = this.queries.getNodesByKind(kind);
- for (const node of kindNodes) {
- if (filter(node)) {
- nodes.set(node.id, node);
- }
- }
- }
- // Include edges between matching nodes
- if (includeEdges) {
- for (const nodeId of nodes.keys()) {
- const outgoing = this.queries.getOutgoingEdges(nodeId);
- for (const edge of outgoing) {
- if (nodes.has(edge.target)) {
- edges.push(edge);
- }
- }
- }
- }
- return {
- nodes,
- edges,
- roots: [],
- };
- }
- /**
- * Access the underlying traverser for direct traversal operations
- */
- getTraverser(): GraphTraverser {
- return this.traverser;
- }
- }
|